源码笔记4.跳跃表skiplist

数据结构

//跳跃表节点
/* ZSETs use a specialized version of Skiplists */
typedef struct zskiplistNode {
    robj *obj; // 成员是redis对象

    double score; // 分数

    struct zskiplistNode *backward; // 后退指针

    struct zskiplistLevel {
        struct zskiplistNode *forward; // 前进指针
        unsigned int span; // 跨度
    } level[]; // 该节点拥有的层级数组 (柔性数组)

} zskiplistNode;
// 跳跃表结构
typedef struct zskiplist {
    struct zskiplistNode *header, *tail; // 表头 表尾

    unsigned long length; // 总长度(节点数)

    int level; // 层数 (最大的层数)
} zskiplist;
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */
#define ZSKIPLIST_P 0.25      /* Skiplist P = 1/4 */

可以先看下有序集合的结构,其中就包含跳跃表的实现。后面再单独分析有序集合zset。

// 有序集合
typedef struct zset {

    // 字典 key是成员 value是分值
    dict *dict;

    // 跳跃表 按分值排序的节点
    zskiplist *zsl;
} zset;

先根据个人理解,画一张跳跃表的图,为了更好的理解下面的内容。
在这里插入图片描述
跳跃表除去“跳跃”的特性本身是个链表,“跳跃”由level这个层级化的信息中的指针带来的。
程序设计上,总是从高层级开始访问,相当于大范围的查找,随着查找范围缩小,慢慢的降低层级,有一种跳着查的感觉。

方法分析

zslCreateNode创建跳跃表节点

// 层数level 分数score 对象是obj
zskiplistNode *zslCreateNode(int level, double score, robj *obj) {
    // 同SDS一样,跳跃表节点分配空间时,要注意最后柔性数组的空间分配
    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel));

    zn->score = score;
    zn->obj = obj;

    return zn;
}

zslCreate创建一个跳跃表

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

    zsl = zmalloc(sizeof(*zsl)); //分配空间

    zsl->level = 1; // 设置层级为1
    zsl->length = 0; // 设置总长度为0

    // 设置一个头结点
    // 头结点层级最大 score为0 对象为空
    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;
    }
    // 头结点的后退指针NULL
    zsl->header->backward = NULL;

    // 尾结点
    zsl->tail = NULL;

    return zsl;
}

zslRandomLevel随机获取一个level,后面再细讲。
zslInsert插入redis对象

zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level;

    redisAssert(!isnan(score));

    x = zsl->header;
    // 从当前的最高层级往下遍历
    for (i = zsl->level-1; i >= 0; i--) {
    	// 理解rank先理解span,span是前一个节点到后一个节点的跨度
    	// 因为每个节点level高度不同,所以span跨度可能会是1,也可能一下子跨过多个节点
    	
		// 初始化rank[最高]=0
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];

        // 沿着前进指针遍历跳跃表
        while (x->level[i].forward &&
            (x->level[i].forward->score < score ||
                // 比较score,相同再比较obj
                (x->level[i].forward->score == score &&
                compareStringObjects(x->level[i].forward->obj,obj) < 0))) {
			// 累加前一个节点的span。最终得到从header到要插入的点的总距离。
            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; // 这部分相连的肯定是header
            update[i]->level[i].span = zsl->length; // 这部分header的跨越是整个跳跃表长度
        }

        // 更新表中节点最大层数
        zsl->level = level;
    }

    // 创建新节点
    x = zslCreateNode(level,score,obj);

    // 将前面记录的指针指向新节点,并做相应的设置
    for (i = 0; i < level; i++) {
        // 新节点的forward指针指向相连节点的forward
        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]);
        // 更新新节点插入之后,沿途节点的 span 值
        // 其中的 +1 计算的是新节点
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    // 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点
    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;
}

zslDeleteNode删除指定节点
zslDelete删除redis对象(先查找,再调用zslDeleteNode)
zslFreeNode释放指定的跳跃表节点
zslFree释放指定的跳跃表

// 其中遍历 所有节点的代码如下
// 因为对象都在第一层,只要一直遍历前进指针即可
    while(node) {
        next = node->level[0].forward;
        zslFreeNode(node);
        node = next;
    }

跳跃表梳理

先看一个简单的有序链表:
在这里插入图片描述
在这样一个链表中,如果要查找某个数据,就需要从头开始逐个比较,直到找到指定的节点。时间复杂度是O(N)。

在这里插入图片描述
现在假设每两个节点,增加一个指针。在这样一个链表中,如果要查找某个数据,同样从头开始逐个比较,但是它一次会跳过两个节点,相比上一个链表,整体时间复杂度变成了O(N/2)。
在这里插入图片描述
按照这个思路,每三个节点再增加一个指针,让跳跃距离更大,这样就拥有了更快的查找能力。可以想象,当链表的长度比较大的时候,这样多个层次的链表进行“跳跃式”的查找,能跳过很多下层的节点,大大提高查找速度。

记下来思考,我们该怎么去构建这个跳跃链表,真的按照上面的想法,每几个节点一个循环这样有序的高度变化吗?
试想一下如果这么做,新插入一个节点,该怎么维持原先的循环有序呢,需要调整新节点以及后面所有的节点。删除一个节点同理。那么维护这样一个链表,就变的更加麻烦。实际上,Redis为了避免这个问题,采取的随机高度的做法,每个节点的高度都是随机性的,在整体上实现每个层次的数量级分化。

上面在读Redis的跳跃表时,有一个函数是zslRandomLevel。具体代码如下:

// ZSKIPLIST_MAXLEVEL=32
// ZSKIPLIST_P=0.25
int zslRandomLevel(void) {
    int level = 1;

    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;

    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

这个函数,在每个新增节点时都会调用,也就是每个节点都会随机出一个level高度。
怎么理解这件事。
函数中有一个循环,条件是随机数小于1/4,高度就增加1。从宏观角度上来看,假设有1000个节点,那么就大概会有250个节点高度大于1,62个节点高度大于2,16个节点高度大于3… 大致就是每一层之上的节点数大约是这一层节点数的
1/4,这样整体保证了这个跳跃表不会过于臃肿也不会失去跳跃性。
Redis取1/4,这个数字不是固定的,在自己的设计中,也可以更改,是一个弹性设计。
在这里插入图片描述
上图是一个Redis中可能生成的跳跃表示例,现在来查找9,需要的步骤是1->4->6->9,只需要4次就找到对应目标,查找示例如下在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值