跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
C++算法

跳表核心原理与 C++ 实现深度解析

综述由AI生成跳表是一种基于多层链表和随机化策略的高效数据结构,能将查找、插入和删除的时间复杂度降至 O(log n)。深入解析了跳表的核心原理、C++ 代码实现细节以及性能特征。内容涵盖节点设计、随机层数生成算法、查找与增删操作的指针维护逻辑,并结合 Redis 有序集合的实际应用案例进行分析。此外,还对比了跳表与 B 树、红黑树等结构的优劣,探讨了其在高并发环境下的表现及适用场景,为开发者在内存数据结构选型上提供参考。

GopherDev发布于 2026/3/23更新于 2026/5/57 浏览
跳表核心原理与 C++ 实现深度解析

引言

跳表(Skip List)是一种动态数据结构,由 William Pugh 于 1989 年提出。它本质上是对传统链表的优化,通过在链表上建立多层索引,将查找时间复杂度从 O(n) 降低到 O(log n)。这种设计在保持链表简单性的同时,实现了接近平衡树的查找效率,且无需维护复杂的旋转操作。

在传统链表中,由于只能顺序遍历,处理大规模数据时效率低下。跳表通过引入随机化策略构建多层索引结构,每一层都是下一层的'加速'版本。查找时从顶层开始逐层向下,快速缩小搜索范围。这种结构特别适合需要频繁插入、删除及查找的动态场景,如数据库索引和缓存系统。

核心思想

跳表的核心在于多层链表与概率性分布。最底层是包含所有数据的完整有序链表,上层链表则是下层节点的子集。每个节点被提升到上一层的概率通常是固定的(例如 50%),这使得高层节点数量随层级指数级减少。

文章配图

这种设计带来了几个显著特点:

  1. 查找效率高:平均时间复杂度为 O(log n),接近平衡树。
  2. 动态性强:插入和删除操作只需调整指针,无需像红黑树那样进行复杂的平衡旋转。
  3. 空间消耗灵活:空间复杂度为 O(n),可通过调整提升概率控制内存占用。
  4. 并发友好:基于局部指针修改,较容易实现无锁或细粒度锁的并发控制。

结构设计

节点定义

每个节点除了存储数据值外,还需要一个指针数组来指向不同层级的下一个节点。节点的层数是随机生成的,决定了它在哪些层级存在。

struct SkipListNode {
    int _val;
    std::vector<SkipListNode *> _nextV;

    SkipListNode(int val, int level) : _val(val), _nextV(level, nullptr) {}
};

跳表类封装

跳表类负责管理头节点、最大层数以及随机层数的生成逻辑。头节点作为哨兵,不存储有效数据,仅用于简化边界处理。

class SkipList {
private:
    typedef SkipListNode Node;
    size_t _maxLevel = 32;
    double _p = 0.25; // 提升概率
    Node *_head;

    // 辅助函数:记录每层的前驱节点
    std::vector<Node *> FindPrevNode(int value);
    
    ;

:
    () { (()); _head =  (, ); }
    ~() {  }

    ;
    ;
    ;
};
// 生成随机层数
int RandomLevel()
public
SkipList
srand
time
0
new
Node
-1
1
SkipList
/* 释放资源 */
bool Find(int value)
void Insert(int value)
bool Erase(int value)

核心操作实现

随机层数生成

这是跳表性能的关键。通常采用几何分布,每次抛硬币决定是否需要进入下一层。这里提供两种常见的实现方式,任选其一即可。

// 方法一:使用 rand()
int RandomLevel() {
    size_t level = 1;
    while (rand() < RAND_MAX * _p && level < _maxLevel) {
        ++level;
    }
    return level;
}

