跳表
跳表
skiplist
是一种有序的数据结构, 通过在每个节点中维持多个指向其它节点的指针、达到快速访问节点的目的支持平均 O(logN), 最坏 O(N)复杂度的查找, 还支持顺序性操作来批量处理节点, 大部分情况下、效率可以合平衡树相媲美, 且实现简单, 很多程序都可以使用skipList替代平衡树, Redis使用跳跃表作为有序集合键的底层实现之一.
跳表只在两个地方使用: 实现有序集合键 & 在集群节点中用作内部结构
跳表的实现
Redis 的跳跃表由 redis.h/zskiplistNode
和 redis.h/zskiplist
两个结构定义, zskiplistNode
结构用于表示跳跃表节点, zskiplist
则用于保存跳跃表节点的相关信息, eg.
节点梳理、及指向表头节点 和 表尾节点的指针.
最左边是跳表的zskiplist
结构, 包含下边这些属性:
header
: 指向跳表的表头节点
tail
: 指向跳表的表尾节点
level
: 记录当前跳表内、层次最大的那个节点的层数(表头节点的层数不计算在内)
length
: 记录跳表的长度, 即: 目前跳表内包含的节点的数量(表头节点不计)
右边是4个zskiplistNode
结构, 属性如下:
level
: 节点中用 L1
, L2
, L3
等标记节点的各个层, L1
代表第一层, 依次类推. 每个层有两个属性: 前进指针
和 跨度
. 前进指针用于访问位于表尾方向的其它节点, 跨度记录前进指针指向的节点和当前节点的距离. 程序从表头向表尾遍历时, 访问会沿着层的前进指针进行.
backward
: 后退指针, 节点中BW
标记的指针, 指向当前节点的前一个指针, 后退指针在程序从表尾向表头遍历时使用
score
: 分值, 节点中的1.0
, 2.0
, 3.0
是各节点保存的分值, 在跳表中节点按各自保存的分值从小到大排列.
obj
: 成员对象, 各个节点中的o1, o2 和 o3 是节点保存的成员对象.
注意: 表头节点和其它节点的构造是一致的, 也有后退指针, 分值 和 成员对象, 但不会被使用.
跳跃表节点
由redis.h/zskiplistNode
结构定义
typedef struct zskiplistNode {
// 层
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
}level[];
struct zskiplistNode *backward; // 后退指针
double score; // 分值
robj *obj; // 成员对象
} zskiplistNode;
层
跳跃表节点的level数组可以包含多个元素, 每个元素都包含一个指向其它节点的指针, 程序可以通过这些层来加速访问其它节点的速度, 一般来说、层的数量越多, 访问其它节点的速度就越快. 每次创建一个新的跳表时、程序根据幂次定律随机生成一个介于1和32之间的值作为level数组的大小, 即 层高
前进指针
每一个层都有一个指向表尾方向的前进指针(level[i].forward), 用于从表头向表尾方向访问节点.
跨度
(level[i].span)用于记录两个节点之间的距离, 两个节点之间的跨度越大,相距就越远. 指向null的所有前进指针的跨度都为0, 因为他们没有连向任何节点
后退指针
(backward)用于从表尾向表头方向访问节点, 跟一次可以跳多个节点的前进指针不同, 每个节点只有一个后退指针、只能后退至前一个节点.
分值和成员
(score
)是一个double类型的浮点数, 跳跃表中所有的节点都按分值从小到大来排序(obj
)是一个指针、指向一个字符串对象、字符串对象中则保存着一个SDS
值
在同一个跳表中、各节点保存的成员对象必须是唯一的、但多个节点保存的分值却是可以相同的. 分值相同的节点按照成员对象在字典序中的大小排序, 小的节点放在前边.
跳跃表
仅靠多个跳跃表节点就可以完成一个跳表、但使用一个zskiplist
结构来持有这些节点, 程序可以更方便的对整个跳表进行处理, eg. 快速访问跳表的头结点和尾结点、快速获取节点数量.
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 表头节点和表尾节点
unsigned long length; // 表中节点的数量
int level; // 表中层数最大的节点的层数
} zskiplist;
header
和 tail
指针分别指向表头和表尾节点, 程序定位表头节点和表尾节点的复杂度是 O(1)
通过length
属性记录节点的数量、程序可以在 O(1) 复杂度内返回跳表长度
level
属性则用于在 O(1)复杂度内获取跳表中层高最大的节点的层数量(表头节点的层高不计)
重点回顾
- 跳表是有序集合的底层实现之一
- Redis的跳表实现由
zskiplist
和zskiplistNode
两个结构组成, 其中zskiplist
用于保存跳表信息(eg. 表头节点、表尾节点、长度)、而zskiplistNode
则用于表示跳跃表节点 - 每个跳跃表节点的层高都是 1~32 之间的随机数
- 同一个跳表中、多个节点可以包含相同的分值、但每个节点的成员对象必须是唯一的
- 跳表中的节点按照分值大小排序、分值相同时、节点按照成员对象的大小排序