当前位置: 首页 > article >正文

跳表skiplist

结构

Redis 中的跳跃表由 server.h/zskiplistNodeserver.h/zskiplist 两个结构定义,前者为跳跃表节点,后者则保存了跳跃节点的相关信息,同之前的 集合 list 结构类似,其实只有 zskiplistNode 就可以实现了,但是引入后者是为了更加方便的操作:
在这里插入图片描述

/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    sds ele;// value
    double score;// 分值
    
    struct zskiplistNode *backward;		// 后退指针  pre
    
    struct zskiplistLevel {
        struct zskiplistNode *forward;	// 前进指针  next
        unsigned long span;				// 跨度
    } level[];// 层   //柔性数组
    
} zskiplistNode;


typedef struct zskiplist {
    struct zskiplistNode *header, *tail;	// 跳跃表头指针
    unsigned long length;					// 表中节点的数量
    int level;								// 表中层数最大的节点的层数
} zskiplist;

初始化

zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;

    // 申请内存空间
    zsl = zmalloc(sizeof(*zsl));
    // 初始化层数为 1
    zsl->level = 1;
    // 初始化长度为 0
    zsl->length = 0;
    // 创建一个层数为 32,分数为 0,没有 value 值的跳跃表头节点
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    
    // 跳跃表头节点初始化
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        // 将跳跃表头节点的所有前进指针 forward 设置为 NULL
        zsl->header->level[j].forward = NULL;
        // 将跳跃表头节点的所有跨度 span 设置为 0
        zsl->header->level[j].span = 0;
    }
    // 跳跃表头节点的后退指针 backward 置为 NULL
    zsl->header->backward = NULL;
    // 表头指向跳跃表尾节点的指针置为 NULL
    zsl->tail = NULL;
    return zsl;
}

插入

t_zset.c/zslInsert
在这里插入图片描述

第一部分:声明需要存储的变量

// 存储搜索路径
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
// 存储经过的节点跨度
unsigned int rank[ZSKIPLIST_MAXLEVEL];
int i, level;

第二部分:搜索当前节点插入位置

插入一个节点

update:要变化的是,每一层中 最后一个比插入节点分数小的节点,变化的可能是span,也可能是指向(如果插入节点的层级够了的话,需要重建该层链表的指向),图中红色即对应的update节点。因为插入8节点的level只有1,update的0号是要重建链表指向的,123号则只修改span的值。

rankspan 是为了排名而增加的字段,因为插入会让在插入节点后所有节点排名+1,保存每个节点的排名是不可能的 (不然第一名易主,后面所有人的排名都要跟着变),可以的是保存节点和下一个节点排名的间隔(span),游戏中的实时排行榜记录的一定是排名的间隔,把排名的变化缩小到前后节点的局部变化。

serverAssert(!isnan(score));
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].ele, ele) < 0)	))
    {
        rank[i] += x->level[i].scan;
        x = x->level[i].forward;
    }
    // 记录搜索路径
    update[i]=x;//出循环时,x的下一个节点分数比要插入的高,所以需要修改的是x
}

第三部分:生成插入节点

/* 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();
// 如果随机生成的 level 超过了当前最大 level 需要更新跳跃表的信息
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;//4-1
    update[i]->level[i].forward = x;//4-2

    /* update span covered by update[i] as x is inserted here */
    x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);//4-3
    update[i]->level[i].span = (rank[0] - rank[i]) + 1;//4-4
}

/* increment span for untouched levels */
for (i = level; i < zsl->level; i++) {
    update[i]->level[i].span++;//4-5
}

第五部分:重排后向指针并返回

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;

删除

删除过程由源码中的 t_zset.c/zslDeleteNode 定义,和插入过程类似,都需要先把这个 “搜索路径” 找出来,然后对于每个层的相关节点重排一下前向后向指针,同时还要注意更新一下最高层数 maxLevel,直接放源码 (如果理解了插入这里还是很容易理解的):
在这里插入图片描述

删除一个节点

update:要变化的是,每一层中 最后一个比插入节点分数小的节点,变化的可能是span,也可能是指向(如果删除节点的层级够了的话,需要重建该层链表的指向),图中红色即对应的update节点。因为删除的8节点的level只有1,update的0号是要重建链表指向的,123号则只修改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]->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--;
}

