跳表源码,你也可以

需要一些C语言基础,本人也只是可以看懂C语言的水平,这样即可
阅读该文需要对Redis跳表有一定的认知。

跳表源码

skiplist和平衡树、哈希表的比较

  • 哈希表

    哈希表只能做单个key的查找,不适合范围查找。

  • 平衡树

    在做范围查找时,需要找到树上指定范围的最小数。然后进行中序遍历,查找其他不超过范围最大值的节点。而跳表,只需要找到较小值后,在第一层的链表向后遍历指定个数,就可以找到所有数值。

    平衡树的插入,删除操作引起平衡树的调整,逻辑复杂,跳表只需要改变前后节点。

    从内存占用上来讲跳表的一个节点的平均指针数目为1.33,而平衡树为2。

    算法实现上,跳表也要更简单一些。

    相比而言,跳表要更简单高效,占用空间少。

跳跃表如何进行操作

跳跃表的插入过程:
在这里插入图片描述

跳跃表的查找过程:

在这里插入图片描述

可以看出他的很关键的一个因素:层数。它会从高层开始查找,查找不到,进行下沉,直到找到。

跳跃表的实现

跳跃表节点:
typedef struct zskiplistNode {
    // value
    sds ele;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned long span;
    } level[];
} zskiplistNode;

名词理解

在这里插入图片描述

7在第三层的跨度即为4,前进指针指向37.后退指针指向3(注意前向指针每一层都有,后退指针是每一节点只有一个)。

跳跃表:
typedef struct zskiplist {
    // 跳跃表头指针
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;
} zskiplist
随机层数(理解跳表重要的一个环节):
randomLevel()
level := 1
// random()返回一个[0...1)的随机数
while random() < p and level < MaxLevel do
level := level + 1
return level

p默认为0.25,MaxLevel为32。

这是一个伪代码,可以发现,生成的随机数每次小于P,层数就加一。

节点层数恰好等于1的概率为1-p。

节点层数大于等于2的概率为p,而节点层数恰好等于2的概率为p(1-p)。

节点层数大于等于3的概率为p ^ 2,而节点层数恰好等于3的概率为p ^ 2(1-p)。

节点层数大于等于4的概率为p ^ 3,而节点层数恰好等于4的概率为p ^ 3(1-p)。

层数,使我们可以做到类似在对数组用到的二分查找法。

创建跳跃表
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++) {
        zsl->header->level[j].forward = NULL;
        zsl->header->level[j].span = 0;
    }
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    return zsl;
}
插入节点

插入节点的关键在找到何处插入该节点,跳跃表是按照score分值进行排序的。

讲解源码前,先知道大概原理:

查找步骤:从当前zskiplist保存的最高的level的头节点开始,向前查找,如果当前节点的score小于插入节点的score,继续向前;如果大于,或者则降低一层继续查找,直到第一层为止。此时,插入点就位于找到的节点之后。

在这个过程中会记录在每层要插入节点位置的前一个节点,因为要更新前一节点的跨度,和其前向指针。(update数组)

也会记录每层插入节点的前一节点的总排名,用于更新跨度(跨度不断相加)(rank数组)

所有查找过程,先查找分数,如分数相同,再找对应的value。

这里理解了update和rank后,代码也就很容易理解了,注释很详细。

跳表有点类似二分查找的特性

zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    // updata[]数组记录每一层位于插入节点的前一个节点
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    // rank[]记录每一层位于插入节点的前一个节点的排名
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    serverAssert(!isnan(score));
    x = zsl->header; // 表头节点
    // 从最高层开始查找
    for (i = zsl->level-1; i >= 0; i--) {
        // 存储rank值是为了计算重新构建后的span值
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        // 前向指针不为空,前置指针的分值小于score或等于当前指针的分值但不是所找obj情况下,继续向前查找
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                (x->level[i].forward->score == score &&
                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();
    if (level > zsl->level) {
        // 如果level大于当前存储的最大level值
        // 设定rank数组中大于原level层以上的值为0
        // 同时设定update数组大于原level层以上的数据
        for (i = zsl->level; i < level; i++) {
            rank[i] = 0;
            update[i] = zsl->header;
            //不明白此处为何更新为length的长度
            update[i]->level[i].span = zsl->length;
        }
        // 更新level值
        zsl->level = level;
    }
    // 创建插入节点
    x = zslCreateNode(level,score,obj);
    for (i = 0; i < level; i++) {
        // 针对跳跃表的每一层,改变其forward指针的指向
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;

        // 更新插入节点的span值
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        // 更新插入点的前一个节点的span值
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    // 更新前一节点高于插入节点层的的span值
    for (i = level; i < zsl->level; i++) {
        update[i]->level[i].span++;
    }
    // 设定插入节点的backward指针
    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++;
    return x;
}
删除节点

理解插入后,删除节点就很好理解了

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

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 &&
                     equalStringObjects(x->obj,obj) < 0)))
        {
            x = x->level[i].forward;
        }
        update[i] = x;
    }
    //如果分值等于且就是所要找的obj,就进行删除。
    x = x->level[0].forward;
    if (x && score == x->score && equalStringObjects(x->obj,obj)) {
        zslDeleteNode(zsl, x, update);
        if (!node)
            zslFreeNode(x);
        else
            *node = x;
        return 1;
    }
    return 0; /* not found */
}
  • 8
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值