跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

详解数据结构之跳表

综述由AI生成跳表(Skip List)这一数据结构。跳表由 William Pugh 发明,是一种允许快速查询、插入和删除有序元素的链表结构。文章阐述了跳表的演化过程,即通过建立多级索引来提高查询效率,将时间复杂度从 O(n) 降低至 O(log n)。同时分析了跳表的优化思路,采用随机层数策略避免维护严格的节点比例关系,从而保证插入删除的高效性。文中还给出了跳表的时间复杂度、空间复杂度分析,以及查找、插入、删除的具体操作步骤和 C++ 模拟实现代码。最后对比了跳表与平衡搜索树及哈希表的优劣,指出跳表在实现简单性和空间消耗上的优势,以及在有序遍历方面的特性。

萤火微光发布于 2026/3/29更新于 2026/5/2325 浏览
详解数据结构之跳表

跳表的定义

跳表是由 William Pugh(音译为威廉·普) 发明的,最早出现于他在 1990 年发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》。跳表全称为跳跃列表,它允许快速查询、插入和删除一个有序连续元素的数据链表。

文章配图

跳表的演化过程

对于单链表来说,即使数据是已经排好序的,想要查询其中的一个数据,只能从头开始遍历链表,这样效率很低,时间复杂度很高,是 O(n)。

文章配图

为了提高查询的效率,我们可以为链表建立一个'索引'。在原始链表的基础上,每两个结点提取一个结点建立索引,我们把抽取出来的结点叫作索引层或者索引,down 表示指向原始链表节点的指针。

文章配图

例如查找 15,首先在索引层遍历,当遍历到值为 14 的结点时,发现下一个结点的值为 17,则目标值在这两个结点之间。通过 14 结点的 down 指针回到原始链表继续遍历,只需再遍历两个结点即可找到数据。整个过程比原始链表少遍历了节点。

如果给索引层再加一层索引,效率会更高。这种通过对链表加多级索引的结构,就是跳表。

文章配图

跳表的优化思路

按照上述生成链表的方式,每一层链表的节点个数是下面一层的节点个数的一半,查找过程类似二分查找,时间复杂度可降低到 O(log n)。但在插入或删除数据时,会打乱上下相邻两层链表上节点个数的严格对应关系。如果要维持这种关系,必须重新调整后续所有节点,导致时间复杂度蜕化成 O(n)。

文章配图

Skiplist 的设计为了避免这种问题,不再严格要求对应比例关系,而是插入一个节点的时候随机出一个层数。每次插入和删除都不需要考虑其他节点的层数。

文章配图

跳表如何保证效率

Skiplist 插入节点时随机出层数,一般跳表会设计一个最大层数 maxLevel 的限制,其次会设置多增加一层的概率 p。计算随机层数的逻辑如下:

文章配图

在 Redis 的 skiplist 实现中,这两个参数的取值为:

文章配图

产生越高的节点层数,概率越低。定量分析如下:

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

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

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

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

……

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

文章配图

现在很容易计算出:

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

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

跳表的时间复杂度

衡量一个算法的效率可以用时间复杂度。链表的查询时间复杂度为 O(n),跳表呢?

如果一个链表有 n 个结点,每两个结点抽取出一个结点建立索引,第一级索引节点数大约为 n/2,第二级为 n/4,第 m 级为 n/(2^m)。

假如一共有 m 级索引,第 m 级的节点数为两个,得出 n/(2^m)=2,从而求得 m=log(n)-1。加上原始链表,整个跳表的高度就是 log(n)。

查询跳表时,如果每一层都需要遍历 k 个结点,最终时间复杂度为 O(k*log(n))。

k 值为多少?按照每两个结点提取一个基点建立索引的情况,每一级最多需要遍历两个结点,所以 k=2。因为最高一级索引只有两个结点,下一层索引比上一层索引两结点之间增加了一个结点,类似二分查找。

文章配图

如上图所示,查询红色结点,查询路线每一级最多遍历两个节点即可。

所以跳表的查询任意数据的时间复杂度为 O(2*log(n)),常数 2 可以忽略,为 O(log(n))。

跳表的空间复杂度

跳表需要额外存储多级索引,所以需要更多的内存空间。

如果一个链表有 n 个结点,各级索引的结点总和就是 n/2+n/4+n/8…+8+4+2=n-2,所以跳表的空间复杂度为 O(n)。

我们可以通过每三个或五个结点抽取一个索引来减少索引所占的内存空间。

总之,跳表的空间复杂度为 O(n),跳表之所以比链表快,是因为跳表是以空间换时间。

跳表的查找

查找步骤:

从最高层开始,向右遍历。若当前节点的下一个节点值小于目标值,继续向右。如果当前节点的下一个节点值等于目标值,则返回。否则,向下降一层,重复上述步骤。直到在最底层找到目标或确认不存在。

