当前位置: 首页 > news >正文

5.跳表(skiplist)

1. 什么是跳表 -skiplist
skiplist 本质上也是一种查找结构,用于解决算法中的查找问题,跟平衡搜索树和哈希表的价值是一样的,可以作为key 或者 key/value 的查找模型。
skiplist ,顾名思义,首先它是一个 list 。实际上,它是在有序链表的基础上发展起来的。如果是一个有序的链表,查找数据的时间复杂度是O(N)。

设计思路: 

William Pugh 开始的优化思路:
1. 假如我们 每相邻两个节点升高一层,增加一个指针,让指针指向下下个节点 ,如下图 b
示。这样所有新增加的指针连成了一个新的链表,但它包含的节点个数只有原来的一半。由
于新增加的指针,我们不再需要与链表中每个节点逐个进行比较了,需要比较的节点数大概
只有原来的一半。
2. 以此类推,我们可以在第二层新产生的链表上,继续为每相邻的两个节点升高一层,增加一个指针,从而产生第三层链表。如下图c ,这样搜索效率就进一步提高了。
3. skiplist 正是受这种多层链表的想法的启发而设计出来的。实际上,按照上面生成链表的方
式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常 类似
二分查找 ,使得查找的时间复杂度可以降低到 O(log n) 。但是这个结构在插入删除数据的时
候有 很大的问题 ,插入或者删除一个节点之后,就会打乱上下相邻两层链表上节点个数严格
2:1 的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点(也
包括新插入的节点)重新进行调整,这会让时间复杂度重新蜕化成 O(n)

为了解决插入时维持严格的对应关系问题:

解决方法:插入节点时随机给一个层数。

2.skiplist的效率如何保证?

一般跳表会设计一个最大层数 maxLevel 的限制,其次会设置一个多增加一层的概率p
其中要求random()产生的随机数等概率均匀分布。
Redis skiplist 实现中,这两个参数的取值为:p = 0.25,maxlevel = 32。
跳表的平均时间复杂度为 O(logN)。(待拓展)

3.skiplist实现

1206. 设计跳表 - 力扣(LeetCode)

search设计:

具体实例:以查找20为例,从头结点出发,从最高层开始比较,6比20小,往6跳;6的最高层为空,往下走一层25比20大,继续往下走一层;9比20小,往9跳;17比20小,往17跳;17最高层25比20大,往下走一层;19比20小,往19跳,19最高层21比20大,往下走一层,但是此时没有下一层了,找不到20

思路:从头结点出发,从最高层开始比较,

1)如果 当前层为空当前层下一个比查找的值大,往下走一层;

2)如果当前层下一个等于要查找的值,返回true;

3)如果当前层下一个小于要查找的值,往当前层的下一个跳。

直到往下走的过程中没有下一层了,那就找不到了,返回false。

代码实现如下:

bool search(int target)
{Node* cur = _head;int level = _head->_nextv.size() - 1;while (level >= 0){//大于,往后跳if (cur->_nextv[level] && target > cur->_nextv[level]->_val){cur = cur->_nextv[level];}//为空或小于,往下走else if (cur->_nextv[level] == nullptr || target < cur->_nextv[level]->_val){--level;}//等于else{return true;}}return false;
}

add设计(允许键值冗余):

分析:类似查找逻辑,从头结点出发,每次从最高层开始,但不同的是如果遇到相等的值,也要往下走(因为要记录前一个),并记录前一个。

实现思路:定义一个prevV,存放对应层数前一个节点的指针,都初始化成头结点指针。

1.记录prevV

cur从头结点出发,level每次从最高层开始,

1)如果 当前层为空当前层下一个大于等于查找的值,更新prevV对应的当前层节点指针为cur,level往下走;

2)如果当前层下一个小于要查找的值,往当前层的下一个跳。

直到往下走的过程中没有下一层了,那就结束了,prevV[0]第一层即为要插入节点的最接近一个。

2.创建新节点,链接新节点与prevV,可能修正头结点

1)如果新的节点层数超出头结点层数,头结点增加层数至与新节点一致,同时prevV也要扩至与新节点层数一致。

2)链接前后指针。(头结点若更新了,也会被修正)

由于删除也要复用记录prevV的逻辑,因此提取出来写个函数。

代码实现如下:

void add(int num)
{vector<Node*> prevV = getPrevV(num);int n = getLevel();Node* newnode = new Node(num, n);//如果超出头节点层数,修正头节点和prevVif ((unsigned)n > _head->_nextv.size()){_head->_nextv.resize(n, nullptr);prevV.resize(n, _head);}//若头结点层数变了,也会更新头结点的指针for (size_t i = 0; i < (unsigned)n; ++i){newnode->_nextv[i] = prevV[i]->_nextv[i];prevV[i]->_nextv[i] = newnode;}
}vector<Node*> getPrevV(int num)
{Node* cur = _head;int level = _head->_nextv.size() - 1;vector<Node*> prevV(level + 1, _head);while (level >= 0){//大于,往后跳if (cur->_nextv[level] && num > cur->_nextv[level]->_val){cur = cur->_nextv[level];}//为空或小于等于,记录前一个节点指针,往下走else if (cur->_nextv[level] == nullptr || num <= cur->_nextv[level]->_val){prevV[level] = cur;--level;}}return prevV;
}

erase设计:

实现思路:定义一个prevV,存放对应层数前一个节点的指针,都初始化成头结点指针。

1.记录prevV

cur从头结点出发,level每次从最高层开始,

1)如果 当前层为空当前层下一个大于等于查找的值,更新prevV对应的当前层节点指针为cur,level往下走;

