Skiplist跳表详解及其模拟实现

article/2025/8/7 21:56:10

文章目录

  • 跳表
    • 1.跳表的概念
    • 2.Skiplist在插入时采用随机层数的方法是如何保证效率的呢?
    • 3.跳表的模拟实现
    • 4.跳表VS平衡搜索树和哈希表

跳表

1.跳表的概念

 跳表是基于有序链表扩展实现的。对于一个普通的有序链表,我们查找数据的时间复杂度是O(N)。而跳表的出现,让当前结点可能有能力跳过多个结点,以此来减少不必要的查找与比较,从而提高效率,跳表的时间复杂度是O(logN)

跳表的发展过程:

  1. 假设我们让每相邻的两个结点升高一层,增加一个指针,让该指针指向下下个节点,如下图b中所示。这样所有新增加的指针又形成了一个新的链表(不同层级的),它包含的结点数目是原来的一般。因此在进行数据查找的时候,我们不需要与链表重的每个结点进行比较了,需要比较的结点数目变为了原来的一半(大约)。
  2. 我们可以在2层链表的基础上进行进一步的扩展,让每相邻的两个结点(都是2层链表的结点)升高一层,增加一个指针,从而产生三层链表,如下图c中所示。这样搜索效率进一步提高了。
Clip_20221024_105859

 跳表的这个提高链表层级的思路非常类似于二分查找,这使得它的查找时间复杂度可以降低到O(logN),因为每提高一层,搜索的效率就会提高一些。

 但是这个结构在插入或删除数据的时候是存在很大缺陷的,插入或删除数据后,会打乱相邻两层链表上结点个数的严格的比例关系(2:1关系),因此如果要进行调整的话,也必须要把后面的所有结点都重新调整,这样会使时间复杂度退化为O(N)。

  1. 为了避免上述的问题,Skiplist在设计的时候不再要求严格的按照比例关系了。它要求在插入一个结点的时候,随机出一个层数。这样每次插入和删除的时候不需要再考虑其它结点的层数了。下图就是使用随机层数后的插入过程。
Clip_20221024_105453

2.Skiplist在插入时采用随机层数的方法是如何保证效率的呢?

 在此之前,我们先来了解一下Skiplist是如何实现随机层数的方法吧!

实现随机层数的方法:

Clip_20221024_134608

在Redis的Skiplist的实现中,参数maxLevel和p的取值如下:

maxLevel = 32
p = 0.25

maxLevel表示最大层数,p表示每多一层的概率。

设当前层数level=1
产生层数为1的概率为(1 - p), 产生大于1层的概率为p
产生层数为2的概率为p * (1 - p),产生大于2层的概率为p^2
产生层数为3的概率为p^2 * (1 - p),产生大于3层的概率为p^3
产生层数为4的概率为p^3 * (1 - p),产生大于4层的概率为p^4

产生层数为n的概率为p^(n-1) * (1 - p),产生大于n层的概率为p^n

一个结点的平均层数(即,每个结点包含的平均指针数目),计算如下:

Clip_20221024_140043
  • 当p = 0.25时,每个结点的平均层数为1.33层。(每个结点包含的平均指针个数为1.33)
  • 当p = 0.5时, 每个节点的平均层数为2层。 (每个节点包含的平均指针个数为2)

3.跳表的模拟实现

