Redis 使用跳跃表(Skip List)作为其某些数据结构的底层实现,例如有序集合(Sorted Set)。跳跃表是一种随机化的数据结构,具有良好的平均时间复杂度,可以高效地进行插入、删除和查找操作。
跳跃表的基本概念
跳跃表通过多级链表来实现,每一级都包含从前一级抽取的一部分元素。因此,可以将跳跃表看作是多个层次的链表,这些链表以不同的概率跳过一些节点,从而实现快速的查找。
我们可以通过图形描述来展示跳跃表的各个节点及其层次关系。在下面的示例中,我们将展示一个包含若干节点的跳跃表,并分别说明插入、删除和查找操作的过程。
跳跃表的基本结构
假设我们有一个简单的跳跃表,包含以下几个节点:1, 3, 5, 7, 9。这些节点在跳跃表中的分布可能如下所示:
Level 3: 1------------------7
| |
Level 2: 1--------5---------7
| | |
Level 1: 1---3----5----7----9
| | | | |
Level 0: head--1---3----5----7----9--NULL
在这个图中,每一层表示跳跃表的一个级别(从底层 Level 0 到最高层 Level 3),每个节点都可能存在于多个层。
插入操作示例
假设我们要插入一个新的节点 4。首先,我们需要找到它应该插入的位置,然后按照随机生成的层数更新相关的指针。
- 在插入之前:
Level 3: 1------------------7
| |
Level 2: 1--------5---------7
| | |
Level 1: 1---3----5----7----9
| | | | |
Level 0: head--1---3----5----7----9--NULL
- 找到插入位置(介于 3 和 5 之间)并确定新节点的层数,假设新节点 4 的层数为 2:
Level 3: 1------------------7
| |
Level 2: 1--------5---------7
| | |
Level 1: 1---3----5----7----9
| | | | |
Level 0: head--1---3----5----7----9--NULL
^
4 (待插入)
- 插入后的结果:
Level 3: 1------------------7
| |
Level 2: 1--------5---------7
| | |
Level 1: 1---3---4----5----7----9
| | | | | |
Level 0: head--1---3---4----5----7----9--NULL
删除操作示例
假设我们要删除节点 5。首先,我们需要找到节点 5 的位置,然后更新相关的指针。
- 在删除之前:
Level 3: 1------------------7
| |
Level 2: 1--------5---------7
| | |
Level 1: 1---3---4----5----7----9
| | | | | |
Level 0: head--1---3---4----5----7----9--NULL
- 确定删除位置并逐层更新指针:
Level 3: 1------------------7
| |
Level 2: 1--------(delete)--7
| |
Level 1: 1---3---4----5----7----9
| | | | | |
Level 0: head--1---3---4----5----7----9--NULL
^
5 (待删除)
- 删除后的结果:
Level 3: 1------------------7
| |
Level 2: 1------------------7
| |
Level 1: 1---3---4---------7----9
| | | | |
Level 0: head--1---3---4---------7----9--NULL
查找操作示例
假设我们要查找节点 7。我们会从最高层开始,逐层向下前进,直到找到目标节点。
- 从最高层(Level 3)开始查找:
Level 3: 1------------------7
^
找到目标
Level 2: 1------------------7
Level 1: 1---3---4---------7----9
Level 0: head--1---3---4---------7----9--NULL
- 如果在当前层(Level 3)没有找到目标节点 7,就会继续在下一层(Level 2)查找:
Level 3: 1------------------7
|
Level 2: 1------------------7
^
找到目标
Level 1: 1---3---4---------7----9
Level 0: head--1---3---4---------7----9--NULL
- 如果在当前层(Level 2)没有找到目标节点 7,就会继续在下一层(Level 1)查找:
Level 3: 1------------------7
|
Level 2: 1------------------7
|
Level 1: 1---3---4---------7----9
^
找到目标
Level 0: head--1---3---4---------7----9--NULL
- 最后,如果需要,还可以继续在底层(Level 0)查找:
Level 3: 1------------------7
|
Level 2: 1------------------7
|
Level 1: 1---3---4---------7----9
|
Level 0: head--1---3---4---------7----9--NULL
^
找到目标
通过这种逐层查找的方法,跳跃表能够快速定位目标节点。每次查找都从最高层开始,逐层向下缩小范围,最终达到有效的查找性能。
跳跃表的结构
跳跃表的每个节点不仅包含指向下一个节点的指针,还可能包含指向更高层次的节点的指针。通常,一个节点会有多级指针,这些指针形成了多个层次的链表结构。
节点结构
在 Redis 的实现中,每个跳跃表节点(zskiplistNode)包含以下内容:
ele:节点存储的元素。score:节点的分数,用于排序。level[]:多个层次的指针和跨度信息。
typedef struct zskiplistNode {
sds ele; // 元素
double score; // 分数
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[];
} zskiplistNode;
跳跃表结构
跳跃表本身(zskiplist)包含:
header:指向头节点的指针。tail:指向尾节点的指针。level:当前跳跃表的最高层数。length:跳跃表中的节点数量。
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
跳跃表的操作
插入操作
插入操作需要找到合适的位置并调整相关指针。具体步骤如下:
- 从最高层开始,逐级向下查找插入位置。
- 在每一层,更新要插入位置之前的节点的指针。
- 根据随机算法确定新节点的层数。
- 插入新节点,并更新相关指针和跨度信息。
zskiplistNode* zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
// 初始化
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
rank[i] = (i == (zsl->level-1)) ? 0 : rank[i+1];
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->level[i].forward;
}
update[i] = x;
}
// 确定新节点的层数
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;
}
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;
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
// 更新跨越层次的跨度
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;
}
在这里,我们完成了插入操作,包括更新指针和跨度信息,并正确设置前后指针。
删除操作
删除操作需要找到要删除的节点,并调整相关指针。具体步骤如下:
- 从最高层开始,逐级向下查找待删除节点的位置。
- 在每一层,更新前一个节点的指针,使其跳过待删除节点。
- 更新跨度信息和后退指针。
- 如果删除的是最高层节点,还需要更新跳跃表的层数。
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;
}
x = x->level[0].forward;
if (x && score == x->score && sdscmp(x->ele, ele) == 0) {
/* Update the forward pointers of the nodes at each level */
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
update[i]->level[i].forward = x->level[i].forward;
update[i]->level[i].span += x->level[i].span - 1;
} else {
update[i]->level[i].span -= 1;
}
}
/* Update backward pointer */
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--;
if (node) *node = x;
return 1;
}
return 0;
}
在上述代码中,我们完成了删除操作的具体步骤,包括更新多层链表中的指针、跨度信息和后退指针,同时也处理了层级调整等细节。
查找操作
查找操作是跳跃表的核心功能之一。通过逐层前进,可以快速找到目标节点。
zskiplistNode* zslFind(zskiplist *zsl, double score, sds ele) {
zskiplistNode *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;
}
}
x = x->level[0].forward;
if (x && score == x->score && sdscmp(x->ele, ele) == 0) {
return x;
}
return NULL;
}
在查找操作中,从最高层开始逐层向下查找,直到找到目标节点或者确定目标节点不存在。
思考题1: 跳跃表层级个数是如何决定的?
在Redis中,跳跃表(Skip List)的实现是通过随机化算法来决定每个节点的层级数。具体来说,当一个新元素被插入到跳跃表中时,会为该元素随机生成一个层级数,称为“高度”。这个高度决定了该元素将在跳跃表中的多少个层级上存在。
以下是决定高度的过程:
- 随机数生成:使用一个随机数生成器来决定是否增加层级。具体方法通常是掷硬币(即生成0或1的随机数)。如果掷硬币结果为1,则增加一层,重复这一过程直到掷硬币结果为0或者达到预设的最大层级。
- 概率控制:可以通过控制掷硬币得到1的概率来调整层级增长的速度。在Redis的默认实现中,掷硬币得到1的概率是0.5,这意味着每一层下一层都有50%的概率被包含。
- 最大层级限制:为了避免生成过高的节点,Redis限制了节点的最高层级。默认情况下,这个最大层级数是32,但实际使用中会根据需要进行调整。
举个例子,如果你插入一个新元素,系统可能会连续多次得到1(假设前四次都得到1),然后得到一次0,那么这个新元素的层级就是4。
这种随机化的方法使得跳跃表具有良好的期望性能,能够在O(log N)时间复杂度内完成查找、插入和删除操作。
思考题2: 删除操作,是每个层级都删除嘛
是的,删除操作涉及到在跳跃表中每个层级都删除对应的节点。具体步骤如下:
- 查找目标节点:首先,通过各层级找到需要删除的目标节点。这一步和查找操作类似,从最高层级开始,如果当前层级的指针所指向的节点值大于或等于要删除的节点,则移动到下一层级重复这个过程。
- 更新指针:一旦找到所有层级中的目标节点,在每个层级上更新前驱节点的指针,使其跳过被删除的节点,直接指向目标节点的后继节点。
- 释放节点:最后,释放目标节点的内存。