跳表SkipList:从原理到高性能实现,彻底搞懂Redis同款数据结构

跳表SkipList:从原理到高性能实现,彻底搞懂Redis同款数据结构

引言

在数据结构的浩瀚星空中,平衡二叉搜索树(如AVL树、红黑树)长期主导着有序数据的快速增删改查场景。然而,它们复杂的旋转操作和多指针调整常常让开发者望而却步。有没有一种结构,既能保持对数级别的查找效率,又具备插入、删除逻辑的简洁性?跳表(SkipList)就是答案。

跳表由William Pugh在1990年提出,是一种基于随机化的层次化有序链表。它在工程中极为成功:Redis有序集合(Sorted Set)的底层实现之一就是跳表,LevelDB的MemTable也采用跳表加速查找。本文将从底层原理出发,手把手带你实现一个完整、可运行的跳表,并深入探讨其优化细节与常见陷阱。

一、核心概念:从链表到多层索引的蜕变

1.1 有序链表的瓶颈

假设我们有一个有序单链表,存储了 [3, 6, 9, 15, 21, 34]。如果要查找元素 21,最坏情况下需要遍历6个节点,时间复杂度 O(n)。链表虽然插入、删除只需修改指针,但查找必须线性扫描,效率低下。

跳表的精髓在于空间换时间——为链表增加多层“索引”,使查找能够跨越多个节点。

跳表示意图
(图:多层索引构建的跳表结构)

如上图,原始链表在第0层(L0)包含所有元素。第1层(L1)提取部分节点作为索引,第2层(L2)再从L1提取,形成金字塔状结构。查询时从最高层开始,逐层下降,每次可以跳过大批节点。

  • 查找 21 的过程:
  • L2层:从3开始,下一个节点是9(小于21,前进),9后面是34(大于21,下降)。
  • L1层:在9处,下一个是21(等于,完成),或者继续按规则判断。
  • 总比较次数远小于 n,平均为 O(log n)

1.2 随机化确定层数

跳表没有严格的平衡条件,其平衡性由概率保证。每个节点插入时,通过抛硬币(随机数生成)决定它要出现在多少层中。常见的实现是:节点有50%概率出现在第1层(L1),25%出现在第2层(L2),以此类推,形成一个几何分布。这保证了高层节点稀疏、低层节点稠密,近似平衡树的形态。

1.3 时间复杂度与空间复杂度

  • 查找/插入/删除:平均 O(log n),最坏 O(n)(极低概率)。
  • 空间复杂度:期望 O(n)。平均每个节点有2个指针(因为层数期望为2),所以额外空间开销可控。

相较于红黑树,跳表实现更简单,查找范围操作也更直观(直接走底层链表遍历区间)。这些优点使其在工业界备受青睐。

二、实战示例:手写一个完整的跳表

下面我们用 Python 实现一个支持泛型键值的跳表,包含 searchinsertdelete 三个核心操作。代码结构清晰,注释详尽,可直接运行测试。

import random
from typing import Any, Optional, List

class SkipNode:
    __slots__ = ('key', 'value', 'forward')
    def __init__(self, key: Any, value: Any, level: int):
        self.key = key          # 键,用于比较大小
        self.value = value      # 存储的值
        # forward 数组:forward[i] 表示该节点在第 i 层的下一个节点指针
        self.forward: List[Optional[SkipNode]] = [None] * (level + 1)

