有序集合是一种经常使用的数据类型,比如根据成绩对学生进行排名等。有序集合的底层实现可以有多种不同的选择,我们可以使用数组、链表、平衡树等结构。数组不便于元素的插入和删除,链表的查询效率低,平衡树或者红黑树虽然效率高,但是实现复杂。于是Redis使用了一种新型的数据结构--跳跃表,跳跃表的效率堪比红黑树,但实现却远比红黑树简单。
1. 分层有序链表
跳跃表可以抽象为分层有序链表,通过分层有序链表我们可以了解到跳跃表的相关知识点。首先考虑的一个问题是如何通过分层有序链表快速查找元素。
1.1 查询节点
我们以查找元素51为例,首先从最上层的链表开始查找,如果当前节点的分值小于要要查找的分值且下一个节点不为NULL,则比较下一个节点,反之降低一层继续查找。
可以看到使用分层有序列表只需要比较4个节点,而单个链表则需要比较6个节点。
现在我们可以定义数据结构来表示该分层有序链表。
// 节点
struct hierarchical_ordered_linked_list_node {
// 分值
double score;
// 节点值
void* value;
// 指向下一个节点
struct hierarchical_ordered_linked_list_node* level[];
};
struct hierarchical_ordered_linked_list {
// 节点的数量
int size;
// 除头节点外最高的节点
int level;
// 指向头节点
struct hierarchical_ordered_linked_list_node* header;
};
1.2 查询某个节点的排名
为了在查找节点的过程中计算该节点的排名,我们需要节点提供一些额外的信息,比如从当前节点到下一个节点相差多少个节点,如在第三层,节点1和节点21相差两个节点。
// 节点
struct hierarchical_ordered_linked_list_node {
// 分值
double score;
// 节点值
void* value;
// 指向下一个节点
struct hierarchical_ordered_linked_list_level {
hierarchical_ordered_linked_list_node* next;
int span;
} level[];
};
struct hierarchical_ordered_linked_list {
// 节点的数量
int size;
// 除根节点外最高的节点
int level;
// 指向头节点
struct hierarchical_ordered_linked_list_node* header;
};
查找时的伪代码:
int rank = 0;
hierarchical_ordered_linked_list_node* node = holl.header->level[holl.level - 1].next;
for (int i = holl.level - 1; i >= 0; --i) {
while (node->level[i].next && (node->level[i].next->score < score || (node->level[i].next->score == score || strcmp(node->level[i].next->value, value) < 0)) {
rank += node->level[i].span;
node = node->level[i].next;
}
if (node->score == score && node->value == value) {
return span;
}
}
return 0;
1.3 插入节点
插入节点首先需要做的找到插入的位置,即最底层最后一个小于新节点分值的节点后面插入。插入节点之后我们需要更新部分节点的指针指向。如节点21、节点41和节点51。
Q1:如何更新节点的指向呢?
在搜索节点插入位置的时候,我们可以将要更新的节点存放到一个数组中,我们称其为update数组,其中的节点为update节点,那么什么节点可以是update节点呢?通过观察我们可以看到每一层中最后一个小于待插入节点的节点就是update节点。
还有一个问题就是update节点所在节点的哪一层指针需要更新?通过观察我们发现每一层只能有一个update节点,因此我们可以通过update数组的下标记录那一层需要更新,比如节点51对应update数组的下标为0,表示第0层需要更新,而节点41对应update数组的下标为1,表示第1层需要更新,而节点21对应update数组的下标为2,表示第二层需要更新。
update[0] = node_51;
update[1] = node_41;
update[2] = node_21;
在更新的时候,操作如下:
for (int i = 0; i <= 2; ++i) {
// update节点的next指针
hierarchical_ordered_linked_list_node* next = update[i].level[i].next;
// update节点的对应层级的节点指向新插入的节点
update[i].level[i].next = new_node;
// 新节点的对应层的下一个节点为update节点在该层指向的下一个节点。
new_node.level[i].next = next;
}
Q2:如何更新用于计算rank的span呢?
for (int i = 0; i <= 0; --i) {
new_node.level[i].next = update[i].level[i].next;
update[i].level[i].next = new_node;
new_node.level[i].span = update[i].level[i].span - (rank[0] - rank[i]);
update[i].level[i].span = rank[0] - rank[i] + 1;
}
2. Redis实现
2.1 Redis 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;
2.3 创建跳跃表
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
// 分配空间
zsl = zmalloc(sizeof(*zsl));
// 设置高度和起始层数
zsl->level = 1;
zsl->length = 0;
// 初始化表头节点
// T = O(1)
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;
}
创建跳跃表的步骤如下:
- 分配空间
- 设置属性
- 创建Header节点
2.3 创建跳跃表节点
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
// 分配空间
zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));
// 设置属性
zn->score = score;
zn->obj = obj;
return zn;
}
创建跳跃表节点的步骤如下:
- 分配空间
- 设置属性:分值和值
2.4 节点搜索
根据Rank搜索:
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;
int i;
// T_wrost = O(N), T_avg = O(log N)
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;
}
// 如果越过的节点数量已经等于 rank
// 那么说明已经到达要找的节点
if (traversed == rank) {
return x;
}
}
// 没找到目标节点
return NULL;
}
2.5 插入节点
zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;
redisAssert(!isnan(score));
// 在各个层查找节点的插入位置
// T_wrost = O(N^2), T_avg = O(N log N)
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
// 如果 i 不是 zsl->level-1 层
// 那么 i 层的起始 rank 值为 i+1 层的 rank 值
// 各个层的 rank 值一层层累积
// 最终 rank[0] 的值加一就是新节点的前置节点的排位
// rank[0] 会在后面成为计算 span 值和 rank 值的基础
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
// 沿着前进指针遍历跳跃表
// T_wrost = O(N^2), T_avg = O(N log N)
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
// 比对分值
(x->level[i].forward->score == score &&
// 比对成员, T = O(N)
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
// 记录沿途跨越了多少个节点
rank[i] += x->level[i].span;
// 移动至下一指针
x = x->level[i].forward;
}
// 记录将要和新节点相连接的节点
update[i] = x;
}
/* we assume the key is not already inside, since we allow duplicated
* scores, and the re-insertion of score and redis object should never
* happen since the caller of zslInsert() should test in the hash table
* if the element is already inside or not.
*
* zslInsert() 的调用者会确保同分值且同成员的元素不会出现,
* 所以这里不需要进一步进行检查,可以直接创建新元素。
*/
// 获取一个随机值作为新节点的层数
// T = O(N)
level = zslRandomLevel();
// 如果新节点的层数比表中其他节点的层数都要大
// 那么初始化表头节点中未使用的层,并将它们记录到 update 数组中
// 将来也指向新节点
if (level > zsl->level) {
// 初始化未使用层
// T = O(1)
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,obj);
// 将前面记录的指针指向新节点,并做相应的设置
// T = O(1)
for (i = 0; i < level; i++) {
// 设置新节点的 forward 指针
x->level[i].forward = update[i]->level[i].forward;
// 将沿途记录的各个节点的 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]);
// 更新新节点插入之后,沿途节点的 span 值
// 其中的 +1 计算的是新节点
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
/* increment span for untouched levels */
// 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点
// T = O(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;
}
插入节点的步骤如下:
- 查找插入的位置,同时记录update节点以及update节点对应的rank值
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
/* store rank that is crossed to reach the insert position */
// 如果 i 不是 zsl->level-1 层
// 那么 i 层的起始 rank 值为 i+1 层的 rank 值
// 各个层的 rank 值一层层累积
// 最终 rank[0] 的值加一就是新节点的前置节点的排位
// rank[0] 会在后面成为计算 span 值和 rank 值的基础
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
// 沿着前进指针遍历跳跃表
// T_wrost = O(N^2), T_avg = O(N log N)
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
// 比对分值
(x->level[i].forward->score == score &&
// 比对成员, T = O(N)
compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
// 记录沿途跨越了多少个节点
rank[i] += x->level[i].span;
// 移动至下一指针
x = x->level[i].forward;
}
// 记录将要和新节点相连接的节点
update[i] = x;
}
- 为新插入的节点生成一个随机的level值
// 获取一个随机值作为新节点的层数
level = zslRandomLevel();
// 如果新节点的层数比表中其他节点的层数都要大
// 那么初始化表头节点中未使用的层,并将它们记录到 update 数组中
// 将来也指向新节点
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,obj);
- 更新新节点在各层的指针指向和span,同时更新每层对应的更新节点的指针指向和span
for (i = 0; i < level; i++) {
// 设置新节点的 forward 指针
x->level[i].forward = update[i]->level[i].forward;
// 将沿途记录的各个节点的 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]);
// 更新新节点插入之后,沿途节点的 span 值
// 其中的 +1 计算的是新节点
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
- 更新节点中大于新节点level的层对应的span要加1。
// 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点
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;
- 节点数加1
// 跳跃表的节点计数增一
zsl->length++;