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++ 跳表的资料请关注我们其它相关文章!

(0)

相关推荐

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

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

  • c++ 如何合并两个有序链表

    1.题目要求 这是一道求职面试时经常要求手写或者机试的经典题目. 已知两个链表head1和head2各自有序,请把它们合并成一个链表依然有序.结果链表要包含head1和head2的所有节点,即使节点值相同. 注意:不能开辟新空间来存储合并后的链表.如果第一次做该题,很容易会想到使用新链表来存储合并后的有序链表.虽然可以如此实现,但是不符合常规解法和面试官的要求. 2.非递归实现 算法过程:  输入:两个有序的单链表head1与head2:  输出:合并后的有序单链表mergeHead:  算法描

  • 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++野指针的原理以及避免方法

    1.定义 指向非法的内存地址指针叫作野指针(Wild Pointer),也叫悬挂指针(Dangling Pointer),意为无法正常使用的指针. 2.出现野指针的常见情形 2.1使用未初始化的指针 出现野指针最典型的情形就是在定义指针变量之后没有对它进行初始化,如下面的程序. #include <iostream> using namespace std; int main() { int* p; cout<<*p<<endl; //编译通过,运行时出错 } 2.2指

  • 解决vscode下调试c/c++程序一闪而过的问题(Windows)

    起因 开始学习c语言了,一开始想的就是直接装个VS就完了,但是一搜都是说vs臃肿啥啥不好,不如用vscode来整,多轻量~于是就网上搜了许多教程开整了,期间也遇到了一些常见的坑,这些集中写个文,希望可以帮助到大家.配置文件也是来自其他文章的,我也进行了部分修改,后面会标出. 解决方法 我们知道使用vscode对c/c++进行调试的本质是这样的--1.GCC编译代码,2.vscode运行编译出的程序.因此调试运行程序一闪而过的本质就是命令行程序运行完结果直接自动关闭了,这和c/c++程序本身也有关

  • c++如何实现Base64算法

    Base64用途 1.用于对SOHO级路由器(网关设备)管理员帐户密码的加密 2.流媒体网站对于播放的流媒体文件的路径的加密 3.迅雷等下载软件对下载链接地址的加密 Base64算法 Base64编码要求把3个8位字节(3*8=24)转化为4个6位的字节(4*6=24),之后在6位的前面补两个0,形成8位一个字节的形式. Base64类 函数: unsigned int CreateMatchingEncodingBuffer (unsigned int p_InputByteCount, ch

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

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

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

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

  • c++如何实现跳表

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

  • Java数据结构之实现跳表

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

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

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

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

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

  • 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中都有用到. 跳表是一种可以替代平

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

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

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

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

  • 详解Redis数据结构之跳跃表

    1.简介 我们先不谈Redis,来看一下跳表. 1.1.业务场景 场景来自小灰的算法之旅,我们需要做一个拍卖行系统,用来查阅和出售游戏中的道具,类似于魔兽世界中的拍卖行那样,还有以下需求: 拍卖行拍卖的商品需要支持四种排序方式,分别是:按价格.按等级.按剩余时间.按出售者ID排序,排序查询要尽可能地快.还要支持输入道具名称的精确查询和不输入名称的全量查询. 这样的业务场景所需要的数据结构该如何设计呢?拍卖行商品列表是线性的,最容易表达线性结构的是数组和链表.假如用有序数组,虽然查找的时候可以使用

随机推荐