c++如何实现跳表(skiplist)

 更新时间:2020年08月12日 14:45:58   作者:evenleo  
这篇文章主要介绍了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++ 跳表的资料请关注脚本之家其它相关文章!

相关文章

  • 一篇文章带你了解C语言的选择结构

    一篇文章带你了解C语言的选择结构

    这篇文章主要为大家介绍了C语言的选择结构,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-01-01
  • C++中基类和派生类之间的转换实例教程

    C++中基类和派生类之间的转换实例教程

    这篇文章主要介绍了C++中基类和派生类之间的转换,有助于深入理解C++面向对象程序设计,需要的朋友可以参考下
    2014-08-08
  • C++11中union的使用方法示例

    C++11中union的使用方法示例

    这篇文章主要给大家介绍了关于C++11中union的使用方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2018-09-09
  • 八皇后问题的相关C++代码解答示例

    八皇后问题的相关C++代码解答示例

    这篇文章主要介绍了八皇后问题的相关C++代码解答示例,文中包括ACM竞赛的八皇后相关知识的练习实例,需要的朋友可以参考下
    2015-08-08
  • C语言实现2048小游戏

    C语言实现2048小游戏

    这篇文章主要为大家详细介绍了C语言实现2048小游戏,注释清晰,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-05-05
  • C++ Boost Spirit精通教程

    C++ Boost Spirit精通教程

    Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
    2022-11-11
  • C++多线程实现电子词典

    C++多线程实现电子词典

    这篇文章主要为大家详细介绍了C++多线程实现电子词典,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-03-03
  • C语言深入探索之单链表与typedef的用法

    C语言深入探索之单链表与typedef的用法

    typedef为C语言的关键字,作用是为一种数据类型定义一个新名字,单链表是后面要学的双链表以及循环链表的基础,要想继续深入了解数据结构以及C语言,我们就要奠定好这块基石!接下来就和我一起学习吧
    2022-05-05
  • C语言使用ffmpeg和sdl实现多路音频播放

    C语言使用ffmpeg和sdl实现多路音频播放

    这篇文章主要为大家详细介绍了一种基于ffmpeg和sdl实现的音频多路混合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以参考一下
    2023-06-06
  • C++归并算法实例

    C++归并算法实例

    这篇文章主要介绍了C++归并算法,实例分析了C++实现基于归并算法合并线性表的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-07-07

最新评论