跳跃表( skiplist) 是一种有序的数据结构, 它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的.
跳跃表支持平均$O(log)$、最坏$O(N)$ 复杂度的节点查找. 大部分情况下,跳跃表的效率可以和平衡树想媲美,并且跳跃表的实现比平衡树更为简单.
<!-- more -->
Redis 使用跳跃表作为有序集合键的底层实现之一, 如果一个有序集合包含的元素数量较多,或者有序集合中元素的成员是比较长的字符串, Redis 会使用跳跃表来作为有序集合的底层实现.
和链表、字典等数据结构被广泛应用在 Redis 中不同, Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构, 除此之外,跳跃表在 Redis 中没有其他用途.
跳跃表的实现
Redis 的跳跃表是有 redis.h/zskiplistNode
和 redis.h/zskiplist
两个结构定义
typedef struct zskiplistNode {
robj *obj; //成员对象
double score; //分值
struct zskiplistNode *backward; //后退指针
//层
struct zskiplistLevel {
struct zskiplistNode *forward; //前进指针
unsigned int span; //跨度
} level[];
} zskiplistNode;
结构体各成员说明如下:
-
层
跳跃表的 level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些指针加快访问速度,一般来说,层的数量越多,访问其他节点的速度越快. 每次创建一个新跳跃表节点时,程序会根据幂次定律(越大的数出现的概率越小)随机生成一个介于1 和 32 之间的值作为 level 数组的大小,这个大小就是层的高度
-
前进指针
每个层都有一个指向表尾方向的指针.用于从表头向表尾方向访问节点.
-
跨度
层的跨度用于记录两个节点之间的距离. 两个节点之间的跨度越大,它们距离越远;指向 NULL 的节点的跨度为0.
-
后退指针
后退指针用于从表尾向表头访问节点,跟可以一次跳过多个节点的前进指针不同,每个节点只有一个后退指针.
-
分值和成员
节点的分支是一个 double 类型的浮点数,跳跃表中的所有节点都按分值从小到大排序.
节点的成员对象是一个指针,指向一个字符串对象,而字符串对象保存着一个 SDS 值.
各节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的: 分值相同的节点按照成员对象在字典序中的大小来排序,成员对象较小的节点会排在前面(靠近表头的方向).
下图显示一个简单的跳跃表布局:
图的左边为 zskiplist
结构,用来管理跳跃表节点.zskiplist
结构定义如下:
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //表头和表尾指针
unsigned long length; //节点的数量
int level; //层数最大的节点的层数
} zskiplist;
表头节点并没有算到 节点数量里面,表头节点和其他节点的构造是一样的:有前进指针、后退指针、分值和成员对象,不过这些属性都不会用到,所以图中省略这些部分,只显示表头节点的各层.
根据跳跃表的结构,程序可以在$O(1)$复杂度内返回表的长度,$O(1)$复杂度内定位表头节点和表尾节点.
我的博客: http://ygmyth.github.io