当前位置:首页 > 随笔 > 正文内容

跳表数据结构深度解析:Redis有序集合的底层设计与工程实现

访客 随笔 2026年6月26日 1

跳表核心机制

跳表(Skip List)是一种概率性平衡的数据结构,通过在有序链表上建立多级索引,将线性查找的时间复杂度从 O(N) 优化至 O(log N)。其核心创新在于用随机化策略替代传统平衡树的复杂旋转操作,以空间换时间的方式实现高效的范围查询与顺序遍历。

分层索引的查询原理

跳表的查询过程采用"自上而下、自左而右"的搜索策略。以查找目标值 23 为例:

  1. 从顶层索引出发,比较当前节点与后继节点的键值
  2. 若后继节点值小于目标,则向右移动(跳过不可能包含结果的区间)
  3. 若后继节点值大于目标,则下降一层(缩小搜索范围至当前区间)
  4. 重复上述过程直至底层,确定目标是否存在

这种策略确保每次比较都能排除大量无效数据,形成类似二分查找的剪枝效果。

随机层级的生成策略

为避免插入删除导致结构退化,跳表采用概率控制节点高度。设晋升概率为 p,节点高度为 k 的概率为 p^(k-1)·(1-p)。Redis 采用 p=0.25、最大层数 64(新版为 32)的配置:

int generateLevel() {
    int lvl = 1;
    // 使用位运算优化随机数生成
    while ((random() & 0xFFFF) < (0xFFFF * _prob)) {
        ++lvl;
        if (lvl >= _maxHeight) break;
    }
    return lvl;
}

当 p=0.25 时,平均每个节点携带 1.33 个指针;p=0.5 时则为 2 个指针。较低的概率系数显著降低内存开销,同时保持对数级查询效率。

完整工程实现

template<typename K, typename V>
struct IndexNode {
    K key;
    V value;
    std::vector<IndexNode*> forward;
    IndexNode(K k, V v, int height) 
        : key(k), value(v), forward(height, nullptr) {}
};

template<typename K, typename V>
class SkipListMap {
    using Node = IndexNode<K, V>;
    Node* sentinel;
    double _prob = 0.25;
    int _maxHeight = 32;
    int _curHeight = 1;
    
    int randomHeight() {
        int h = 1;
        while (h < _maxHeight && (double)rand() / RAND_MAX < _prob) 
            ++h;
        return h;
    }
    
    std::vector<Node*> findPredecessors(const K& target) {
        std::vector<Node*> update(_curHeight);
        Node* curr = sentinel;
        for (int lvl = _curHeight - 1; lvl >= 0; --lvl) {
            while (curr->forward[lvl] && curr->forward[lvl]->key < target)
                curr = curr->forward[lvl];
            update[lvl] = curr;
        }
        return update;
    }
    
public:
    SkipListMap() {
        srand(time(nullptr));
        sentinel = new Node(K(), V(), 1);
    }
    
    bool contains(const K& key) {
        Node* curr = sentinel;
        for (int lvl = _curHeight - 1; lvl >= 0; --lvl) {
            while (curr->forward[lvl] && curr->forward[lvl]->key < key)
                curr = curr->forward[lvl];
        }
        return curr->forward[0] && curr->forward[0]->key == key;
    }
    
    void insert(const K& key, const V& val) {
        auto prev = findPredecessors(key);
        if (prev[0]->forward[0] && prev[0]->forward[0]->key == key) {
            prev[0]->forward[0]->value = val;
            return;
        }
        
        int newHeight = randomHeight();
        if (newHeight > _curHeight) {
            prev.resize(newHeight, sentinel);
            sentinel->forward.resize(newHeight, nullptr);
            _curHeight = newHeight;
        }
        
        Node* newbie = new Node(key, val, newHeight);
        for (int i = 0; i < newHeight; ++i) {
            newbie->forward[i] = prev[i]->forward[i];
            prev[i]->forward[i] = newbie;
        }
    }
    
    bool remove(const K& key) {
        auto prev = findPredecessors(key);
        Node* victim = prev[0]->forward[0];
        if (!victim || victim->key != key) return false;
        
        int victimHeight = victim->forward.size();
        for (int i = 0; i < victimHeight; ++i)
            prev[i]->forward[i] = victim->forward[i];
        
        delete victim;
        while (_curHeight > 1 && !sentinel->forward[_curHeight - 1])
            --_curHeight;
        return true;
    }
    