跳表的插入

插入步骤:

查找插入位置并记录插入位置每一层前面的前驱节点:确定目标值在各层的插入位置并记录插入位置每一层前面的前驱节点。随机生成层数:通过抛硬币(随机算法)决定新节点的层数。插入节点:从底层到生成的最高层,逐层插入新节点并更新指针,若新插入节点的层数高于头节点的层数,则需要提高头结点的层数并更新对应指针。

跳表的删除

删除步骤:

查找目标节点并记录插入位置每一层前面的前驱节点:确定目标值在各层的位置并记录插入位置每一层前面的前驱节点。逐层删除:从要删除节点的最高层到底层,更新前驱节点的指针。释放内存:删除节点并回收空间。更新头结点的层数:若因为删除节点导致整体层数(除头结点外)下降,则需要降低头结点的层数。

跳表的模拟实现

struct SkiplistNode{ 
    int _val; 
    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]==nullptr || cur->_nextV[level]->_val > target) --level; 
            else return true; 
        } 
        return false; 
    } 
    void add(int num) { 
        vector<Node*> prevV=FindPrevNode(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(size_t i=0;i<n;i++){ 
            newNode->_nextV[i]=prevV[i]->_nextV[i]; 
            prevV[i]->_nextV[i]=newNode; 
        } 
    } 
    bool erase(int num) { 
        vector<Node*> prevV=FindPrevNode(num); 
        if(prevV[0]->_nextV[0]==nullptr || prevV[0]->_nextV[0]->_val!=num) return false; 
        else{ 
            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 x=_head->_nextV.size()-1; 
            while(x>=0){ 
                if(_head->_nextV[x]==nullptr) --x; 
                else break; 
            } 
            _head->_nextV.resize(x+1); 
            return true; 
        } 
    } 
    vector<Node*> FindPrevNode(int num){ 
        Node* cur=_head; 
        int level=_head->_nextV.size()-1; 
        vector<Node*> prevV(level+1,_head); 
        while(level>=0){ 
            if(cur->_nextV[level] && cur->_nextV[level]->_val < num) cur=cur->_nextV[level]; 
            else if(cur->_nextV[level]==nullptr || cur->_nextV[level]->_val >= num){ 
                prevV[level]=cur; 
                --level; 
            } 
        } 
        return prevV; 
    } 
    int RandomLevel(){ 
        size_t level=1; 
        while(rand()<=RAND_MAX*_p && level<_maxLevel){ 
            ++level; 
        } 
        return level; 
    } 
private: 
    Node* _head; 
    int _maxLevel=32; 
    double _p=0.25; 
};

跳表与平衡搜索树及哈希表的对比

  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. 哈希表在极端场景下哈希冲突高,效率下降厉害,需要红黑树补足接力。

目录

  1. 跳表的定义
  2. 跳表的演化过程
  3. 跳表的优化思路
  4. 跳表如何保证效率
  5. 跳表的时间复杂度
  6. 跳表的空间复杂度
  7. 跳表的查找
  8. 跳表的插入
  9. 跳表的删除
  10. 跳表的模拟实现
  11. 跳表与平衡搜索树及哈希表的对比
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 如何用 AI 自动生成 Python Celery 分布式任务代码
  • Java AI 学习路径:从基础到工程化落地
  • YOLO26-Pose 零样本姿态估计实战:机器人视觉新范式
  • AI 辅助 FPGA 开发:Vivado 配置与智能编程实践
  • Coze 工作流与 AI 视频自动化制作
  • 基于西门子TIA、PLCSIM Advanced与Kepware实现Fanuc机器人虚拟仿真调试
  • 8 家大厂 AI Agent 横评:OpenClaw AutoClaw KimiClaw QClaw 实测
  • 从零开始编写 LoRA 代码:原理与实战指南
  • TRAE Skills 全解析:从概念到实践
  • MySQL 实战:VARCHAR 类型安全转换为 INT
  • 开源 AI 短剧生成工具 Horseplay 介绍
  • 腾讯混元 Image-3.0 开源:800 亿参数多模态模型解析
  • AI 语音合成新趋势:大模型驱动的自然韵律生成
  • OpenClaw 接入 QVeris 实现 AI 助手实时数据查询
  • Flutter sse_stream 在鸿蒙端的适配与背压处理方案
  • VSCode Copilot 登录异常排查与修复指南
  • SWE-CI: 基于持续集成评估智能体代码维护能力
  • 基于 SpringBoot+Vue 的流浪动物管理系统设计与实现
  • 基于 skywalking-python 的 Python 应用分布式追踪实战
  • Java SE 基础:文件系统操作详解

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online