【每日算法】Day 16-1:跳表(Skip List)——Redis有序集合的核心实现原理(C++手写实现)

解锁O(log n)高效查询的链表奇迹!今日深入解析跳表的数据结构设计与实现细节,从基础概念到Redis级优化策略,彻底掌握这一平衡树的优雅替代方案。


一、跳表核心思想

跳表(Skip List) 是一种基于多层有序链表的概率型数据结构,核心特性:

  1. 多层结构:包含L0(完整数据层)到Lh(顶层索引层)

  2. 快速搜索:利用高层索引实现二分查找式跳跃

  3. 动态平衡:通过随机层数维持高效查询性能

与平衡树的对比优势:

特性 跳表 红黑树
实现复杂度 简单(无需旋转操作) 复杂(需维护平衡)
范围查询效率 O(log n) + O(m) O(log n + m)
并发性能 更易实现锁细粒度控制 全局重平衡影响并发
内存占用 额外指针空间(约2倍) 平衡信息存储

二、跳表节点定义

cpp 复制代码
struct SkipListNode {
    int val;
    vector<SkipListNode*> next; // 多层后继指针
    SkipListNode(int v, int level) : val(v), next(level, nullptr) {}
};

三、跳表完整实现(C++)

1. 基础结构
cpp 复制代码
class SkipList {
private:
    const float P = 0.25;     // 节点晋升概率
    int maxLevel = 16;        // 最大层数限制
    int curLevel = 0;         // 当前最高层
    SkipListNode* head;       // 头节点(哑节点)

    // 随机生成节点层数
    int randomLevel() {
        int level = 1;
        while ((rand() % 100) < P*100 && level < maxLevel)
            level++;
        return level;
    }

public:
    SkipList() {
        head = new SkipListNode(INT_MIN, maxLevel);
    }
    
    ~SkipList() {
        // 层序遍历销毁所有节点(代码略)
    }
};
2. 搜索操作
cpp 复制代码
bool search(int target) {
    SkipListNode* curr = head;
    for (int i = curLevel-1; i >= 0; --i) {
        while (curr->next[i] && curr->next[i]->val < target) {
            curr = curr->next[i];
        }
    }
    curr = curr->next[0];
    return curr && curr->val == target;
}
3. 插入操作
cpp 复制代码
void add(int num) {
    vector<SkipListNode*> update(maxLevel, head);
    SkipListNode* curr = head;
    
    // 记录每层需要更新的节点
    for (int i = curLevel-1; i >= 0; --i) {
        while (curr->next[i] && curr->next[i]->val < num) {
            curr = curr->next[i];
        }
        update[i] = curr;
    }
    
    // 创建新节点
    int newLevel = randomLevel();
    if (newLevel > curLevel) {
        for (int i = curLevel; i < newLevel; ++i)
            update[i] = head;
        curLevel = newLevel;
    }
    
    SkipListNode* newNode = new SkipListNode(num, newLevel);
    for (int i = 0; i < newLevel; ++i) {
        newNode->next[i] = update[i]->next[i];
        update[i]->next[i] = newNode;
    }
}
4. 删除操作
cpp 复制代码
bool erase(int num) {
    vector<SkipListNode*> update(maxLevel, nullptr);
    SkipListNode* curr = head;
    
    // 定位待删除节点
    for (int i = curLevel-1; i >= 0; --i) {
        while (curr->next[i] && curr->next[i]->val < num) {
            curr = curr->next[i];
        }
        update[i] = curr;
    }
    
    curr = curr->next[0];
    if (!curr || curr->val != num) return false;
    
    // 更新各层指针
    for (int i = 0; i < curLevel; ++i) {
        if (update[i]->next[i] != curr) break;
        update[i]->next[i] = curr->next[i];
    }
    
    // 更新当前最高层
    while (curLevel > 1 && head->next[curLevel-1] == nullptr)
        curLevel--;
    
    delete curr;
    return true;
}

四、Redis的跳表优化策略