// 方法二:使用 C++11 随机数库
#include <random>
int RandomLevel() {
    static std::default_random_engine generator(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;
}

查找操作

查找过程遵循'先右后下'的原则。从最高层开始,如果当前节点的值小于目标值,则向右移动;否则向下移动一层。当到达底层时,若找到目标则返回 true。

bool Find(int value) {
    Node *cur = _head;
    int level = _head->_nextV.size() - 1;
    while (level >= 0) {
        if (cur->_nextV[level] && cur->_nextV[level]->_val < value) {
            cur = cur->_nextV[level]; // 向右走
        } else if (cur->_nextV[level] == nullptr || cur->_nextV[level]->_val > value) {
            --level; // 向下走
        } else {
            return true; // 找到
        }
    }
    return false;
}

插入操作

插入前需先定位位置并记录每层的前驱节点。新节点的层数由随机算法决定。如果新节点层数超过当前跳表最大层数,需扩展头节点的指针数组。

void Insert(int value) {
    std::vector<Node *> prevV = FindPrevNode(value);
    int n = RandomLevel();
    Node *newnode = new Node(value, 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;
    }
}

删除操作

删除逻辑与插入类似,利用 FindPrevNode 获取前驱节点,然后逐层跳过待删除节点。删除后若最高层变为空,应收缩头节点层数以节省空间。

bool Erase(int value) {
    std::vector<Node *> prevV = FindPrevNode(value);
    if (prevV[0]->_nextV[0] == nullptr || prevV[0]->_nextV[0]->_val != value) {
        return false;
    }

    Node *del = prevV[0]->_nextV[0];
    size_t level = del->_nextV.size();

    // 移除各层指针
    for (size_t i = 0; i < level; ++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;
}

性能分析

时间与空间复杂度

  • 时间复杂度:查找、插入、删除的平均时间复杂度均为 O(log n)。虽然最坏情况下可能退化为 O(n),但概率极低。
  • 空间复杂度:为 O(n)。每个节点平均占用 log n 个指针空间。当提升概率 p=0.5 时,总空间约为 2n。

缓存友好性

跳表的线性链表结构比树结构更利于 CPU 缓存预取。单层内的节点访问是连续的,减少了缓存缺失率。不过,跨层跳转仍会带来一定的随机访问开销。

高并发支持

在高并发场景下,跳表表现优异。可以通过无锁机制(CAS 操作)或分层锁来实现线程安全。读多写少场景下,读写锁分离能进一步提升性能。

实际应用:Redis 中的跳表

Redis 的有序集合(Sorted Set)底层就是由哈希表和跳表共同实现的。哈希表用于 O(1) 的成员查找,而跳表用于维护元素的排序和范围查询。

  • 排行榜系统:利用跳表的有序性,快速获取分数排名靠前的元素。
  • 范围查询:跳表支持高效的区间遍历,时间复杂度为 O(log n + m)。
  • 优先级队列:结合 LRU 等淘汰策略,跳表能快速定位并移除优先级最低的元素。

局限性与替代方案

尽管跳表优势明显,但在某些场景下也有局限性:

  1. 随机性风险:极端情况下可能退化,不如平衡树稳定。
  2. 空间开销:相比单链表,指针数组占用更多内存。
  3. 磁盘友好性差:跳表主要适用于内存数据结构,不适合直接用于磁盘索引。

针对这些情况,可以考虑以下替代方案:

  • B 树 / B+ 树:适合文件系统和大容量数据库索引,磁盘 I/O 友好。
  • 红黑树:C++ STL 中 std::map 的实现,平衡性好,内存中查找稳定。
  • AVL 树:严格平衡,查找极快,但插入删除时的旋转开销较大。

选择哪种结构取决于具体需求:如果是内存中的高频动态更新,跳表是很好的选择;如果是持久化存储或严格平衡要求,树结构可能更合适。

总结

跳表通过巧妙的多层索引和随机化策略,在实现复杂度与性能之间取得了极佳平衡。它避免了平衡树复杂的旋转逻辑,同时提供了对数级的操作效率。理解跳表的原理不仅有助于掌握这一数据结构,也能帮助我们在 Redis、数据库索引等实际系统中做出更合理的技术选型。

目录

  1. 引言
  2. 核心思想
  3. 结构设计
  4. 节点定义
  5. 跳表类封装
  6. 核心操作实现
  7. 随机层数生成
  8. 查找操作
  9. 插入操作
  10. 删除操作
  11. 性能分析
  12. 时间与空间复杂度
  13. 缓存友好性
  14. 高并发支持
  15. 实际应用:Redis 中的跳表
  16. 局限性与替代方案
  17. 总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 面向新手的鸿蒙跨平台开发技术选型指南
  • AI 绘画电商产品提示词撰写指南
  • 二分查找实战:山峰数组峰顶索引与寻找峰值
  • 基于 DeepSeek 的企业级知识管理系统设计与实现
  • 二分查找实战:山峰数组峰顶索引与寻找峰值
  • Qwen3-TTS-VoiceDesign 实战:AR 导览眼镜空间音频定位语音生成
  • Linux Ext2 文件系统深度解析
  • MySQL 分库分表实战:垂直分库与水平分表策略及分片键选择
  • 利用 DeepSeek 提示词与工具降低论文 AIGC 检测率实战
  • 基于 VeRL 框架的 GSPO 算法在 Atlas 800T A2 服务器上实践
  • Llama-2-7B 昇腾 NPU 性能测评与部署实践
  • FLOAT:基于流匹配的音频驱动说话者头像生成模型
  • AI工具链:Gradio演示界面
  • AI 提示词:零基础入门与核心概念
  • 二分查找实战:旋转数组最小值与缺失数字求解
  • 路径类动态规划入门:3 道经典例题详解
  • 纯 QWidget 实现电子地图控件:多线程瓦片加载与图形覆盖
  • 无人机低空智能巡飞巡检平台架构与应用
  • QClaw 接入微信:AI 从聊天工具向执行助手的进化
  • 使用 Rokid 灵珠平台搭建旅游 AR 智能体指南

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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