struct SkiplistNode{int val;vector<SkiplistNode*> nextV;SkiplistNode(int num, int level):val(num),nextV(level, nullptr){}
};class Skiplist {
public:Skiplist() :head(new SkiplistNode(-1, 1))  //头结点处我们初始化它是1层的(其实也可以直接拉到32层maxLevel,不过性能略有损耗){srand(time(nullptr));   //生成随机数种子}bool search(int target) {SkiplistNode* cur = head;int level = cur->nextV.size() - 1;  //层数的下标//当level小于0时退出循环while(level >= 0){//1. 当前结点的下一个为nullptr 或 下一个的值 > target, 此时向下走if(cur->nextV[level] == nullptr || cur->nextV[level]->val > target){--level;}//2. 当前结点的下一个不为nullptr && 下一个的值 < target, 此时向右走else if(cur->nextV[level]->val < target){cur = cur->nextV[level];}//3. 下一个的值 == targetelse{return true;}}return false;}//找prevV, add和erase均需要用vector<SkiplistNode*> findPrevVector(int target){SkiplistNode* cur = head;int level = cur->nextV.size() - 1;  //下面使用的都是下标, 所以这里作-1处理vector<SkiplistNode*> prevV(level + 1, head); //这里的prevV数组中, "i下标处存储的是第i+1层"的前一个节点//这里prevV默认初始化必须为head, 因为SkipList为空时, 插入的前一个节点一定是headwhile(level >= 0){//1. 当下一个为nullptr 或 下一个的值 >= target, 此时向下走。(这里考虑允许数据冗余了)if(cur->nextV[level] == nullptr || cur->nextV[level]->val >= target){prevV[level] = cur; //prevV[level]存的是cur!!!--level;}else{//2. 当下一个不为nullptr && 下一个的值 < targetcur = cur->nextV[level];}}return prevV;}void add(int num) {int level = randomLevel();  //随机生成层数if(level > head->nextV.size()){ //判断是否需要更新头结点的层数head->nextV.resize(level, nullptr);}vector<SkiplistNode*> prevV = findPrevVector(num);SkiplistNode* newNode = new SkiplistNode(num, level);//更新连接关系for(size_t i = 0; i < level; ++i){newNode->nextV[i] = prevV[i]->nextV[i]; //prevV[i]: i下标就是第i+1层的前一个结点prevV[i]->nextV[i] = newNode;}}bool erase(int num) {vector<SkiplistNode*> prevV = findPrevVector(num);//先判断num值是否存在. 判断prevV[0]的下一个是否为nullptr, 以及下一个值是否为num//这里必须使用0下标, 其它下标的层数可能不存在if(prevV[0]->nextV[0] == nullptr || prevV[0]->nextV[0]->val != num){return false;}else{//删除, 并更新连接关系SkiplistNode* delNode = prevV[0]->nextV[0];for(size_t i = 0; i < delNode->nextV.size(); ++i){prevV[i]->nextV[i] = delNode->nextV[i];}delete delNode;//判断是否需要更新头结点nextV的大小(无关紧要)size_t i = head->nextV.size() - 1;for(; i >= 0; --i){if(head->nextV[i] != nullptr)break;}head->nextV.resize(i + 1);return true;}}int randomLevel(){int level = 1;//rand()的概率是[0, RAND_MAX], 我们这里限定rand() < RAND_MAX * p, 这个概率正好为pwhile(rand() <= RAND_MAX * p && level < maxLevel){++level;}return level;}private:SkiplistNode* head;size_t maxLevel = 32;double p = 0.25;
};

4.跳表VS平衡搜索树和哈希表

 跳表本质是一种搜索结构,它根平衡搜索树和哈希表是同一领域的,它同时支持key或key/value的搜索模型。

  1. Skiplist相比平衡搜索树(AVL、红黑树),遍历数据时都可以做到有序输出,时间复杂度也差不多。
    Skiplist的优势:
    a. Skiplist的实现更简单,容易控制; 平衡搜索树的增删查改都较为复杂。
    b. Skiplist的额外空间消耗更低,Skiplist在p=0.25时,每个结点包含的指针数目为1.33; 而平衡搜索树每个结点都是一个三叉链(存储3个指针),并且还需要额外存储平衡因子/颜色属性。
  2. Skiplist相比哈希表就没什么优势了。哈希表的时间复杂度为O(1), 要比Skiplist快一些,但是Skiplist的空间消耗要少一些。
    Skiplist的优势:
    a. 遍历数据有序。
    b. Skiplist空间消耗少一些,哈希表存在表空间的消耗以及每个下标处挂的链表的指针消耗。
    c. 哈希表扩容时有性能损耗。
    d. 哈希表在极端场景下(哈希冲突高),效率会大幅度下降,此时需要使用红黑树来补救。

