跳表 (Skip List)是一种随机化的数据结构,实质上是一种可以进行二分查找的有序链表。跳表通过增加多级索引,提高了查找、插入和删除操作的效率。
跳表支持平均O(logn)、最坏O(n)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。大部分情况下性能接近平衡树(如红黑树)。
一、跳表的选层机制
跳表的选层机制是通过随机函数来决定的。当需要插入或更新一个节点时,跳表会通过随机函数确定需要建立索引的层数。例如,要插入一个元素时,跳表会随机生成一个层数,然后从链表的底层开始,逐层向上建立索引。这种随机化机制确保了跳表的平衡性,使得各层的元素分布较为均匀。
具体规则如下:
-
随机函数决定层级:
- 插入新节点时,通过随机算法确定该节点的层级。
- 常见方法是模拟“抛硬币”实验:每次抛硬币,若为正面则层级加1,直到抛出反面为止。
- Redis 中的实现:
- 概率
p = 1/4
(即每次有25%的概率增加层级)。 - 最大层级
maxLevel = 32
(超过此值不再增加层级)。
- 概率
- 结果:
- 节点的层级越高,概率越低。例如,层级为1的概率为
3/4
,层级为2的概率为3/16
,层级为3的概率为3/64
,依此类推。 - 平均每个节点的层级约为
1/p = 4
(当p=1/4
时)。
- 节点的层级越高,概率越低。例如,层级为1的概率为
-
层级分配示例:
- 若新节点的层级为
k
,则它会在跳表的Level 1
到Level k
中各层插入一个指针。 - 如果
k
大于当前跳表的最大层级,则更新跳表的最大层级为k
。
- 若新节点的层级为
二、跳表的插入过程
插入操作的核心是确定新节点的层级,并更新各层级的指针。以下是具体步骤:
1. 确定新节点的层级
- 使用随机函数(如抛硬币)生成新节点的层级
k
。- 例如,在 Redis 中,通过
zslRandomLevel()
函数实现,初始层级为1,每次以p=1/4
的概率递增,直到达到最大层级或抛出反面。
- 例如,在 Redis 中,通过
2. 找到插入位置
-
从跳表的最高层开始,逐层向下查找插入位置:
- 在当前层级向右遍历,找到小于目标值的最大节点。
- 如果当前层级的下一个节点值大于目标值,或到达层级末尾,则下降到下一层。
- 重复上述步骤,直到到达最底层(Level 0),此时确定插入位置。
-
记录路径:
在查找过程中,维护一个update
数组,记录每一层级的前驱节点(用于后续更新指针)。
3. 逐层插入新节点
- 在每一层级(从
Level 1
到Level k
)中:- 将新节点的
next
指针指向update[i]
的下一个节点。 - 将
update[i]
的next
指针指向新节点。
- 将新节点的
- 如果新节点的层级
k
超过跳表当前最大层级,则更新跳表的最大层级为k
。
4. 更新相关属性(索引)
- 节点计数:跳表的总节点数加1。
- 跨度(span)(可选):某些实现中会维护节点间的跨度(即到下一节点的距离),插入后需更新受影响节点的跨度。
三、查找与删除
1. 查找(Search)
查找操作的目标是在跳表中找到一个特定值的位置。从最高层开始,向右移动直到找到一个大于目标值的节点,然后下降到下一层继续这个过程,直到到达最底层或者找到了目标值。
- 步骤:
- 从最高层的头节点开始。
- 向右移动,直到遇到一个比目标值大的节点或到达链表尾部。
- 下降到下一层,重复上述步骤。
- 如果在最底层找到了目标值,则返回该节点;否则,表示跳表中不存在此值。
2. 删除(Deletion)
删除操作包括找到待删除的节点并从所有涉及的层中移除它,同时调整相关节点的指针。
- 步骤:
- 首先执行查找操作定位到要删除的节点。
- 对于每个包含该节点的层,调整前驱节点的指针,使其跳过该节点。
- 释放被删除节点占用的资源。
- 如果删除的是某一层的唯一节点,可能需要减少跳表的层数。