- skiplist是一种有序数据结构,通过在每个节点中维持多个指针指向其他指针,从而达到快速访问节点的目的;
- 平均查找O(NlogN),最坏O(N)的时间复杂度。
- 大部分情况下,skiplist效率可以跟平衡树相媲美,且实现更简单。
- Redis使用skiplist作为有序集合键的底层实现之一
1、skiplist的实现
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
zskiplist结构:
- header:指向跳跃表的表头节点
- tail:指向表尾节点
- level:记录当前跳跃表内,层数最大的那个节点的层数(表头节点层数不记录在内)
- length:记录长度,跳跃表目前包含节点的数量(不包含表头节点)
右边是四个zskiplistNode结构,包含:
- level:用L1、L2、L3等标记节点的各个层,L1代表第一层以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,跨度记录了前进指针所指向的节点和当前节点的距离。
- 后退指针(backward):用BW标记节点的后退指针,指向位于当前节点的前一个节点。
- 分值(score):节点按照各自所保存的分值从小到大排序;
- 成员对象(obj):节点保存的成员对象
表头节点也有后退指针、分值和成员对象,不过由于表头结点的这些属性不会用到,上图中将其省略了
1.1 跳跃表节点
层
zskiplistNode的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层加快访问其他节点的速度。
每次创建一个新zskiplistNode时,都根据幂次定律(越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小。
前进指针
前进指针主要用于从表头向表尾方向遍历(每次都从上到下找到第一个跨度为1的层)。如上图所示的遍历过程如下:
- 访问表头,从第四层的前进指针移动到第二个节点
- 在第二个节点中,沿着第二层的前进指针移动到表中的第三个节点
- 在第三个节点中,沿着第二层的前进指针移动到表中的第四个节点
- 当沿着第四个节点但的前进指针移动时,碰到一个NULL,到达跳跃表末尾,结束遍历。
跨度
- 指向NULL的前进指针的跨度为0.
- 跨度是用来计算rank的:在查找某个节点的过程中将沿途访问过的所有层的跨度累计起来就是目标节点阿紫跳跃表中的rank.
后退指针
- 用于从表尾向表头方向访问节点
- 每个节点只有一个后退指针,每次只能后退一步
- 根据tail访问表尾节点,依次通过后退指针访问前面的节点知道遇到NULL,访问结束;
分值和成员
- score是一个double的浮点数
- obj指向一个字符串对象,字符串对象里面存放着一个SDS值。
- 同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点的分值可以是相同的。
2、总结
- 跳跃表是有序集合的底层实现之一
- 每个跳跃表节点的层高都是1~32之间的随机数
- 按照分值大小排序,分数相同时。按照成员对象排序。