2)如果当前层下一个小于要查找的值,往当前层的下一个跳。

直到往下走的过程中没有下一层了,那就结束了,prevV[0]第一层即为要插入节点的最接近一个。

2.删除节点,链接前后指针,若头结点层数降了,修正

拿到需要删除的节点的指针del,为prevV[0]的第一层的下一个。若 del为空 或 del->val不等于要删除的值,返回false。

1)链接前后指针。

2)从头结点的最高层开始,如果当前层的下一个为空,--level;反之终止。

直到头节点层数被降为1 或者 已经降到不能再降了,循环终止。

头结点resize新的层数,降低层数。

代码实现如下:

bool erase(int num)
{vector<Node*> prevV = getPrevV(num);//prevV[0]是被删除元素的前一个Node* del = prevV[0]->_nextv[0];if (del == nullptr || del->_val != num){return false;}//prevV->_next  del del->_nextfor (size_t i = 0; i < del->_nextv.size(); ++i){prevV[i]->_nextv[i] = del->_nextv[i];}delete del;//修正头结点的层数int level = _head->_nextv.size() - 1;//修正直到层数为1 或 不出现空了while (level > 0){if (_head->_nextv[level] == nullptr)--level;elsebreak;}_head->_nextv.resize(level + 1);return true;
}

getLevel(randomLevel)设计:

1)用c的rand和srand,利用几何概型,[0,RAND_MAX*p]占[0,RADN_MAX]的p,概率为p。

int getLevel()
{int level = 1;while (rand() <= RAND_MAX * _p && level < _maxLevel){++level;}return level;
}

2)用c++的库,头文件<random>,<chrono>

int getLevel()
{static std::default_random_enginegenerator(std::chrono::system_clock::now().time_since_epoch().count());static std::uniform_real_distribution<double> distribution(0.0, 1.0);size_t level = 1;while (distribution(generator) <= _p && level < _maxLevel){++level;}return level;
}
4.skiplist 跟平衡搜索树和哈希表的对比
摘至文献:
Skip lists are a data structure that can be used in place of balanced trees.
Skip lists use probabilistic balancing rather than strictly enforced balancing and as a result the algorithms for insertion and deletion in skip lists are much simpler and significantly faster than equivalent algorithms for balanced trees.
跳表是一个可以被用来代替平衡搜索树的数据结构。
跳表用基于概率的平衡而不是严格的平衡,事实上,插入和删除在跳表中比起等效平衡树的算法更简单,而且更快。 

skiplist跟平衡搜索树和哈希表的对比:

1. skiplist 相比平衡搜索树 (AVL 树和红黑树 ) 对比,都可以做到遍历数据有序,时间复杂度也差
不多。
skiplist 的优势是:
a skiplist 实现简单,容易控制。平衡树增删查改遍历都更复杂。
b skiplist 的额外空间消耗更低。平衡树节点存储每个值有三叉链,平衡因子 / 颜色等消耗。
skiplist p=1/2 时,每个节点所包含的平均指针数目为 2 skiplist p=1/4 时,每个节点所包
含的平均指针数目为 1.33
2. skiplist 相比哈希表而言,就没有那么大的优势了。相比而言
a 、哈希表平均时间复杂度是O(1),比 skiplist 快。
b 、哈希表空间消耗略多一点。
skiplist 优势如下:
a 、遍历数据有序
b skiplist 空间消耗略小一点,哈希表存在链接指针和表空间消耗。
c 、哈希表扩容有性能损耗。
d 、哈希表再极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力。

相关文章:

  • C++ | STL之list详解:双向链表的灵活操作与高效实践
  • 【项目管理】第17章 项目干系人管理-- 知识点整理
  • GEO供应商盈达科技发布:AI信源占位白皮书​
  • IDEA类图标识
  • AI驱动SEO关键词实战策略
  • 基于RV1126开发板的rknn-toolkit-lite使用方法
  • 【Docker-13】Docker Container容器
  • Google提示工程
  • Active Directory域服务管理与高级应用技术白皮书
  • Linux 深入浅出信号量:从线程到进程的同步与互斥实战指南
  • Leetcode 3514. Number of Unique XOR Triplets II
  • python爬虫 线程,进程,协程
  • Oracle数据库数据编程SQL<01. 课外关注:数据库查重方法全面详解>
  • Linux指令和权限(10-3)
  • 聚铭网络亮相2025超云产品技术大会,联合发布“铭智安全运营大模型一体机及解决方案”
  • Rust 之五 所有权、.. 和 _ 语法、引用和切片、Vec<T>、HashMap<K, V>
  • MIT6.S081 - Lab8 Locks(锁优化 | 并发安全)
  • HTTP请求方法安全剖析(不安全的网络请求方法):从CVE-2017-12615看PUT/DELETE的风险利用
  • JavaScript的常用数组API原理
  • jspm企业采购管理系统的设计与实现(源码+lw+部署文档+讲解),源码可白嫖!
  • 纪念沈渭滨︱“要把近代史搞得会通”——读《士与大变动时代》随札
  • 全总:五一拟表彰全国劳模先进工作者2426名,盛李豪入选
  • 吸引更多开发者,上海智元发布行业首款具身智能一站式开发平台
  • 徐之凯评《突如其来的勇气》|早熟的抵抗
  • 路面突陷大坑致车毁人亡,家属称不知谁来管,长治当地回应
  • 沃尔沃中国公开赛夺冠,这是吴阿顺与上海的十年之约