    // 范围查询:获取 [low, high] 区间内所有元素
    std::vector<std::pair<K,V>> rangeQuery(const K& low, const K& high) {
        std::vector<std::pair<K,V>> result;
        Node* curr = sentinel->forward[0];
        while (curr && curr->key < low) curr = curr->forward[0];
        while (curr && curr->key <= high) {
            result.emplace_back(curr->key, curr->value);
            curr = curr->forward[0];
        }
        return result;
    }
};

Redis 选型决策分析

维度跳表红黑树哈希表
实现复杂度低,无旋转操作高,需维护平衡条件中等,需处理冲突
内存开销1.33×N 指针(p=0.25)3×N 指针+颜色位1.3×N~2×N 空间
范围查询O(log N + M) 原生支持O(log N + M) 需中序遍历O(N) 需全表扫描
顺序遍历直接遍历最底层非直观,需栈辅助不支持
并发控制锁粒度细,易于乐观锁旋转操作需全局锁分段锁或全局锁

Redis 选择跳表作为 ZSet 底层结构,核心考量在于:范围查询是 sorted set 的核心操作,跳表在此场景下代码简洁且性能优异;同时其实现比红黑树减少约 50% 的指针存储,在内存敏感的数据库场景中具有显著优势。

标签: SkipList

相关文章

可以按小时收费的VPS

很多 VPS 提供商都支持 按小时计费(hourly billing),想短期试用 / 临时搭建节点、测试网络、短期项目等场景非常合适。下面是当前最主流且靠谱的按小时 VPS 选项,分别按不同需求场景整理: 1. Vultr(全球节点,包括日本) 按小时计费 可选机房:东京 / 大阪 / 洛杉矶 / 法兰克福 / 伦敦 … 支持 PayPal(部分情况),但更常用信用卡/PayPal+卡价格参考$...

在 iPhone 上下载国外App

地区/国家限制App Store 会根据 Apple ID 的国家或地区限制应用下载。如果你的 Apple ID 绑定的是中国大陆,就可能无法下载 OpenAI 官方的 ChatGPT 应用,因为它在大陆 App Store 不上架。解决办法:换成美国、加拿大、香港等地区的 Apple ID。或者在现有 Apple ID 上更改地区。注册一个国外 Apple ID(推荐)比如注册 美国区 Appl...

Selenium自动化测试入门指南

Selenium自动化测试入门指南

什么是自动化测试? 自动化测试是指利用软件工具自动执行测试用例,模拟用户操作,如打开网页、点击链接、输入文本等,并验证结果是否符合预期。 其主要优点包括: 大幅减少人工成本 测试速度快 可以在非工作时间运行 支持持续集成和交付 然而,它也存在一些局限性,例如开发成本较高、不适合快速变化的项目、依赖稳定的UI界面等。 自动化测试的应用条件 适合引入自动化测试的情况包括: 手动测试耗时且需要大量...

MariaDB Galera集群故障快速恢复指南

OpenStack控制节点采用三节点MariaDB Galera集群架构。当数据库集群因故障重启时,有时会出现Galera集群无法正常启动的问题。虽然有多种方法可以恢复数据库服务,但如何实现快速启动同时确保数据完整性呢? 通过分析日志发现,MariaDB Galera集群节点宕机时会在日志中输出以下信息: [Note] WSREP: 新集群视图:全局状态: 874d8e7e-5980-11e8-8...

Android 中 EventBus 的通信机制与实现原理深度解析

EventBus 核心设计思想 EventBus 是一个基于观察者模式的事件总线框架,广泛应用于 Android 平台以实现组件解耦。它通过中心化的消息分发机制,使不同层级、不同线程的对象能够以"发布-订阅"方式通信,避免了传统接口回调或广播带来的强依赖问题。 核心角色说明 事件(Event):任意 Java 对象,作为数据载体,如网络状态变更通知、用户登录信息等。 发布者(Publi...

二叉树基础操作实现(C语言)

二叉树基础操作实现(C语言)

二叉树遍历方法 以下为二叉树结构示例 前序访问实现: void traversePreOrder(TreeNode* node) { if (node == NULL) { printf("N "); return; } printf("%d ", node->value); trave...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。