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

引言

二分查找底层依赖的是数组随机访问的特性,所以只能用数组来实现。如果数据存储在链表中,就真的没法用二分查找算法了吗?实际上,只需要对链表稍加改造,就可以支持类似“二分”的查找算法。改造之后的数据结构叫作跳表。

定义

跳表是一个随机化的数据结构。它允许快速查询一个有序连续元素的数据链表。跳跃列表的平均查找和插入时间复杂度都是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++ 跳表的资料请关注我们其它相关文章!

(0)

相关推荐

  • c++禁止函数的传值调用的方法

    代码编译运行环境:VS2017+Debug+Win32 按照参数形式的不同,C++应该有三种函数调用方式:传值调用.引用调用和指针调用.对于基本数据类型的变量作为实参进行参数传递时,采用传值调用与引用调用和指针调用的效率相差不大.但是,对于类类型来说,传值调用和引用调用之间的区别很大,类对象的尺寸越大,这种差别越大. 传值调用与后面两者的区别在于传值调用在进入函数体之前,会在栈上建立一个实参的副本,而引用和指针调用没有这个动作.建立副本的操作是利用拷贝构造函数进行的.因此,要禁止传值调用,就必须

  • C++ 使用new与delete需注意的原则

    C++的动态内存管理是通过new和delete两个操作来完成的,即用new来申请空间,用delete来释放空间.在使用new和delete时,注意以下原则. 1.new与delete需一一对应 用new操作申请空间,如果申请成功,必须在以后的某个时刻用delete释放该空间,既不能忘记释放,也不能多次释放.前者会引起内存泄露,后者会引起运行时错误.如下面的程序. #include <iostream> using namespace std; int main() { int *p; p=ne

  • 深入了解C++ 结构体(struct)与共用体(union)

    编码运行环境:VS2017+Win32+Debug,Win32表示生成32bits的应用程序. 结构体(struct)与共用体(union)是C语言中就已经存在的数据类型,C++对他们进行了扩充,最大的变化是允许在结构和公用体中定义成员函数.下面将通过实例讲解二者的特性和用法. 1.struct 以下是一个使用了结构体的C++程序. #include <iostream> using namespace std; struct Room { int floor; int No; }; stru

  • c++实现跳跃表(Skip List)的方法示例

    前言 Skip List是一种随机化的数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间).基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表(因此得名).所有操作都以对数随机化的时间进行.Skip List可以很好解决有序链表查找特定值的困难. 跳表是平衡树的一种替代的数据结构,但是和红黑树不相同的是,跳表对于树的平衡的实现是基于一种随机化的算法的,跳跃表使用概率均衡技术而不是使用

  • VSCode添加头文件(C/C++)的实现示例

    使用VSCode编译C/C++时,会存在找不到头文件的情况这时候需要设置两个地方: 1.c_cpp_properites.json 2.task.json 以下是我修改的对应的文件 { "configurations": [ { "name": "Win32", "includePath": [ "${workspaceFolder}/**", "${workspaceRoot}", &

  • c++字符串分割的方法

    C++ 中经常需要对字符串按照分隔符进行分割以获得子串序列,子串的顺序与其在原字符串中出现的顺序一致.一般有两种需求场景:  (1)给定一个分隔符(单个字符或子串)分割字符串:  (2)给定一个或多个分隔符(单个字符),分割字符串. 当给定的分隔符不在原字符串中,则原字符串不被分割,返回单个元素为原字符串的 vector. 注意,本文实现时,如果被分割后的子串为空串,则不计入最终的子串序列.比如原字符串是"a,b",分隔符为",",那么分割后的子串序列为 [&quo

  • 详解C++作用域与生命周期

    Pascal之父Nicklaus Wirth曾经提出一个公式,展示出了程序的本质:程序=算法+数据结构.后人又给出一个公式与之遥相呼应:软件=程序+文档.这两个公式可以简洁明了的为我们展示程序和软件的组成. 程序的运行过程可以理解为算法对数据的加工过程,程序的运行的结果,就是算法加工数据产生的结果数据.算法描述的是对数据加工的步骤,对应于程序中的函数.数据结构描述的是数据在计算机中的组织结构,对应于程序中的数据类型.程序中数据对应的就是无处不在变量.对于我们编程人员,面对的无非就是函数,数据类型

  • python实现跳表SkipList的示例代码

    跳表 跳表,又叫做跳跃表.跳跃列表,在有序链表的基础上增加了"跳跃"的功能,由William Pugh于1990年发布,设计的初衷是为了取代平衡树(比如红黑树). Redis.LevelDB 都是著名的 Key-Value 数据库,而Redis中 的 SortedSet.LevelDB 中的 MemTable 都用到了跳表. 对比平衡树,跳表的实现和维护会更加简单,跳表的搜索.删除.添加的平均时间复杂度是 O(logn). 跳表的结构如图所示: 可以发现,对于一个节点Node,其含有多

  • Python数据结构与算法之跳表详解

    目录 0. 学习目标 1. 跳表的基本概念 1.1 跳表介绍 1.2 跳表的性能 1.3 跳表与普通链表的异同 2. 跳表的实现 2.1 跳表结点类 2.2 跳表的初始化 2.3 获取跳表长度 2.4 读取指定位置元素 2.5 查找指定元素 2.6 在跳表中插入新元素 2.7 删除跳表中指定元素 2.8 其它一些有用的操作 3. 跳表应用 3.1 跳表应用示例 0. 学习目标 在诸如单链表.双线链表等普通链表中,查找.插入和删除操作由于必须从头结点遍历链表才能找到相关链表,因此时间复杂度均为O(

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

    引言 二分查找底层依赖的是数组随机访问的特性,所以只能用数组来实现.如果数据存储在链表中,就真的没法用二分查找算法了吗?实际上,只需要对链表稍加改造,就可以支持类似"二分"的查找算法.改造之后的数据结构叫作跳表. 定义 跳表是一个随机化的数据结构.它允许快速查询一个有序连续元素的数据链表.跳跃列表的平均查找和插入时间复杂度都是O(log n),优于普通队列的O(n).性能上和红黑树,AVL树不相上下,但跳表的原理非常简单,目前Redis和LevelDB中都有用到. 跳表是一种可以替代平

  • Java实现跳跃表(skiplist)的简单实例

    跳跃链表是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好. 基本上,跳跃列表是对有序的链表增加上附加的前进链接,增加是以随机化的方式进行的,所以在列表中的查找可以快速的跳过部分列表(因此得名).所有操作都以对数随机化的时间进行. 实现原理: 跳跃表的结构是:假如底层有10个节点, 那么底层的上一层理论上就有5个节点,再上一层理论上就有2个或3个节点,再上一层理论上就有1个节点.所以从这里可以看出每一层的节点个数为其下

  • c++如何实现跳表

    引言 二分查找底层依赖的是数组随机访问的特性,所以只能用数组来实现.如果数据存储在链表中,就真的没法用二分查找算法了吗?实际上,只需要对链表稍加改造,就可以支持类似"二分"的查找算法.改造之后的数据结构叫作跳表. 定义 跳表是一个随机化的数据结构.它允许快速查询一个有序连续元素的数据链表.跳跃列表的平均查找和插入时间复杂度都是O(log n),优于普通队列的O(n).性能上和红黑树,AVL树不相上下,但跳表的原理非常简单,目前Redis和LevelDB中都有用到. 跳表是一种可以替代平

  • Java数据结构之实现跳表

    1.跳表的定义 跳跃表是一种随机化数据结构,基于并联的链表,其效率可比拟于二叉查找树(对于大多数操作需要O(log n)平均时间),并且对并发算法友好. SkipList(跳表)是一种可以代替平衡树的数据结构,默认是按照Key值升序的.SkipList让已排序的数据分布在多层链表中,以0-1随机数决定一个数据的向上攀升与否,通过"空间来换取时间"的一个算法,在每个节点中增加了向前的指针,在插入.删除.查找时可以忽略一些不可能涉及到的结点,从而提高了效率. 在Java的API中已经有了实

  • 为何Redis使用跳表而非红黑树实现SortedSet

    目录 什么是跳表 跳表的意义究竟在于何处? 跳表的搜索时间复杂度 跳表是不是很费内存? 插入和删除的时间复杂度 插入 删除 跳表索引动态更新 跳表的代码实现(Java 版) 数据结构定义 搜索算法 插入和删除算法 插入 删除 知道跳表(Skip List)是在看关于Redis的书的时候,Redis中的有序集合使用了跳表数据结构.接着就查了一些博客,来学习一下跳表.后面会使用Java代码来简单实现跳表. 什么是跳表 跳表由William Pugh发明,他在论文<Skip lists: a prob

  • 简单谈谈Mysql索引与redis跳表

    摘要 面试时,交流有关mysql索引问题时,发现有些人能够涛涛不绝的说出B+树和B树,平衡二叉树的区别,却说不出B+树和hash索引的区别.这种一看就知道是死记硬背,没有理解索引的本质.本文旨在剖析这背后的原理,欢迎留言探讨 问题 如果对以下问题感到困惑或一知半解,请继续看下去,相信本文一定会对你有帮助 mysql 索引如何实现 mysql 索引结构B+树与hash有何区别.分别适用于什么场景 数据库的索引还能有其他实现吗 redis跳表是如何实现的 跳表和B+树,LSM树有和区别呢 解析 首先

  • Redis高效率原因及数据结构分析

    目录 1.什么是redis?它主要用来干什么的? 2.redis为什么这么快? 基于内存存储实现 高效的数据结构 1.SDS简单动态字符串 2.字典 3.跳表 合理的数据编码 合理的线程模型 1.I/O多路复用 2.什么是I/O多路复用? 3.单线程模型 虚拟内存机制 Redis的虚拟内存机制是啥呢? 1.什么是redis?它主要用来干什么的? Redis,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写.支持网络.可基于内存亦可持

  • Redis ziplist 压缩列表的源码解析

    目录 前言 源码解读 ziplist 布局 entry 节点 prelen encoding 编码 总结 前言 相信对使用过 Redis 的人来说,数据类型 List 是不会陌生的吧.大多数人需要实现一个队列时候,首选的就是 List 了.但是其实 Redis 的 List 类型有多种实现方式.这篇文章就是介绍其中一种实现 ziplist - 压缩列表. 源码解读 一如既往,关于 ziplist 的定义和实现还是放在一对文件中,分别是 ziplist.h 和 ziplist.c.在 ziplis

随机推荐