跳表原理及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的操作顯然更加局部性一些,鎖需要盯住的節點更少,因此在這樣的情況下性能好一些。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM