跳跃表的实现
zskiplist
typedef struct zskiplist {
//header:指向跳跃表的表头节点。
//tail:指向跳跃表的表尾节点。
struct zskiplistNode *header, *tail;
//length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)
unsigned long length;
// level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。
int level;
} zskiplist;
注意表头节点和其他节点的构造是一样的:表头节点也有后退指针、分值和成员对象,不过表头节点的这些属性都不会被用到,所以图中省略了这些部分,只显示了表头节点的各个层。
跳跃表与普通的链表相比:
跳跃表上的一个节点包含多个元素,每个元素可以指向另外一个节点上的某个元素
头节点
尾节点
长度 length
记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)
层数最大节点的层数 level
记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
zskiplistNode
/* ZSETs use a specialized version of Skiplists */
/*zskiplist 管理整个跳表,zskiplistNode 表示跳表上的一个节点
typedef struct zskiplistNode {
//成员对象:各个节点中的o1、o2和o3是节点所保存的成员对象。
sds ele;
//score:各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列
double score;
//backward指针,节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用
struct zskiplistNode *backward;
//level:节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层
//每个层都带有两个属性:前进指针和跨度
struct zskiplistLevel {
//前进指针用于访问位于表尾方向的其他节点
struct zskiplistNode *forward;
//跨度则记录了前进指针所指向节点和当前节点的距离
unsigned long span;
} level[];
} zskiplistNode;
跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。
每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。
下图分别展示了三个高度为1层、3层和5层的节点,因为C语言的数组索引总是从0开始的,所以节点的第一层是level[0],而第二层是level[1],以此类推。
属性
sds ele 成员对象
double score 分数
看到分数,应该可以想到它是实现有序集合的重要数据结构
struct zskiplistNode *backward 后退指针
后退指针的作用
- 有序结合 sorted set 它有倒序查询的功能实现, 跳跃表可以通过后退指针实现从后往前遍历
level 结构体数组
struct zskiplistLevel {
//前进指针用于访问位于表尾方向的其他节点
struct zskiplistNode *forward;
//跨度则记录了前进指针所指向节点和当前节点的距离
unsigned long span;
} level[]
forward 索引作用,指向尾方向的其他节点
span 记录跨度
跳表 API 源码
zslCreate
初始化,创建头部节点,
/* Create a new skiplist. */
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl));
zsl->level = 1;
zsl->length = 0;
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL;
zsl->tail = NULL;
return zsl;
}
zslRandomLevel 创建每一层时随机生成一个高度
每次创建一个新跳跃表节点的时候,程序都根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。
通过zslRandomLevel获得节点的随机层数level之后,我们可以根据给定的数据元素ele以及以及分值score,通zslCreateNode函数来创建一个跳跃表节点;而当一个给定的跳跃表节点不在被使用的时候,则会通过zslFreeNode来进行释放
分析一下zslRandomLevel的算法
https://blog.youkuaiyun.com/kisimple/article/details/38706729
LinkedList 增删改查的时间复杂度都是O(N),它最大的问题就是通过一个节点只能reach到下一个节点,那么改进的思路就是通过一个节点reach到多个节点,例如下图,
这种情况下便可将复杂度减小为O(N/2)。这是一种典型的空间换时间的优化思路。
SkipList 更进一步,采用了分治算法和随机算法设计。将每个节点所能reach到的最远的节点,两个节点之间看成是一个组,整个SkipList被分成了许多个组,而这些组的形成是随机的。
总的来说,redis的跳表可以实现这样一种优化:通过level机制实现索引,这种索引的效率不是固定的,因为使用了随机算法。SkipList使用的是概率平衡
zslCreateNode
创建表头节点 zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
默认32层,
跳跃表节点查找
zslGetRank 返回节点所在排位
查询的范式
https://zhuanlan.zhihu.com/p/253010463
不管是查询还是插入,只要是根据节点定位的过程都差不多
// 创建一个跳跃表节点的数组update, 长度为所有元素的最高层,默认32
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i; //指针,指向
//x节点代表
x = zsl->header;
for (i = zsl->level - 1; i >= 0; i--)
{
while (x->level[i].forward &&
(x->level[i].forward->score ||
(x->level[i].forward->score == score &&
sdscmp->(x->level[i].forward->ele, ele) < 0)))
{
x = x->level[i].forward;
}
update[i] = x;
}
- 创建一个跳跃表节点的数组update,这个数组用于保存整个搜索所经历的路径,定义一个节点的指针x,初始指向跳跃表的头部,用于在遍历过程表示已查找到的最接近目标节点的节点。
- 从跳跃表的头部开始向后,从整个跳跃表的最高层向下,在每层去查找最接近目标节点的那个节点。
- 如果在某一层,通过在该层对节点的跳跃的遍历找到了更接近目标节点的节点,那么更新x指针。
- 完成跳跃表每一层的遍历后,无论x指针有没有被更新,都将x存储在update对应的层中,用于记录该层最接近目标节点的节点。
按照上述步骤完成了从跳跃表最高层向下的遍历之后,update[0]中存储的便是整个跳跃表中最接近目标节点的节点,那么update[0]-forward所指向的节点,应该就是最终查找的终点,通过对这个最终节点与目标节点的比较可以确定在这个跳跃表中是否能查找到目标节点。
跳跃表节点插入 zslInsert 重点
,
/* Insert a new node in the skiplist. Assumes the element does not already
* exist (up to the caller to enforce that). The skiplist takes ownership
* of the passed SDS string 'ele'. */
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
//通过zslInsert这个接口,会向指定的跳跃表中插入一个新的节点,调用者需要在调用之前保证,这个新的节点并不存在与给定的跳跃表中
serverAssert(!isnan(score));
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
//解读一下这个while条件 1. while
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele,ele) < 0)))
{
rank[i] += x->level[i].span;
//x开始根据跳表索引跳转
x = x->level[i].forward;
}
update[i] = x;
}
//找到了最近的
/* we assume the element is not already inside, since we allow duplicated
* scores, reinserting the same element should never happen since the
* caller of zslInsert() should test in the hash table if the element is
* already inside or not. */
* 给一个level
level = zslRandomLevel();
//该节点level更高,更新level
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
rank[i] = 0;
update[i] = zsl->header;
update[i]->level[i].span = zsl->length;
}
zsl->level = level;
}
x = zslCreateNode(level,score,ele);
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
/* update span covered by update[i] as x is inserted here */
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
zsl->length++;
return x;
}
1. while
分为两部分:两个部分是与的关系,第一部分的含义当前level存在后驱节点的意思。我们知道一开始查找的是首节点,它明确有zsl->level-1层,所以不会越界,但是高层后驱节点可能为null。
后半部分满足表示的含义如果 x的某一层level[i] 的后驱节点的分数小于当前分数或者等于当前分数但是比当前值的字符串小。
满足这两个任意一个说明这一层level[i] 指向的节点还在我们要定位的节点之前。(我们要找第一个比该节点大的值)
sdscmp 比较两个 sds对象
memcmp就是c函数,比较二者字符串的字典序大小,负数表示 s1小于s2。