redis源码注释四:跳表

本文深入解析跳表数据结构的原理,包括其随机化特性、时间复杂度及空间复杂度分析,重点介绍Redis中跳表的具体实现,涵盖创建、插入、删除、查找等核心操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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 1p
那么:
层数为1的概率为 1 − p 1-p 1p
层数为2的概率为 p ( 1 − p ) p(1-p) p(1p);第1次为正面,第2次为反面
层数为3的概率为 p 2 ( 1 − p ) p^2(1-p) p2(1p);第1次为正面,第2次为正面,第3次为反面
层数为k的概率为 p k − 1 ( 1 − p ) p^{k-1}(1-p) pk1(1p);第1次为正面,第2次为正面,第k次为反面

so:总的结点数=每层的结点个数求和,即: Σ ( 层 数 ∗ 对 应 概 率 ) \Sigma(层数*对应概率) Σ()
在这里插入图片描述
重要的步骤:
1) k p k − 1 kp^{k-1} kpk1等于 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

该函数用于获取一个随机的层数,层数大小在1ZSKIPLIST_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) 

函数原形:给定scoreele,返回新插入的结点。

插入新结点需要注意什么,我们可以根据结点的结构体罗列出来。

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. 总结

想明白跳表的操作需要个好脑子,可惜我没有。
脑子里要时刻有个跳表,尤其是结构体组成,不然就晕了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值