0. 跳表
0.1 简介
跳跃表(skiplist)是一种随机化的数据,由William Pugh 在论文《Skip lists: a probabilistic
alternative to balanced trees》中提出,这种数据结构以有序的方式在层次化的链表中保存元
素,它的效率可以和平衡树媲美——查找、删除、添加等操作都可以在对数期望时间下完成,
并且比起平衡树来说,跳跃表的实现要简单直观得多。——《redis设计与实现》
跳表可以实现O(logN)
级别的查找时间复杂度,而且实现相对简单,不用像平衡树那样左旋右旋。
具体概念参考:一文彻底搞懂跳表的各种时间复杂度、适用场景以及实现原理
每次查找的时候都是从最上层开始找,如果查找的值大于当前结点的值则向右找,否则向下找。
思路上类似二分查找。
0.2 空间复杂度推导
这里比较关键的一个要素就是层数,每个结点的层数如何决定,层数有会怎样影响跳表的性能?
层数的决定是按照概率算法来的,简单点就是抛硬币,初始层数为1,如果是反面则层数不再累加,如果是正面则层数累加然后再次抛硬币,直到抛出反面为止。这里的概率就定为1/2.
我们将情况一般化,假设每次硬币为正面的概率为
p
p
p,反面的概率就是
1
−
p
1-p
1−p,
那么:
层数为1的概率为
1
−
p
1-p
1−p;
层数为2的概率为
p
(
1
−
p
)
p(1-p)
p(1−p);第1次为正面,第2次为反面
层数为3的概率为
p
2
(
1
−
p
)
p^2(1-p)
p2(1−p);第1次为正面,第2次为正面,第3次为反面
层数为k的概率为
p
k
−
1
(
1
−
p
)
p^{k-1}(1-p)
pk−1(1−p);第1次为正面,第2次为正面,第k次为反面
so:总的结点数=每层的结点个数求和,即:
Σ
(
层
数
∗
对
应
概
率
)
\Sigma(层数*对应概率)
Σ(层数∗对应概率)
重要的步骤:
1)
k
p
k
−
1
kp^{k-1}
kpk−1等于
p
k
p^k
pk的导数。
2)先求导再求和变为先求和再求导。
所以最后的结论是,空间复杂度与概率
p
p
p有关,如
p
p
p为
1
/
2
1/2
1/2,则复杂度为
O
(
2
N
)
O(2N)
O(2N);
redis中的
p
p
p采用的是
1
/
4
1/4
1/4,算下来是1.33.
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
虽然说理论上层数可以无限大,但是实际上这种情况不能发生,于是乎,redis定义了一个最大的层数,为64,只要大于64的层数统统取64.
#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */
1. 跳表结构
1.1 每个结点的结构
typedef struct zskiplistNode { //跳表中每个结点的结构
sds ele; //每个结点存储的内容
double score; //结点的分数
struct zskiplistNode *backward;//后退
struct zskiplistLevel {
struct zskiplistNode *forward;//某一层该节点的后继结点
unsigned long span;//这个层跨越的节点数量
} level[];//一个柔性数组,存储层该节点的后继
} zskiplistNode;
每个结点中比较难以理解的就是level[]
,这是一个数组,存储的是zskiplistLevel
结构,该结构中包含一个指向下一个结点的指针,实际上就是我们看到的这个:
图可以看这篇文章的:跳跃表以及跳跃表在redis中的实现
1.2 整个跳表的结构
typedef struct zskiplist { //跳表的结构
struct zskiplistNode *header, *tail;//指向头尾结点的指针
unsigned long length;//跳表的长度
int level;//跳表的最大层数
} zskiplist;
2. 跳表操作
2.1 创建跳表
2.1.1 创建跳表的结点zslCreateNode
zskiplistNode *zslCreateNode(int level, double score, sds ele)
创建一个层数为level,分数为score,内容为ele的结点。
zskiplistNode *zslCreateNode(int level, double score, sds ele) { //创建一个层数为level,分数为score,内容为ele的结点
zskiplistNode *zn =
zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));//由于柔型数组不算进结点的大小,所以总的大小是结点大小和柔性数组大小之和
zn->score = score;
zn->ele = ele;
return zn;
}
注意结点分配的尺寸。
2.1.2 创建跳表zslCreate
/* Create a new skiplist. */
zskiplist *zslCreate(void) { //返回指向跳表的指针
int j;
zskiplist *zsl;
zsl = zmalloc(sizeof(*zsl));
zsl->level = 1; //最大层数为1
zsl->length = 0;//结点个数为0
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);//头结点的层数是64
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;//将header每层结点的后继均设置为空
zsl->header->level[j].span = 0;
}
zsl->header->backward = NULL;
zsl->tail = NULL;//尾结点为空
return zsl;
}
2.2 释放跳表
释放跳表的过程就是释放跳表中每个结点的过程。
我们根据跳表的header获得跳表的第一个结点,然后挨个释放即可。
void zslFreeNode(zskiplistNode *node) { //释放单个结点
sdsfree(node->ele);//释放结点中的内容
zfree(node);//释放结点
}
/* Free a whole skiplist. */
void zslFree(zskiplist *zsl) { //释放整个跳表
zskiplistNode *node = zsl->header->level[0].forward, *next;//找到跳表第1层的首个结点
zfree(zsl->header);//释放header
while(node) {
next = node->level[0].forward;//当前节点的下一个
zslFreeNode(node);//释放当前结点,释放当前结点会将结点中的内容和指针等都释放
node = next;//继续释放下一个
}
zfree(zsl);
}
2.3 随机获取一个层数zslRandomLevel
该函数用于获取一个随机的层数,层数大小在1
到ZSKIPLIST_MAXLEVEL
之间。
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) //如果random返回的数在区间SKIPLIST_P * 0xFFFF内,则继续抛硬币
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;//层数最大不能超过ZSKIPLIST_MAXLEVEL
}
2.4 插入新结点zslInsert
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele)
函数原形:给定score
和ele
,返回新插入的结点。
插入新结点需要注意什么,我们可以根据结点的结构体罗列出来。
typedef struct zskiplistNode { //跳表中每个结点的结构
sds ele; //给定
double score; //给定
struct zskiplistNode *backward;//后退
struct zskiplistLevel {
struct zskiplistNode *forward;//某一层该节点的后继结点
unsigned long span;//这个层跨越的节点数量
} level[];//一个柔性数组,存储层该节点的后继
} zskiplistNode;
从上面结点的结构体可以看出,对于新插入的结点,要设置的主要是每一层的后继以及span。
同时,在插入新结点之后,整个链表的其他结点也会受到影响,其他结点的forward和span也可能会改变。
我们需要做的工作:
1)找到新结点的所有前驱结点,然后改变指针指向(这和单链表的插入没什么区别)
2)更新前驱结点和新结点的span
(这是比较麻烦的事情)
为了完成上述两项工作,我们使用update
数组存储新结点的前驱,使用rank
数组存储前驱结点在整个链表中的位置。
update[i]
表示新结点第i
层的前驱;
rank[i]
表示第i
层前驱在整个链表中的位置;如rank[2]=1
表示第2
层的前驱是整个链表的第1
个结点。
在准备完数组之后,我们插入结点的流程是:
1)找到新结点的所有前驱(查找某一结点的过程)
2)确定新结点的层数(抛硬币zslRandomLevel
)
3)改变指针指向,新结点指向前驱的后继,前驱的后继指向新结点(链表的插入)
4)更新 新结点的span,前驱结点的span
5)更新 新结点后继结点的前驱
6)增加跳表长度,返回
这里只是描述了大致的流程,在过程中还有些细节,看注释:
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;//update是要插入位置每一层的前驱结点们
unsigned int rank[ZSKIPLIST_MAXLEVEL];//rank表示前驱结点在整个链表中的位置,也就是第i层从链表头结点到插入位置一共有跨过了几个结点
int i, level;
serverAssert(!isnan(score));
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) { //找出每一层的update[i]和rank[i]
/* store rank that is crossed to reach the insert position */
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];//每一层的rank初始化为上一层的rank,因为整个查找的方向是向右或者向下,所以本层的rank一定大于等于上一层的rank
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)))//当前结点有后继而且后继的score小于插入的结点,则继续往右查找,直到没有后继后者后继的score大于插入的结点,使得循环退出的结点就是该层的update[i]
{
rank[i] += x->level[i].span;//不断更新rank,每次加上当前结点到其后继的跨度
x = x->level[i].forward;//更新当前结点
}
update[i] = x;//循环退出后,当前结点就是该层的update[i]
}
/* 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 = zslRandomLevel(); //给插入的结点随机获取一个层数
/*
如果随机获得的层数大于当前跳表的最大层数,
那么对于最大层数上面的某一层,从链表的头结点往右看,第一个看到的就是新插入的结点,
也就是说,新插入结点的前驱是头结点
*/
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;//对于头结点,没插入新结点之前,当前最高层上面的每一层,span都是整个链表的长度(一眼看到最右侧)
}
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]);//x在第i层的span等于前驱的span减去(rank[0] - rank[i]),(rank[0] - rank[i])是新结点到其前驱的距离
update[i]->level[i].span = (rank[0] - rank[i]) + 1;//前驱的span变为新结点到前驱的距离+1
}
/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;//高层的span加一,加的就是新增的结点
}
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;//如果新结点的后继存在,则将后继的前驱backward设置为新结点
else
zsl->tail = x;//新结点的后继不存在,新结点为跳表的尾结点
zsl->length++;//跳表长度累加
return x;//返回新插入的结点
}
这里比较难理解的地方:
1)rank[i]
初始化的方式
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
rank[i]
初始化为rank[i+1]
,这个要回想跳表中查找一个结点的过程,方向总是向右或者向下,所以下层的起始位置肯定不会比上一层的靠左。
2)span的更新
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);//x在第i层的span等于前驱的span减去(rank[0] - rank[i]),(rank[0] - rank[i])是新结点到其前驱的距离
update[i]->level[i].span = (rank[0] - rank[i]) + 1;//前驱的span变为新结点到前驱的距离+1
这个结合下图来看:
2.5 删除结点
2.5.1 已知前驱结点,删除某个结点
在已经知道待删除结点的所有前驱的情况下,我们将前驱的后继和前驱的span更改即可。
/* Internal function used by zslDelete, zslDeleteByScore and zslDeleteByRank */
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) { //删除某个结点
int i;
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) { //某一层前驱update[i]的后继为待删除的结点
update[i]->level[i].span += x->level[i].span - 1;//则更新span
update[i]->level[i].forward = x->level[i].forward;//并将后继指向待删除结点的后继
} else {
update[i]->level[i].span -= 1;//前驱update[i]的后继不是待删除的结点,说明层数较高,span--即可
}
}
if (x->level[0].forward) { //有后继
x->level[0].forward->backward = x->backward;//后继的前驱为待删除结点的前驱
} else {
zsl->tail = x->backward;//没有后继说明待删除结点为跳表尾结点,需要更新跳表尾结点
}
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;//更新这个跳表的最大层数
zsl->length--;//跳表长度减一
}
2.5.2 删除某结点
删除某结点的步骤为:
1)找到结点的所有前驱;
2)删除该节点;
函数原形:
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node)
返回值:1表示找到并删除了结点,0表示没找到。
注意传进去的node:
如果node为空,则找到结点删除后会将结点释放;
如果node不为空,则会将待删除的结点通过node带出来,调用者可以对带出来的node进行别的操作。
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
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)))
{
x = x->level[i].forward;
}
update[i] = x;//找到每一层中待删除结点的前驱
}
/* We may have multiple elements with the same score, what we need
* is to find the element with both the right score and object. */
x = x->level[0].forward;
if (x && score == x->score && sdscmp(x->ele,ele) == 0) { //x为待删除结点
zslDeleteNode(zsl, x, update);//删除x
if (!node)
zslFreeNode(x);//如果node为空则释放x
else
*node = x;//否则将x带出去,以便调用者再次使用
return 1;
}
return 0; /* not found */
}
2.6 查找结点
通过zslGetRank函数获得给定score和ele对应结点的rank,0表示没找到,非0表示对应rank;
然后根据rank调用zslGetElementByRank查找结点,NULL表示没找到,非NULL表示对应结点。
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) { //找到给定score和ele对应结点的rank
zskiplistNode *x;
unsigned long rank = 0;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
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 += x->level[i].span;//小于往右,rank累加上span
x = x->level[i].forward;
}
//否则往下
/* x might be equal to zsl->header, so test if obj is non-NULL */
if (x->ele && sdscmp(x->ele,ele) == 0) { //找到则返回rank
return rank;
}
}
return 0;//找不到返回0
}
/* Finds an element by its rank. The rank argument needs to be 1-based. */
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) { //根据rank找到某个结点
zskiplistNode *x;
unsigned long traversed = 0;
int i;
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
{
traversed += x->level[i].span;
x = x->level[i].forward;
}
if (traversed == rank) {
return x;//找到则返回结点
}
}
return NULL;//找不到返回空
}
3. 总结
想明白跳表的操作需要个好脑子,可惜我没有。
脑子里要时刻有个跳表,尤其是结构体组成,不然就晕了。