1. 特殊设计要点
  • 晋升概率P=1/4:平衡空间与时间效率

  • 最大层数=32:足够支持2^64元素的理论需求

  • ZSKIPLIST_MAXLEVEL:动态调整最高层数

  • 双向指针:支持反向遍历(Redis 5.0+)

2. 存储结构图示
cpp 复制代码
Redis跳表节点结构:
+------------+-----------+-------+-------+-----+-------+
| 成员对象   | 分值(score) | backward | level[] | ... |
+------------+-----------+-------+-------+-----+-------+

五、大厂真题实战

真题1:设计排行榜系统(某大厂2024面试)

需求:

实时维护玩家分数排名,支持:

  1. 更新玩家分数

  2. 查询Top N玩家

  3. 查询玩家排名

跳表解法:

cpp 复制代码
class Leaderboard {
private:
    struct Node {
        int playerId;
        int score;
        // 重载比较运算符
        bool operator<(const Node& other) const {
            return score > other.score; // 按分数降序
        }
    };
    SkipList<Node> skipList;
    unordered_map<int, SkipListNode<Node>*> cache;

public:
    void addScore(int playerId, int score) {
        if (cache.count(playerId)) {
            auto node = cache[playerId];
            int oldScore = node->val.score;
            skipList.erase({playerId, oldScore});
            score += oldScore;
        }
        auto newNode = skipList.add({playerId, score});
        cache[playerId] = newNode;
    }
    
    vector<int> top(int K) {
        vector<int> res;
        auto curr = skipList.head->next[0];
        while (K-- && curr) {
            res.push_back(curr->val.playerId);
            curr = curr->next[0];
        }
        return res;
    }
};
真题2:时间序列数据库索引(某大厂2023笔试)

需求:

高效查询时间范围内的数据点
跳表变种设计:

  • 将时间戳作为排序键

  • 在高层索引中存储时间区间统计量(如最大值/最小值)

  • 范围查询时利用高层索引快速定位起始点


六、复杂度与优化对比

操作 时间复杂度 空间复杂度 优化方向
插入 平均O(log n) O(n) 调整晋升概率P
删除 O(log n) O(n) 延迟删除优化
查询 O(log n) O(n) 增加高层索引密度
范围查询 O(log n + m) O(n) 双向指针优化

七、常见误区与调试技巧

  1. 层数分配不均:随机数生成器质量影响性能(建议使用MT19937)

  2. 指针未初始化:新节点next数组需全部置空

  3. 内存泄漏:需分层遍历释放所有节点

  4. 调试技巧

    • 可视化打印跳表结构

    • 为节点添加唯一ID辅助调试

    • 边界测试(空表/单节点/连续插入相同值)


进阶学习资源:

  1. Redis源码src/t_zset.c中的zskiplist实现

  2. 《算法导论》跳表复杂度证明

  3. Paper: "Skip Lists: A Probabilistic Alternative to Balanced Trees"

LeetCode真题训练:

相关推荐
漫随流水40 分钟前
leetcode算法(111.二叉树的最小深度)
数据结构·算法·leetcode·二叉树
fpcc42 分钟前
跟我学C++中级篇——Linux中文件和链接及重定向
linux·c++
Fcy6482 小时前
C++ set&&map的模拟实现
开发语言·c++·stl
2501_941805938 小时前
在大阪智能零售场景中构建支付实时处理与高并发顾客行为分析平台的工程设计实践经验分享
数据库
李慕婉学姐8 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
珠海西格电力8 小时前
零碳园区有哪些政策支持?
大数据·数据库·人工智能·物联网·能源
じ☆冷颜〃8 小时前
黎曼几何驱动的算法与系统设计:理论、实践与跨领域应用
笔记·python·深度学习·网络协议·算法·机器学习
数据大魔方8 小时前
【期货量化实战】日内动量策略:顺势而为的短线交易法(Python源码)
开发语言·数据库·python·mysql·算法·github·程序员创富
POLITE38 小时前
Leetcode 23. 合并 K 个升序链表 (Day 12)
算法·leetcode·链表
fpcc9 小时前
C++编程实践——链式调用的实践
c++