详解Redis数据结构之跳跃表

1. 跳跃表概述

Redis是一个内存数据库,支持多种数据结构,跳跃表(Skip List)是其中之一。跳跃表是一种基于链表实现的数据结构,提供了快速查找、插入和删除元素的方法。它在可预测的时间复杂度内查找元素,与平衡树相比,跳跃表有着更高的查找效率,更简单的实现和更容易扩展。

2. 跳跃表的结构

跳跃表的结构由多个层组成,每一层都是一个链表。跳表的神奇之处在于,每个元素可以在多个层中存在,且下层的元素数量是上层的元素数量的零头,每层元素的分布完美地组成了一个线性的层次结构,层数越高,元素数量越少,每个元素在上层都有一定概率出现。顶层只有一个元素,底层可以有很多个元素,最后一层称为基础层(base layer)。

2.1 节点结构

跳跃表中每个节点由以下几部分组成:

值:节点存储的值,用作查找,插入和删除操作的依据。

前驱指针:指向链表中前一个节点。

后继指针:指向链表中后一个节点。

短路径指针:指向当前节点在上一层的对应节点(或空节点)。

typedef struct zskiplistNode {

sds ele; // 节点存储的值

double score; // 分值,用于排序

struct zskiplistNode *backward; // 前驱指针

struct zskiplistLevel {

struct zskiplistNode *forward; // 后继指针

unsigned int span; // 本节点到下一节点的距离

} level[];

} zskiplistNode;

2.2 层级结构

每个跳跃表节点的层级是通过随机算法决定的,每个节点的层数通过抛硬币实现。从基础层向上层升级,该节点的层数增加的概率就会逐渐减少,每提升一层,这个概率就会减半,直到概率小于等于0.5%

+---------+

| node |

+---------+

+-------+ span=3 +------+ +------+

| | | |

+---------+ v v v v

| node |------------->| node |->| NULL |

+---------+ span=1 span=1 | |

| | |

+---------+ v +---------+ v

| node |->| node |->| NULL | | +------+

+---------+ span=1 | | | | NULL |

| | | | |

+---------+ v v +------+

| node |->| NULL |

+---------+ | |

v |

+------+

| NULL |

+------+

3. 跳跃表的操作

3.1 插入操作

在跳跃表中插入一个节点的时候,需要首先找到待插入节点的前驱节点,然后在各层之间依次插入新节点。在每个层中,新节点的前驱是该层中待插入节点的前驱节点,后继是该节点原有的后继节点。

插入节点需要维护每层节点的 span 值,用于指示每一层中新节点与后面节点的距离,新节点与前驱节点在同一层,因此跨度为 1,而对于各层之间,跨度就是其下层节点的 span 值。

zskiplistNode *zslInsert(zskiplist *zl, double score, sds ele) {

zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x; // update 数组记录每一层插入节点的前驱

unsigned int rank[ZSKIPLIST_MAXLEVEL];

int i, level;

// 查找待插入节点的前驱节点

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 (x->level[i].forward &&

(x->level[i].forward->score < score ||

(x->level[i].forward->score == score &&

strcmp(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;

}

3.2 删除操作

在跳跃表中删除一个节点的时候,需要首先找到该节点的前驱节点,然后在各层之间依次删除节点。在每个层中,待删除节点的前驱是该层中待删除节点的前驱节点,后继是该节点原有的后继节点。

删除节点需要同时更新每一层节点的 span 值,使得跨度正确。对于每一层中的被删节点,它的前驱节点的跨度将变为之前的跨度减去该节点原有跨度。

zskiplistNode *zslDelete(zskiplist *zl, double score, sds ele) {

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 &&

strcmp(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(ele,x->ele) == 0) {

for (i = 0; i < zsl->level; i++) {

if (update[i]->level[i].forward == x) {

update[i]->level[i].span += x->level[i].span - 1;

update[i]->level[i].forward = x->level[i].forward;

} else {

update[i]->level[i].span -= 1;

}

}

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--;

return x;

}

return NULL;

}

3.3 查找操作

在跳跃表中查找某一个值的节点的时候,需要从顶层开始遍历,查找到节点对应的值小于等于当前值,直到找到基础层。最后在基础层中找到值相等的节点,即为匹配节点。

zskiplistNode *zslGetElementByRank(zskiplist *zl, unsigned long rank) {

unsigned long traversed = 0;

int i;

zskiplistNode *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;

}

4. 总结

通过本文的介绍,我们可以了解到跳表的优越性以及实现。跳表作为Redis中常用的数据结构之一,在应用开发中扮演了重要的角色,掌握跳表的原理和实现,有助于我们更好地进行数据调用和优化。

免责声明:本文来自互联网,本站所有信息(包括但不限于文字、视频、音频、数据及图表),不保证该信息的准确性、真实性、完整性、有效性、及时性、原创性等,版权归属于原作者,如无意侵犯媒体或个人知识产权,请来电或致函告之,本站将在第一时间处理。猿码集站发布此文目的在于促进信息交流,此文观点与本站立场无关,不承担任何责任。

数据库标签