class SkipList:
    _MAX_LEVEL = 16            # 最大层数,可根据数据量调整(2^16 ≈ 65535)
    _PROB = 0.5                # 提升概率

    def __init__(self):
        # 头节点:不存储实际数据,其 forward 数组作为各层的起始指针
        self.head = SkipNode(None, None, self._MAX_LEVEL)
        self.level = 0         # 当前跳表实际最高层数
        self._size = 0

    def _random_level(self) -> int:
        """随机生成节点层数:从0开始,每次以概率 PROB 上升一层"""
        lvl = 0
        while random.random() < self._PROB and lvl < self._MAX_LEVEL:
            lvl += 1
        return lvl

    def search(self, key: Any) -> Optional[Any]:
        """查找 key 对应的 value,若不存在返回 None"""
        cur = self.head
        # 从最高层向下搜索
        for i in range(self.level, -1, -1):
            while cur.forward[i] and cur.forward[i].key < key:
                cur = cur.forward[i]   # 在同一层向右移动
        # 此时 cur 位于第0层、目标位置的前一个节点
        cur = cur.forward[0]
        if cur and cur.key == key:
            return cur.value
        return None

    def insert(self, key: Any, value: Any) -> bool:
        """插入键值对,若 key 已存在则更新 value 并返回 False"""
        update = [None] * (self._MAX_LEVEL + 1)  # 记录每层需要更新的前驱节点
        cur = self.head

        # 1. 从高层往低层找到每层的前驱位置,存入 update 数组
        for i in range(self.level, -1, -1):
            while cur.forward[i] and cur.forward[i].key < key:
                cur = cur.forward[i]
            update[i] = cur

        # 2. 检查底层下一个节点是否已经存在 key
        cur = cur.forward[0]
        if cur and cur.key == key:
            cur.value = value       # 更新值
            return False

        # 3. 生成随机层数
        new_level = self._random_level()
        if new_level > self.level:
            # 新节点层数超过当前最高层,将多余的层用头节点填充
            for i in range(self.level + 1, new_level + 1):
                update[i] = self.head
            self.level = new_level

        # 4. 创建新节点,并像插入链表一样修改各层指针
        new_node = SkipNode(key, value, new_level)
        for i in range(new_level + 1):
            new_node.forward[i] = update[i].forward[i]
            update[i].forward[i] = new_node

        self._size += 1
        return True

    def delete(self, key: Any) -> bool:
        """删除指定 key,成功返回 True,不存在返回 False"""
        update = [None] * (self._MAX_LEVEL + 1)
        cur = self.head

        for i in range(self.level, -1, -1):
            while cur.forward[i] and cur.forward[i].key < key:
                cur = cur.forward[i]
            update[i] = cur

        cur = cur.forward[0]
        if not cur or cur.key != key:
            return False

        # 从底层向上逐层修改指针,移除节点
        for i in range(self.level + 1):
            if update[i].forward[i] != cur:
                break      # 若本层已经没有指向目标节点,更高层也肯定没有
            update[i].forward[i] = cur.forward[i]

        # 更新当前最高层数(可能会有空层下降)
        while self.level > 0 and self.head.forward[self.level] is None:
            self.level -= 1

        self._size -= 1
        return True

    def __len__(self) -> int:
        return self._size

    def display(self) -> None:
        """打印跳表各层结构,便于调试"""
        print(f"SkipList (level={self.level}, size={self._size})")
        for i in range(self.level, -1, -1):
            cur = self.head.forward[i]
            line = f"L{i}: H"
            while cur:
                line += f" -> ({cur.key}:{cur.value})"
                cur = cur.forward[i]
            print(line if " ->" in line else line + " (empty)")

测试代码

if __name__ == "__main__":
    sl = SkipList()
    data = [5, 3, 9, 1, 7, 12, 6, 8]
    for k in data:
        sl.insert(k, k * 10)

    sl.display()
    print("\n查找 key=7:", sl.search(7))  # 70
    print("删除 key=3:", sl.delete(3))
    sl.display()
    print("删除 key=100:", sl.delete(100))

运行后可以看到跳表的多层结构,直观感受索引的跃迁。

三、常见问题与注意事项

3.1 随机层数生成策略

_PROB 通常设为 0.5,每个节点出现在第 k 层的概率为 (1-PROB) * PROB^k,使得期望层数为 1/(1-PROB) = 2。也可以使用 PROB=0.25 来降低空间消耗,但会略微增长查询时间。切勿限制层数过低(例如 _MAX_LEVEL=3),否则退化为低效链表。一般 MAX_LEVEL 设为 log(1/PROB)(期望最大元素数),对于百万级数据,16 层足够。

3.2 并发访问与线程安全

本文实现的跳表是非线程安全的。实际生产环境(如Java的ConcurrentSkipListMap)常采用无锁设计,使用CAS(Compare And Swap)原子更新 forward 指针,并配合标记节点处理删除,实现高并发。如果需在多线程环境使用,简单加读写锁会极大损失性能,建议优先考虑成熟的并发库。

3.3 内存管理与对象开销

跳表的每个节点包含一个指针数组,对于小型对象,指针数组的额外空间可能显得突出。在内存敏感场景,可考虑:
- 使用灵活的层数分配(实际分配 random_level+1 个指针,而非 MAX_LEVEL),例如此例中 forward 为动态列表。
- 采用 非分层节点(所有节点在同一层链表,但通过标记/跨度优化范围查询),Redis 跳表实现即包含跨度字段,支持 ZRANK 等操作高效计算排名。

3.4 与红黑树的对比与选择

  • 实现难度:跳表插入/删除只需修改相邻指针,无旋转,逻辑更直观。
  • 范围查询:跳表找到起点后,直接在L0层向前遍历,非常高效;红黑树需要中序遍历,可能涉及栈或父指针。
  • 内存占用:红黑树每个节点固定3个指针(左右孩子+颜色/父),跳表节点平均约2个指针,但可变。
  • 性能稳定:红黑树是严格平衡的,最坏情况也是 O(log n);跳表最坏可能退化,但概率极低。

Redis选择了跳表,部分原因正是因为其范围操作高效、实现简单且容易在节点中扩展额外字段(如span跨度)。

四、总结

跳表以一种优雅的概率化方式,解决了有序链表快速查询的痛点。它用多层随机索引换来了对数平均查找时间,同时保留了链表插入删除的灵活性。本文从原理出发,提供了完整可运行的Python实现,并探讨了并发、内存等工程实践中的注意事项。

掌握跳表不仅能帮助你理解Redis、LevelDB等系统中高性能索引的实现,还能为你设计自己的数据结构提供灵感:当确定性平衡难以维护时,引入恰到好处的随机性,往往能化繁为简。 欢迎你基于此代码继续扩展,实现范围查询、迭代器等功能,动手深入体会跳表的魅力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

至乐活着

失业续命中,这篇有用就赏杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值