Redis深度历险-跳表 Redis深度历险-跳表

Redis深度历险-跳表

跳表是一个比较经典的数据结构,非常有用但又不像红黑树那么复杂,非常值得学习;在Ridis中除了提供set这种无序集合,还提供了zset这种有序集合,有序集合就是使用跳表实现的

跳表的意义

跳表是一种结合了二分法和链表特性的数据结构,使用链表的方式提供快速的插入和删除,使用二分法的思想快速定位数据。

链表

链表是最基础的数据结构,其特性有

  • 在任意位置快速的插入和删除
  • 不允许随机访问数据

跳表

对于一个有序链表来说,在进行插入数据时必须从头到尾进行遍历以查找插入的位置,这个时间复杂度时O(n)级别的,对于Redis是不可接受的,而跳表将查找这个时间复杂度降低为O(logn)

跳表是基于链表实现的,链表无法实现二分法的原因就是无法快速定位中间节点;跳表的实现原理就是:

  • 维护一个多层次的链表
  • 上一层链表是下一层的索引链表,原始链表的每两个结点有一个结点在索引链表当中,并保持顺序
  • 在查找时从最上层开始逐层往下查找,每次都能排出底层一半的数据
  • 最终到达底层,底层含有所有数据

跳表的问题

  最理想的状态是每一层的三个节点l, m, r,其中m节点刚好属于下一层l,r的中间,但是在实际当中链表不停的变化这样的实现是非常复杂的,Redis使用的是随机的方式。

Redis中的跳表

结构体

typedef struct zskiplistNode {
    sds ele;                                                            //节点数据
    double score;                                                   //节点权值
    struct zskiplistNode *backward;             //前一个节点
    struct zskiplistLevel { 
        struct zskiplistNode *forward;      //下一个节点
        unsigned long span;                             //跨度,到下一个节点的距离
    } level[];
} zskiplistNode;

Redis中并不是纯粹的链表而是为了实现zset来的,zset存储的是字符串而且为每个字符串存储一个权值;

  • 一个节点可能会存在于多层中
  • level字段中的forward存储来了该节点在每一层的下一个节点
  • level字段中的span存储来该节点到每一层下一个节点的距离
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;        //链表头尾节点
    unsigned long length;                                       //链表总长度
    int level;                                                          //此链表在第几层
} zskiplist;
typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

真正的zset中还使用了一个字典,用来根据字符串查找权值

创建

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

    zsl = zmalloc(sizeof(*zsl));
    zsl->level = 1;
    zsl->length = 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;
    }
    zsl->header->backward = NULL;
    zsl->tail = NULL;
    return zsl;
}

一个跳表最多32层

插入

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

    serverAssert(!isnan(score));
    x = zsl->header;
  
    //按跳表的规则,从上往下进行查找
    for (i = zsl->level-1; i >= 0; i--) {
        rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
        //按score查找,在score一致的情况下会比较字符串
        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;
    }
    //update[i]表示:如果数据在第i层插入那么数据将会插入在update[i]后
    //rank[i]表示:头节点到update[i]的距离
  
    //新插入的数据最高存储在level层中,level是随机的到的
    level = zslRandomLevel();
    //如果插入的数据大于当前高度,更新rank和update以及跳表的高度
    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;
    }
    
    //创建一个层数为score的节点
    x = zslCreateNode(level,score,ele);
  
    //更新[0, level)层的数据,插入到update[i]后
    for (i = 0; i < level; i++) {
        //元素插入到节点update[i]后面
        x->level[i].forward = update[i]->level[i].forward;
        update[i]->level[i].forward = x;
        
        //在第0层存储的是所有数据, 而新元素插入在update[i]后,头节点到新元素的距离是rank[0]+1
        //新元素插入到update[i]后,所以update[i]的跨度变为到新元素的距离,即头节点到新元素(rank[0]+1)的距离减去rank[i]
        
        //新元素的跨度就是到update[i]下一个元素的距离,这就是一个简单的减法了
        x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
        update[i]->level[i].span = (rank[0] - rank[i]) + 1;
    }

    //更新update[i]的跨度数据
    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;
}

这里的跨度问题会导致整个流程变得复杂,不过了解span是什么意思就可以了,不必过分深究实现细节;

重要的是这里使用的zslCreateNode(level,score,ele)随机算法

层数随机算法

每隔节点层数的选择是非常关键的,如果选的不好可能导致跳表的退化

int zslRandomLevel(void) {
    int level = 1;
    //ZSKIPLIST_P等于0.25
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

这里使用了很简单的算法(random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)的概率是1/4,即意味着从下一层到上一层的概率约为1/4,类似于一颗四叉树

范围删除元素

unsigned long zslDeleteRangeByRank(zskiplist *zsl, unsigned int start, unsigned int end, dict *dict) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned long traversed = 0, removed = 0;
    int i;
        
    //从上往下遍历,traversed存储的是头节点到节点x的距离
    //update[i]存储的是第i层距离小于start的元素,再往后的节点从头节点到此节点的距离就大雨start了
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) {
        while (x->level[i].forward && (traversed + x->level[i].span) < start) {
            traversed += x->level[i].span;
            x = x->level[i].forward;
        }
        update[i] = x;
    }
        
   //从x的下一个元素开始删除,直到距离大于end
    traversed++;
    x = x->level[0].forward;
    while (x && traversed <= end) {
        zskiplistNode *next = x->level[0].forward;
        zslDeleteNode(zsl,x,update);
        dictDelete(dict,x->ele);
        zslFreeNode(x);
        removed++;
        traversed++;
        x = next;
    }
    return removed;
}

此函数删除的是整个集合第start到第end个元素,这个时候span字段就显示出作用来了,在删除时不涉及任何层数的调整

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章