http://chatgpt.dhexx.cn/article/9OKgaT6Y.shtml

相关文章

skiplist - 跳表

一 前言 跳表(skiplist、跳跃表) 是一个很优秀的数据结构&#xff0c;比如用于 Redis、levelDB等出名的开源项目上。跳表在原有的有序链表上面增加了多级索引&#xff0c;通过索引来实现快速查找。 它的结构特点在名称能很好的体现出来&#xff0c;就像兔子一样&#xff0c;蹦…

Leveldb skiplist 实现及解析

skiplist 原理介绍 skiplist 由William Pugh 在论文Skip Lists: A Probabilistic Alternative to Balanced Trees 中提出的一种数据结构,skiplist 是一种随机化存储的多层线性链表结构,插入,查找,删除的都是对数级别的时间复杂度。skiplist 和平衡树有相同的时间复杂度,但…

跳表SkipList介绍与实现

目录 一.跳表介绍 二.实现思路 &#xff08;一&#xff09;.结点结构 &#xff08;二&#xff09;.检索 &#xff08;三&#xff09;.插入 &#xff08;四&#xff09;.删除 三.实现代码 一.跳表介绍 跳表是一种随机化数据结构&#xff0c;主要用于快速检索数据。实质上…

SkipList(跳表)

SkipList(跳表) 文章目录 SkipList(跳表)参考前言跳表的原理跳表的插入和删除插入操作删除操作 跳表的时间空间复杂度分析时间复杂度空间复杂度 调表的基本操作插入数据删除数据 Go 实现小结 参考 https://juejin.cn/post/6844903955831619597#heading-2https://blog.csdn.net…

每日一博 - 如何理解跳表(SkipList)

文章目录 什么是跳跃表SkipList跳表关键字Why Skip ListCode跳表-查询跳表-删除跳表-插入 小结完整Code 什么是跳跃表SkipList 跳跃表(简称跳表)由美国计算机科学家William Pugh于1989年发明 论文&#xff1a; Skip lists: a probabilistic alternative to balanced trees 跳…

Skiplist(跳表)实现

前言&#xff1a;跳表本质是一种查找结构&#xff0c;相较于AVL树、红黑树、哈希表&#xff0c;实现起来要更加简单&#xff0c;而且效率也不差。 一.Skiplist概念 跳表本质也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff0c;跟平衡搜索树&#xff08;AVL树、红…

跳表(SkipList)设计与实现(java)

微信搜一搜「bigsai」关注这个有趣的程序员&#xff0c;一起做个朋友 新人原创公众号&#xff0c;求支持一下&#xff01;你的点赞三连肯定对我至关重要&#xff01; 文章已收录在 我的Github bigsai-algorithm 欢迎star 前言 跳表是面试常问的一种数据结构&#xff0c;它在很…

SkipList和java中ConcurrentSkipListMap的实现

文章目录 简介SkipListConcurrentSkipListMapSkipList的实现concurrent的实现 总结 SkipList和java中ConcurrentSkipListMap的实现 简介 一开始听说SkipList我是一脸懵逼的&#xff0c;啥&#xff1f;还有SkipList&#xff1f;这个是什么玩意。 后面经过我的不断搜索和学习&a…

跳表(SkipList)及ConcurrentSkipListMap源码解析

二分查找和AVL树查找 二分查找要求元素可以随机访问&#xff0c;所以决定了需要把元素存储在连续内存。这样查找确实很快&#xff0c;但是插入和删除元素的时候&#xff0c;为了保证元素的有序性&#xff0c;就需要大量的移动元素了。 如果需要的是一个能够进行二分查找&#…

Redis数据结构之——跳表skiplist

