Redis数据结构之跳跃表

Redis数据结构

跳跃表

跳跃表是一种有序数据结构,它通过在每个节点中维系多个指向其他节点的指针来达到快速访问节点的目的。
跳跃表支持平均查找O(logn)、最坏O(N)复杂度的节点查找,还可以通过顺序性来批量处理节点。
Redis使用跳跃表来作为有序集合的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合的元素成员是比较长的字符串时,Redis就会采用跳跃表来实现有序集合。

跳跃表的实现

Redis跳跃表是由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode表示节点,而zskiplist用于保存跳跃节点的相关信息,比如节点数量,以及指向表头节点和表尾节点的指针。

在这里插入图片描述

上图展示了一个跳跃表,位于左边的是zskiplist结构,该结构包含以下属性:

  • header:指向跳跃表头节点;
  • tail:指向跳跃表尾节点;
  • leval:记录当前跳跃表内层数最大的那个节点的层数(表头节点忽略不计)
  • length:记录跳跃表长度(跳跃表包含的节点个数)

位于右边4个的是zskiplistNode结构,该结构包含以下属性:

  • 层(level):节点中用L1,L2等来表示第一层,第二层。每个层都带有两个属性:前进指针和跨度。前进指针用于访问后序节点(即表尾方向的节点),跨度则记录了前进指针指向的下一个节点和当前节点的距离。
  • 后退指针(backward):节点中用BW标记节点的后退指针,它指向位于当前节点的前一个节点。
  • 分值(score):在跳跃表中,节点按照分值来积性排序;
  • 成员对象(obj):各个节点的o1、o2、o3是节点保存的成员对象。
跳跃表节点
typedef struct zskiplistNode {
    
    // 后退指针
    struct zskiplistNode *backward;
    
    // 分值
    double score;
    
    // 成员对象
    robj *obj;
    
    // 层
    struct zskiplistLevel {
        
        // 前进指针
        struct zskiplistNode *forward;
        
        // 跨度
        unsigned int span;
    } level [];
} zskiplistNode;

  1. 每次创建一个跳跃表节点时,程序都会根据幂次定律(越大的数出现的概率越小)来随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的高度。
跳跃表

仅靠多个跳跃表节点就可以组成一个跳跃表。zskiplist结构如下:

typedef struct zskiplist {
    
    //表头节点和表尾节点
    struct skiplistNode *header, *tail;
    
    // 表中节点的数量
    unsigned long length;
    
    // 表中层数最大的节点的层数
    int level;
} zskiplist;
跳跃表操作
新建跳表

跳跃表的创建调用 zslCreate 接口,默认层数 level 为 1, 跳跃表长度 length 为 0,tail 置NULL, zslCreateNode 为创建一个跳跃表结点的接口,这里用来创建头结点,函数实现在 t_zset.c 中:

/* Create a new skiplist. */
zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl;
   
    //申请内存 
    zsl = zmalloc(sizeof(*zsl));
    
    // 初始化level为1
    zsl->level = 1;
   
   // 初始化长度为0
    zsl->length = 0;
    
    // 新建头节点
    zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
    
    // 创建一个层数为32,分值为0,成员对象为NULL的表头结点
    for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
        // 设定每层的forward指针指向NULL
        zsl->header->level[j].forward = NULL;
        // 设定跨度为0
        zsl->header->level[j].span = 0;
     }
     
    // 设定backward指向NULL
    zsl->header->backward = NULL;
    zsl->tail = NULL;return zsl;}
插入新节点

跳跃表的插入操作类似于链表插入操作,首先要查找节点的插入位置,生成一个节点,然后在进行插入操作。跳跃表的操作操作分为4个步骤:

  1. 寻找插入位置;
  2. 随机插入节点层数;
  3. 生成插入节点并插入;
  4. 更新其他额外信息。
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    /*
     * 1. 寻找插入位置
     */
    serverAssert(!isnan(score));
    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 &&
                    sdscmp(x->level[i].forward->ele,ele) < 0)))
        {
            rank[i] += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    
    /* 
     * 2. 随机插入节点层数
     */
    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;
    }
    
    /*
     * 3. 生成节点并插入
     */
    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]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    /* increment span for untouched levels */
    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;
}

新结点在第i层的后继节点,也就是之前update[i]的后继节点, 它的排名是
update[i]->level[i].span+ rank[i],插入新结点之后,它的排名加1,也就是
update[i]->level[i].span + rank[i] + 1。新结点的排名,就是rank[0]+ 1,因此,新结点在第i层的层跨度就是(update[i]->level[i].span + rank[i] + 1) – (rank[0] + 1),也就是
update[i]->level[i].span- (rank[0] - rank[i])。

前驱结点update[i]的层跨度,等于新结点的排名rank[0]+ 1,减去update[i]的排名rank[i],也就是(rank[0] + 1) - rank[i],也就是(rank[0] -rank[i]) + 1。

删除节点

跳跃表的删除操作与链表的删除操作类似,主要分为2个步骤:

  1. 寻找待删除节点;
  2. 执行节点删除操作
/* Internal function used by zslDelete, zslDeleteRangeByScore and
 * zslDeleteRangeByRank. */
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--;
}

a、遍历所有层 i,如果待删除结点 x 的层高小于 i,显然这一层的前驱结点 update[i] 在第 i 层的的后继结点不会是x,所以只需要将前驱结点的第 i 层的跨度 span 减1 即可;否则,前驱结点的第 i 层的后继结点就是 x,这时候需要根据 x 的 forward 和 span 对前驱结点的第 i 层 forward 和 span 进行更新;
b、如果被删除的结点是原跳跃表的最后一个结点(没有后继结点),则更新跳跃表的 tail 指针;否则,更新它后继结点的 backward 指针;
c、结点删除后,如果删除的结点的层高是其它所有结点中最高的 (没有并列),那么,势必会导致整个跳跃表的最大层高的减少,这时就要将跳跃表的 level 字段进行更新。最后 length 自减 1。

参考文献

1.《Redis设计与实现》
2. https://blog.csdn.net/lz710117239/article/details/78408919
3. https://blog.csdn.net/WhereIsHeroFrom/article/details/84997418

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值