1. 程式人生 > 程式設計 >c++如何實現跳錶

c++如何實現跳錶

引言

二分查詢底層依賴的是陣列隨機訪問的特性,所以只能用陣列來實現。如果資料儲存在連結串列中,就真的沒法用二分查詢演算法了嗎?實際上,只需要對連結串列稍加改造,就可以支援類似“二分”的查詢演算法。改造之後的資料結構叫作跳錶。

定義

跳錶是一個隨機化的資料結構。它允許快速查詢一個有序連續元素的資料鏈表。跳躍列表的平均查詢和插入時間複雜度都是O(log n),優於普通佇列的O(n)。效能上和紅黑樹,AVL樹不相上下,但跳錶的原理非常簡單,目前Redis和LevelDB中都有用到。
跳錶是一種可以替代平衡樹的資料結構。跳錶追求的是概率性平衡,而不是嚴格平衡。因此,跟平衡二叉樹相比,跳錶的插入和刪除操作要簡單得多,執行也更快。

C++簡單實現

下面實現過程主要是簡單實現跳錶的過程,不是多執行緒安全的,LevelDB實現的跳錶支援多執行緒安全,用了std::atomic原子操作,本文主要是為了理解跳錶的原理,所以採用最簡單的實現。

#ifndef SKIPLIST_H
#define SKIPLIST_H

#include <ctime>
#include <initializer_list>
#include <iostream>
#include <random>

template <typename Key>
class Skiplist {
public:
 struct Node {
  Node(Key k) : key(k) {}
  Key key;
  Node* next[1]; // C語言中的柔性陣列技巧
 };

private:
 int maxLevel;
 Node* head;

 enum { kMaxLevel = 12 };

public:
 Skiplist() : maxLevel(1)
 {
  head = newNode(0,kMaxLevel);
 }

 Skiplist(std::initializer_list<Key> init) : Skiplist()
 {
  for (const Key& k : init)
  {
   insert(k);
  }
 }

 ~Skiplist()
 {
  Node* pNode = head;
  Node* delNode;
  while (nullptr != pNode)
  {
   delNode = pNode;
   pNode = pNode->next[0];
   free(delNode); // 對應malloc
  }
 }

 // 禁止拷貝構造和賦值
 Skiplist(const Skiplist&) = delete;
 Skiplist& operator=(const Skiplist&) = delete;
 Skiplist& operator=(Skiplist&&) = delete;

private:
 Node* newNode(const Key& key,int level)
 {
  /*
  * 開闢sizeof(Node) + sizeof(Node*) * (level - 1)大小的空間
  * sizeof(Node*) * (level - 1)大小的空間是給Node.next[1]指標陣列用的
  * 為什麼是level-1而不是level,因為sizeof(Node)已包含一個Node*指標的空間
  */ 
  void* node_memory = malloc(sizeof(Node) + sizeof(Node*) * (level - 1));
  Node* node = new (node_memory) Node(key);
  for (int i = 0; i < level; ++i)
   node->next[i] = nullptr;

  return node;
 }
 /*
 * 隨機函式,範圍[1,kMaxLevel],越小概率越大
 */ 
 static int randomLevel()
 {
  int level = 1;
  while (rand() % 2 && level < kMaxLevel)
   level++;

  return level;
 }

public:
 Node* find(const Key& key)
 {
  // 從最高層開始查詢,每層查詢最後一個小於key的前繼節點,不斷縮小範圍
  Node* pNode = head;
  for (int i = maxLevel - 1; i >= 0; --i)
  {
   while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
   {
    pNode = pNode->next[i];
   }
  }

  // 如果第一層的pNode[0]->key == key,則返回pNode->next[0],即找到key
  if (nullptr != pNode->next[0] && pNode->next[0]->key == key)
   return pNode->next[0];

  return nullptr;
 }

 void insert(const Key& key)
 {
  int level = randomLevel();
  Node* new_node = newNode(key,level);
  Node* prev[kMaxLevel];
  Node* pNode = head;
  // 從最高層開始查詢,每層查詢最後一個小於key的前繼節點
  for (int i = level - 1; i >= 0; --i)
  {
   while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
   {
    pNode = pNode->next[i];
   }
   prev[i] = pNode;
  }
  // 然後每層將新節點插入到前繼節點後面
  for (int i = 0; i < level; ++i)
  {
   new_node->next[i] = prev[i]->next[i];
   prev[i]->next[i] = new_node;
  }

  if (maxLevel < level) // 層數大於最大層數,更新最大層數
   maxLevel = level;
 }

 void erase(const Key& key)
 {
  Node* prev[maxLevel];
  Node* pNode = head;
  // 從最高層開始查詢,每層查詢最後一個小於key的前繼節點
  for (int i = maxLevel - 1; i >= 0; --i)
  {
   while (pNode->next[i] != nullptr && pNode->next[i]->key < key)
    pNode = pNode->next[i];
   prev[i] = pNode;
  }
  
  // 如果找到key,
  if (pNode->next[0] != nullptr && pNode->next[0]->key == key)
  {
   Node *delNode = pNode->next[0];
   // 從最高層開始,如果當前層的next節點的值等於key,則刪除next節點
   for (int i = maxLevel - 1; i >= 0; --i)
   {
    if (prev[i]->next[i] != nullptr && key == prev[i]->next[i]->key)
     prev[i]->next[i] = prev[i]->next[i]->next[i];
   }
   free(delNode); // 最後銷燬pNode->next[0]節點
  }
  
  // 如果max_level>1且頭結點的next指標為空,則該層已無資料,max_level減一
  while (maxLevel > 1 && head->next[maxLevel] == nullptr)
  {
   maxLevel--;
  }
 }
};

#endif

Redis和LevelDB選用跳錶而棄用紅黑樹的原因

  1. Skiplist的複雜度和紅黑樹一樣,而且實現起來更簡單。
  2. 在併發環境下Skiplist有另外一個優勢,紅黑樹在插入和刪除的時候可能需要做一些rebalance的操作,這樣的操作可能會涉及到整個樹的其他部分,而skiplist的操作顯然更加區域性性一些,鎖需要盯住的節點更少,因此在這樣的情況下效能好一些。

以上就是c++如何實現跳錶的詳細內容,更多關於c++ 跳錶的資料請關注我們其它相關文章!