写在前面 以下内容是基于Redis 6.2.6 版本整理总结 一、跳表&#xff08;skiplist&#xff09; 如何理解跳表&#xff1f;在了解跳表之前&#xff0c;我们先从普通链表开始&#xff0c;一点点揭开跳表的神秘面纱~ 首先&#xff0c;普通单链表来说&#xff0c;即使链表是有序…

redis中ziplist,skiplist

ziplist压缩表 ziplist主要是为了节约内存&#xff0c;他将元素存储在一块连续的内存空间中&#xff0c;这样在查询数据的时候也可以利用CPU的缓存访问数据&#xff0c;加快查询的效率 相较于数组而言。我们知道,数组要求每个元素的大小都相同,如果我们要存储不同长度的字符串…

跳表-skiplist的简单实现

文章目录 1、什么是跳表-skiplist2、skiplist的效率如何保证&#xff1f;3、skiplist的实现4、skiplist跟平衡搜索树和哈希表的对比 1、什么是跳表-skiplist skiplist本质上也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff0c;跟平衡搜索树和哈希表的价值是一样…

跳表:Skiplist原理介绍和优缺点

skiplist介绍 不要求上下相邻两层链表之间的节点个数有严格的对应关系&#xff0c;而是为每个节点随机出一个层数(level)。比如&#xff0c;一个节点随机出的层数是3&#xff0c;那么就把它链入到第1层到第3层这三层链表中。为了表达清楚&#xff0c;下图展示了如何通过一步步的…

skiplist 跳跃表详解及其编程实现

skiplist介绍 跳表(skip List)是一种随机化的数据结构&#xff0c;基于并联的链表&#xff0c;实现简单&#xff0c;插入、删除、查找的复杂度均为O(logN)。跳表的具体定义&#xff0c; 请参考参考维基百科 点我&#xff0c;中文版。跳表是由William Pugh发明的&#xff0c;这…

浅析SkipList跳跃表原理及代码实现

转载请注明&#xff1a;http://blog.csdn.net/ict2014/article/details/17394259 SkipList在leveldb以及lucence中都广为使用&#xff0c;是比较高效的数据结构。由于它的代码以及原理实现的简单性&#xff0c;更为人们所接受。我们首先看看SkipList的定义&#xff0c;为什么叫…

跳表(skiplist)的理解

听到跳表(skiplist)这个名字,既然是list,那么应该跟链表有关。 跳表是有序链表,但是我们知道,即使对于排过序的链表,我们对于查找还是需要进行通过链表的指针进行遍历的,时间复杂度很高依然是O(n),这个显然是不能接受的。是否可以像数组那样,通过二分法进行查找呢,但…

SkipList详解

本文参考&#xff1a;《大数据日知录》 概念 SkipList是一种用来代替平衡树的数据结构。 虽然在最坏的情况下SkipList的效率要低于平衡树&#xff0c;但是大多数情况下效率仍然非常高&#xff0c;其插入、删除、查找的时间复杂度都是O(log(N))。 除了高效外&#xff0c;其实现…

跳表(skipList)

一、为何有skipList这种数据结构的出现 我们知道二分查找算法之所以能达到 O(logn) 这样高效的一个重要原因在于它所依赖的数据结构是数组&#xff0c;数组支持随机访问一个元素&#xff0c;通过下标很容易定位到中间元素。而链表是不支持随机访问的&#xff0c;只能从头到尾依…

跳表 skiplist 简介

跳表 skiplist 跳表 (Skip List) 是由 William Pugh 在 1990 年发表的文章 Skip Lists: A Probabilistic Alternative toBalanced Trees 中描述的一种查找数据结构&#xff0c;支持对数据的快速查找&#xff0c;插入和删除。 对于 AVL 树、红黑树等平衡树&#xff0c;在插入过…

SkipList(跳跃表)详解

Introduction: skiplist本质上也是一种查找结构&#xff0c;用于解决算法中的查找问题&#xff08;Searching&#xff09;&#xff0c;即根据给定的key&#xff0c;快速查到它所在的位置&#xff08;或者对应的value&#xff09; 一般用于解决查找问题的数据结构分为两个大类…