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

深入理解跳表(Skip List):原理、实现与应用

目录

一、 什么是跳表?

1.1 基本思想

1.2 随机层数的引入

二、跳表的效率保证

2.1 随机层数的生成

2.2 平均层数与空间复杂度

2.3 时间复杂度

三、 跳表的实现

四、跳表 vs 平衡树 vs 哈希表

4.1 跳表的优势

4.2 跳表的劣势

五、总结

参考文献


跳表(Skip List)是一种基于概率的高效动态数据结构,支持快速查找、插入和删除操作,其时间复杂度可达到O(logN)。本文将深入探讨跳表的原理、实现细节,并通过代码示例和图表分析其性能优势。

一、 什么是跳表?

跳表(Skip List)是一种用于解决查找问题的数据结构,与平衡搜索树(如AVL树、红黑树)和哈希表具有相同的功能,可用于实现键或键值对的查找模型。跳表由William Pugh于1990年在其论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》中首次提出。

1.1 基本思想

跳表基于有序链表构建。在普通有序链表中,查找的时间复杂度为O(N)。为了提升效率,跳表引入了“多层”链表的概念:

  1. 第一层:所有节点按顺序连接。

  2. 第二层:每两个节点中选取一个作为“索引”,指向下一层的对应节点。

  3. 更高层:依此类推,每一层的节点数约为下一层的一半。

这样,查找过程类似于二分查找,时间复杂度可优化至O(logN)。

1.2 随机层数的引入

若严格保持上下两层节点数2:1的比例,插入和删除操作会破坏结构,导致重新调整的开销。跳表通过随机层数的方式解决这一问题:每个新插入节点的层数由随机函数决定,不再严格保持比例关系,从而保证操作的高效性。

二、跳表的效率保证

2.1 随机层数的生成

跳表通常设定一个最大层数maxLevel和一个概率值p。生成随机层数的伪代码如下:

def randomLevel():lvl = 1while random() < p and lvl < maxLevel:lvl += 1return lvl

Redisskiplist实现中,这两个参数的取值为:

p = 1/4

maxLevel = 32

2.2 平均层数与空间复杂度

  1. 根据前面randomLevel()的伪码,我们很容易看出,产生越高的节点层数,概率越低。定量的分析如下:

  • 节点层数至少为1。而大于1的节点层数,满足一个概率分布。 

  • 节点层数恰好等于1的概率为1-p。 

  • 节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。 

  • 节点层数大于等于3的概率为p^2,而节点层数恰好等于3的概率为p^2*(1-p)。 

  • 节点层数大于等于4的概率为p^3,而节点层数恰好等于4的概率为p^3*(1-p)。 

因此,一个节点的平均层数(也即包含的平均指针数目),计算如下:

现在很容易计算出:

当p=1/2时,每个节点所包含的平均指针数目为2;

当p=1/4时,每个节点所包含的平均指针数目为1.33。

2.3 时间复杂度

跳表的查找、插入、删除操作的时间复杂度均为O(logN),其推导过程较为复杂,需要有一定的数据功底,有兴趣的 老铁,可以参考以下文章中的讲解:

铁蕾大佬的博客:http://zhangtielei.com/posts/blog-redis-skiplist.html

William_Pugh大佬的论文:ftp://ftp.cs.umd.edu/pub/skipLists/skiplists.pdf

三、 跳表的实现

以下是一个基于C++的跳表实现示例,包含搜索、插入、删除等操作:

