数据结构---跳表

什么是跳表:

跳表其实是对普通链表的一种优化结构,实质就是一种可以进行二分查找的有序链表。跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。跳表不仅能提高搜索性能,同时也可以提高插入和删除操作的性能。并且Redis中的有序集合(Sorted Set)就是用跳表实现的,zset 的内部实现是一个 hash 字典加一个跳跃列表 (skiplist)。

数据结构:

传统的链表结构在增删的操作上性能十分优异,但是当需要进行查询时就显得有点无力需要通过不断的遍历来找出目标。跳表有点像hash map一样通过一定的变形和复合在牺牲一定程度修改操作性能和空间占用的条件下来换取查询的优化。下面是一个跳表的简单结构图:
在这里插入图片描述
图中只画了四层,最下就是普通的链表结构。往上数分别是一层索引,二层索引以此类推。当我们进行查询时会先从最顶层开始往下找:
在这里插入图片描述
拿上图这个最顶层为一级索引的跳表来看,假设我们当前需要查找16号元素。首先从1-4开始找判断10是否在1-4中间,如果不在则继续从4-7判断10依然不在这个范围内。以此类推,在判断到13-17时确定16在这个区间内。此时从13开始往后遍历最终找到16。总共判断了6次

在这里插入图片描述
当我们添加了二级索引后效率又会提升多少呢,1-7不符合,7-13不符合且13后没有目标。则进入下面的一级索引13处进行判断。13-17符合目标区间,进入原始链表从13开始遍历。下一步直接找到,这次只经过了4次判断就查询到了目标对象。明显比上面只有一级索引的情况更快。

插入随机层数:

在进行插入时会遇到一个问题就是 大量新节点插入到链表后,上层索引节点就会变得不够用。这个时候就需要从新插入的一些节点中选取提拔到上层。但是提谁是个问题,也不能只要插入就向上提那么又会导致上层节点溢出太多,变得更加不均匀。

这个问题已经有大牛替我们解决好了,采取的策略是通过抛硬币来决定新插入结点跨越的层数:每次我们要插入一个结点的时候,就来抛硬币,如果抛出来的是正面,则继续抛,直到出现负面为止,统计这个过程中出现正面的次数,这个次数作为结点跨越的层数。因为跳表添加和删除节点是无法预测的所以无法保证索引部分始终均匀只能用这种方式让其大体趋于均匀。

加入我们在上图中插入11,通过从顶层的不断查找11最终插入到了10的后面。此时就需要通过抛硬币的方式判断11是否需要提到上层索引假设第一次抛出的是正面,则需要继续抛如果第二次抛的为负面此时就终止并将11提到一级索引加到9-13的中间down指向原始链表的11处。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需要更新一下跳跃列表的最大高度。(在redis中的晋升率为25%所以redis的跳跃列表更加的扁平化,层高相对较低,在单个层上需要遍历的节点数量会稍多一点。

删除:

删除操作相对简单一些,只需要在索引层找到要删除的节点,一直向下删除每一个节点即可。比如上图的13从第二层索引开始13删除7指向null,第一层索引13删除9指向17以此类推。如果某一层索引删除后只剩一个节点那么整个层就可以干掉了。

redis中元素排名的实现

/* ZSETs use a specialized version of Skiplists */
typedefstruct zskiplistNode {
    // value
    sds ele;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsignedlong span;
    } level[];
} zskiplistNode;

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

跳跃表本身是有序的,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;
    unsignedlong 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;
        }
    }
    return0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值