/* Delete an element with matching score/element from the skiplist.
 * The function returns 1 if the node was found and deleted, otherwise
 * 0 is returned.
 *
 * If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise
 * it is not freed (but just unlinked) and *node is set to the node pointer,
 * so that it is possible for the caller to reuse the node (including the
 * referenced SDS string at node->ele). */
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) {
        zslDeleteNode(zsl, x, update);
        if (!node)
            zslFreeNode(x);
        else
            *node = x;
        return 1;
    }
    return 0; /* not found */
}

更新

当我们调用 ZADD 方法时,如果对应的 value 不存在,那就是插入过程,如果这个 value 已经存在,只是调整一下 score 的值,那就需要走一个更新流程。

假设这个新的 score 值并不会带来排序上的变化,那么就不需要调整位置,直接修改元素的 score 值就可以了,但是如果排序位置改变了,那就需要调整位置,该如何调整呢?

从源码 t_zset.c/zsetAdd 函数 1350 行左右可以看到,Redis 采用了一个非常简单的策略:

/* Remove and re-insert when score changed. */
if (score != curscore) {
    zobj->ptr = zzlDelete(zobj->ptr,eptr);
    zobj->ptr = zzlInsert(zobj->ptr,ele,score);
    *flags |= ZADD_UPDATED;
}

把这个元素删除再插入这个,需要经过两次路径搜索,从这一点上来看,Redis 的 ZADD 代码似乎还有进一步优化的空间。

排名

跳跃表本身是有序的,Redis 在 skiplist 的 forward 指针上进行了优化,给每一个 forward 指针都增加了 span 属性,用来 表示从前一个节点沿着当前层的 forward 指针跳到当前这个节点中间会跳过多少个节点。在上面的源码中我们也可以看到 Redis 在插入、删除操作时都会小心翼翼地更新 span 值的大小。

所以,沿着 “搜索路径”,把所有经过节点(比查找节点分数小的节点)的跨度 span 值进行累加,直到找到 就可以算出当前元素的最终 rank 值了。

从表中也可以看到,从一个点到另一个点,不走回头路的情况下,span和一定是相同的
在这里插入图片描述

/* Find the rank for an element by both score and key.
 * Returns 0 when the element cannot be found, rank otherwise.
 * Note that the rank is 1-based due to the span of zsl->header to the
 * first element. */
unsigned long zslGetRank(zskiplist *zsl, double score, sds ele) {
    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))) {
            // span 累加
            rank += x->level[i].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) {
            return rank;
        }
    }
    return 0;
}

http://www.kler.cn/a/6648.html

相关文章:

  • 文心一言对接FreeSWITCH实现大模型呼叫中心
  • electron-vite【实战系列教程】
  • CH340系列芯片驱动电路·CH340系列芯片驱动!!!
  • 【机器人】ATM 用于策略学习的任意点轨迹建模 RSS 2024 | 论文精读
  • cenos如何升级git到2以上版本
  • Java中的LIst
  • Android12 Launcher3客制化:添加非抽屉模式(可动态切换)、图标自动补位功能
  • 使用C#中的Aspose Word 在对Word文档中的书签详细总结(包括增删改查等操作)
  • python实战应用讲解-【numpy专题篇】numpy常见函数使用示例(十)(附python示例代码)
  • 2023年美赛春季赛 赛题浅析
  • 在Linux系统下使用GDB调试C++程序【命令行调试与vscode编译器调试】
  • django前后端不分离——图片验证码,基于Bootstrap3的自定义分页器,Bootstrap模态框无法使用解决方案
  • list模拟实现
  • 00后面试华为软件测试工程师,竭尽全力拿到15K。。。。。
  • 解析安装程序使用指南
  • 华为OD机试-最优资源分配-2022Q4 A卷-Py/Java/JS
  • 美团暑期实习
  • 【python设计模式】10、组合模式
  • 从大厂到创业公司,管理上需要怎样转变?
  • ChatGPT背后的技术和多模态异构数据处理的未来展望——我与一位资深工程师的走心探讨
  • 获取元素通常使用的两种方式
  • 【华为机试真题详解JAVA实现】—坐标移动
  • 携手共进 智享未来丨美格智能2023年代理商合作伙伴大会成功举办
  • MAC干净卸载IDEA
  • 阿里9年测试经验告诉你:作为一名年薪40w自动化测试需要具备那些能力
  • PLC - 笔记