https://leetcode.cn/problems/design-skiplist/
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <random>
#include <chrono>struct SkiplistNode {int _val;std::vector<SkiplistNode*> _nextV;SkiplistNode(int val, int level) : _val(val), _nextV(level, nullptr) {}
};class Skiplist {typedef SkiplistNode Node;
public:Skiplist() {srand(time(0));_head = new Node(-1, 1);}bool search(int target) {Node* cur = _head;int level = _head->_nextV.size() - 1;while (level >= 0) {if (cur->_nextV[level] && cur->_nextV[level]->_val < target) {cur = cur->_nextV[level];} else if (!cur->_nextV[level] || cur->_nextV[level]->_val > target) {level--;} else {return true;}}return false;}std::vector<Node*> findPrevNodes(int num) {Node* cur = _head;int level = _head->_nextV.size() - 1;std::vector<Node*> prevV(level + 1, _head);while (level >= 0) {if (cur->_nextV[level] && cur->_nextV[level]->_val < num) {cur = cur->_nextV[level];} else {prevV[level] = cur;level--;}}return prevV;}void add(int num) {auto prevV = findPrevNodes(num);int n = randomLevel();Node* newNode = new Node(num, n);if (n > _head->_nextV.size()) {_head->_nextV.resize(n, nullptr);prevV.resize(n, _head);}for (int i = 0; i < n; i++) {newNode->_nextV[i] = prevV[i]->_nextV[i];prevV[i]->_nextV[i] = newNode;}}bool erase(int num) {auto prevV = findPrevNodes(num);if (!prevV[0]->_nextV[0] || prevV[0]->_nextV[0]->_val != num) {return false;}Node* del = prevV[0]->_nextV[0];for (size_t i = 0; i < del->_nextV.size(); i++) {prevV[i]->_nextV[i] = del->_nextV[i];}delete del;// 降低头节点层数(若最高层为空)int i = _head->_nextV.size() - 1;while (i >= 0 && _head->_nextV[i] == nullptr) i--;_head->_nextV.resize(i + 1);return true;}int randomLevel() {int level = 1;while ((rand() / (double)RAND_MAX) < _p && level < _maxLevel) {level++;}return level;}private:Node* _head;size_t _maxLevel = 32;double _p = 0.25;
};

四、跳表 vs 平衡树 vs 哈希表

特性跳表平衡树(AVL/红黑树)哈希表
时间复杂度O(logN)O(logN)O(1)(平均)
空间复杂度较低(p=1/4时≈1.33)较高(每个节点多指针)中等(需扩容)
实现难度简单复杂中等
是否有序
扩容开销

4.1 跳表的优势

  • 实现简单:相比平衡树,跳表的实现和调试更容易。

  • 空间效率高:通过调整概率p,可控制空间开销。

  • 有序性:支持有序遍历,而哈希表不具备该特性。

4.2 跳表的劣势

  • 查询速度不如哈希表:哈希表平均O(1)的查询速度更优。

  • 极端情况下性能波动:由于随机性,最坏情况下的性能可能较差(但概率极低)。

五、总结

跳表是一种简单却高效的数据结构,通过概率性的多层索引机制实现了接近平衡树的性能,同时避免了平衡树复杂的旋转操作。其在Redis、LevelDB等知名系统中的应用也证明了其实用性和可靠性。

对于需要有序性且频繁更新的场景,跳表是一个非常好的选择。尽管在查询速度上不如哈希表,但其有序性和动态性使其在众多场景中脱颖而出。


参考文献

  • William Pugh的原始论文

  • Redis作者关于跳表的讲解

  • LeetCode 1206. 设计跳表


文章转载自:

http://tMVRyrqR.qstjr.cn
http://X5pg1BDn.qstjr.cn
http://RVS6geuv.qstjr.cn
http://lB3dcoNr.qstjr.cn
http://14KQrVL3.qstjr.cn
http://EdXDO1Rm.qstjr.cn
http://iMBYrvKP.qstjr.cn
http://W7n1TJjA.qstjr.cn
http://eihyH7BS.qstjr.cn
http://4wc0ZiyZ.qstjr.cn
http://4KEYi22Y.qstjr.cn
http://2GLw4Os5.qstjr.cn
http://bvUZnQcC.qstjr.cn
http://j0Qg23hp.qstjr.cn
http://oUuKACQm.qstjr.cn
http://p4gVmJWh.qstjr.cn
http://01ap2TRh.qstjr.cn
http://MguwP0af.qstjr.cn
http://xwAdORPW.qstjr.cn
http://UKSFRk09.qstjr.cn
http://nR6Nq1i5.qstjr.cn
http://pZA3d5TM.qstjr.cn
http://f7V9qT0c.qstjr.cn
http://qV976jVs.qstjr.cn
http://YdvHFPoW.qstjr.cn
http://FCnOjCt6.qstjr.cn
http://CFe7yx4b.qstjr.cn
http://76kwmFIW.qstjr.cn
http://44CB6yhS.qstjr.cn
http://CfAEk0u6.qstjr.cn
http://www.dtcms.com/a/383258.html

相关文章:

  • SciKit-Learn 全面分析 20newsgroups 新闻组文本数据集(文本分类)
  • 使用 Neo4j 和 Ollama 在本地构建知识图谱
  • 【愚公系列】《人工智能70年》018-语音识别的历史性突破(剑桥语音的黄金十年)
  • Debezium日常分享系列之:MongoDB 新文档状态提取
  • Linux 日志分析:用 ELK 搭建个人运维监控平台
  • docker内如何用ollama启动大模型
  • Flask学习笔记(二)--路由和变量
  • FlashAttention(V3)深度解析:从原理到工程实现-Hopper架构下的注意力机制优化革命
  • 一文入门:机器学习
  • Uniswap:DeFi领域的革命性交易协议
  • 3. 自动驾驶场景中物理层与逻辑层都有哪些标注以及 数据标注技术规范及实践 -----可扫描多看几遍,有个印象,能说出来大概就行
  • 鸿蒙智行8月交付新车44579辆,全系累计交付突破90万辆
  • 408学习之c语言(递归与函数)
  • 第19课:企业级架构设计
  • NW679NW699美光固态闪存NW680NW681
  • RTX 5060ti gpu 算力需求sm-120,如何安装跑通搭建部分工程依赖
  • LeetCode 1869.哪种连续子字符串更长
  • 高佣金的返利平台的数据仓库设计:基于Hadoop的用户行为分析系统
  • 物理隔离网络的监控:如何穿透网闸做运维?
  • 知识图谱网页版可视化可移动代码
  • 【iOS】static、const、extern关键字
  • Grafana+Loki+Alloy构建企业级日志平台
  • Redis 实现分布式锁的探索与实践
  • 设计模式-适配器模式详解
  • Java 分布式缓存实现:结合 RMI 与本地文件缓存
  • Ajax-day2(图书管理)-渲染列表
  • 在Excel和WPS表格中快速复制上一行内容
  • 11-复习java程序设计中学习的面向对象编程
  • 《云计算如何驱动企业数字化转型:关键技术与实践案例》
  • LSTM 深度解析:从门控机制到实际应用