简介
跳跃表是一种有序数据结构,它通过在每个节点中维持指向其它节点的指针,从而达到快速访问节点的目的。
redis用它来实现有序集合健(zset)
复杂度:平均O(logN) 最坏O(N)
跳跃表的结构
/*
* 跳跃表
*/
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
- header和tail指针分别指向跳跃表的表头结点和表尾结点,通过这两个指针,定位表头结点和表尾结点的复杂度为O(1)。
- 表尾结点是表中最后一个结点。而表头结点实际上是一个伪结点,该结点的成员对象为NULL,分值为0,它的层数固定为32(层的最大值)。
- length属性记录结点的数最,程序可以在O(1)的时间复杂度内返回跳跃表的长度。
- level属性记录跳跃表的层数,也就是表中层高最大的那个结点的层数,注意,表头结点的层高并不计算在内。
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
- obj是该结点的成员对象指针(member),score是该对象的分值,是一个浮点数,跳跃表中的所有结点,都是根据score从小到大来排序的。
- 同一个跳跃表中,各个结点保存的成员对象必须是唯一的,但是多个结点保存的分值却可以是相同的:分值相同的结点将按照成员对象的字典顺序从小到大进行排序。
- level数组是一个柔性数组成员,它可以包含多个元素,每个元素都包含一个层指针(level[i].forward),指向该结点在本层的后继结点。该指针用于从表头向表尾方向访问结点。可以通过这些层指针来加快访问结点的速度。
- 每次创建一个新跳跃表结点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是该结点包含的层数。
/*
* 有序集合
*/
typedef struct zset {
// 字典,键为成员,值为分值
// 用于支持 O(1) 复杂度的按成员取分值操作
dict *dict;
// 跳跃表,按分值排序成员
// 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
// 以及范围操作
zskiplist *zsl;
} zset
添加member和score时会把member和score存入dict里,用来支持 O(1) 复杂度的按成员取分值操作
实现逻辑
1. 获取集合的长度O(1) zskiplist->length
2. 获取score值O(1) redis会把member和score存入dict里
3. zrank怎么取排名的? 时间复杂度:T_wrost = O(N), T_avg = O(log N)
4. zadd新增节点 时间复杂度:T_wrost = O(N), T_avg = O(log N)
- redis从header节点遍历整个跳跃表,从level层开始,沿着前进指针遍历,在各个层查找节点的插入位置,沿途会记录下跨越的节点数rank[i],和将要和新节点相连接的节点update[i]
- 通过幂次定律获取一个随机值作为新节点的层数,最大32,然后创建新节点
- 将前面记录的指针指向新节点,并做相应的设置,设置各个层的span,forward
- 设置新节点的后退指针,跳跃表的节点计数增一
最后
个人公众号:技术源区
欢迎扫描下图关注公众号:技术源区,分享一些有深度的文章!