1.初步认识跳跃表

图中所示,跳跃表与普通链表的区别在于,每一个节点可以有多个后置节点,图中是一个4层的跳跃表

第0层: head->3->6->7->9->12->17->19->21->25->26->tail
第1层: head->6->9->17->25->tail
第2层: head->6->25->tail
第3层: head->6->tail

传统意义的单链表是一个线性结构,向有序的链表中插入一个节点需要O(n)的时间,查找操作需要O(n)的时间。如果我们使用图中所示的跳跃表,就可以减少查找所需时间为O(n/2),因为我们可以先通过每个节点的最上面的指针先进行查找,这样子就能跳过一半的节点。比如我们想查找19,首先和6比较,大于6之后,在和9进行比较,然后在和12进行比较......最后比较到21的时候,发现21大于19,说明查找的点在17和21之间,从这个过程中,我们可以看出,查找的时候跳过了3、7、12等点,因此查找的复杂度为O(n/2)。

2.redis中实现的skiplist

  • 结构体 zskiplist

    typedef struct zskiplist {
    
      // 表头节点和表尾节点
    struct zskiplistNode *header, *tail; // 表中节点的数量
    unsigned long length; // 表中层数最大的节点的层数
    int level; } zskiplist;
    // 节点
    typedef struct zskiplistNode { // 成员对象
    robj *obj; // 分值
    double score; // 后退指针
    struct zskiplistNode *backward; // 前一个节点 // 层
    struct zskiplistLevel { // 前进指针
    struct zskiplistNode *forward; // 下一个节点 // 跨度
    unsigned int span; // 当前节点在第i层到下一个节点forward需要跨过的节点数 } level[]; } zskiplistNode;

redis实现的跳跃表特点:

1.zskiplistNode中保存着前置节点backward
2.跳跃表的层数最大值32,每次插入新节点都会生成一个随机的level(1~32)作为新节点的层数
3.删除节点可能会引起跳跃表层数的下降,插入节点可能会引起跳跃表层数上升
4.查找节点的时间复杂度平均为 O(logn)
5.插入和删除的成本都比较低,拥有平衡二叉树的查找性能
  • 创建一条skiplist

       // 创建一条长度为0的skiplist
    zskiplist *zslCreate(void) {
    int j;
    zskiplist *zsl; // 分配空间
    zsl = zmalloc(sizeof(*zsl)); zsl->level = 1; // 起始层数
    zsl->length = 0; // 跳跃表长度 // 初始化表头节点
    // T = O(1)
    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;
    } // 创建新节点
    zskiplistNode *zslCreateNode(int level, double score, robj *obj) { // 分配空间
    zskiplistNode *zn = zmalloc(sizeof(*zn)+level*sizeof(struct zskiplistLevel)); // 设置属性
    zn->score = score;
    zn->obj = obj; return zn;
    }
  • 插入一个节点

     zskiplistNode *zslInsert(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    unsigned int rank[ZSKIPLIST_MAXLEVEL];
    int i, level; redisAssert(!isnan(score)); // 保证score合法性 // level越高每一次forward跨越的节点越多,先大间距的查找,随着level的减小,查找范围逐渐缩小
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) { // rank[i]用来记录当前节点x与header的距离,随着x的移动,rank[i]实时更新
    rank[i] = i == (zsl->level-1) ? 0 : rank[i+1]; // 沿着前进指针遍历跳跃表
    // T_wrost = O(N^2), T_avg = O(N log N)
    while (x->level[i].forward &&
    (x->level[i].forward->score < score ||
    // 比对分值
    (x->level[i].forward->score == score &&
    // 比对成员, T = O(N)
    compareStringObjects(x->level[i].forward->obj,obj) < 0))) { // 记录沿途跨越了多少个节点
    rank[i] += x->level[i].span; // 移动至下一指针
    x = x->level[i].forward;
    }
    // 第i层第一个大于 score的节点,将作为插入节点obj在第i层连接的的前一个节点
    update[i] = x;
    } /* we assume the key is not already inside, since we allow duplicated
    * scores, and the re-insertion of score and redis object should never
    * happen since the caller of zslInsert() should test in the hash table
    * if the element is already inside or not.
    *
    * zslInsert() 的调用者会确保同分值且同成员的元素不会出现,
    * 所以这里不需要进一步进行检查,可以直接创建新元素。
    */ // 获取一个随机值作为新节点的层数
    // T = O(N)
    level = zslRandomLevel(); // 如果新节点的层数比表中其他节点的层数都要大
    // 那么初始化表头节点中未使用的层,并将它们记录到 update 数组中
    // 将来也指向新节点
    if (level > zsl->level) { // 初始化未使用层
    // T = O(1)
    for (i = zsl->level; i < level; i++) {
    rank[i] = 0;
    update[i] = zsl->header;
    update[i]->level[i].span = zsl->length;
    } // 更新表中节点最大层数
    zsl->level = level;
    } // 创建新节点
    x = zslCreateNode(level,score,obj); // 将前面记录的指针指向新节点,并做相应的设置
    // update[i]保存着第i层x的前置节点,rank[i]保存的是第i层x的前置节点离header的距离,rank[0]+1即是x离header的距离
    for (i = 0; i < level; i++) { // 设置新节点的 forward 指针
    x->level[i].forward = update[i]->level[i].forward; // 将沿途记录的各个节点的 forward 指针指向新节点
    update[i]->level[i].forward = x; /* update span covered by update[i] as x is inserted here */
    // 用x前置节点到x后置节点的跨度减去x到前置节点的距离等于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; // (rank[0] - rank[i]) 为x距离update[i]的距离
    } /* increment span for untouched levels */
    // 未接触的节点的 span 值也需要增一,因为这些节点到后置节点中间插入了一个节点x
    // T = O(1)
    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; // x是跳跃表的尾部节点 // 跳跃表的节点计数增一
    zsl->length++; return x;
    }
  • 删除一个节点

    int zslDelete(zskiplist *zsl, double score, robj *obj) {
    zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
    int i; // 遍历跳跃表,查找目标节点,并记录所有沿途节点
    // T_wrost = O(N^2), T_avg = O(N log N)
    x = zsl->header;
    for (i = zsl->level-1; i >= 0; i--) { // 遍历跳跃表的复杂度为 T_wrost = O(N), T_avg = O(log N)
    while (x->level[i].forward &&
    (x->level[i].forward->score < score ||
    // 比对分值
    (x->level[i].forward->score == score &&
    // 比对对象,T = O(N)
    compareStringObjects(x->level[i].forward->obj,obj) < 0))) // 沿着前进指针移动
    x = x->level[i].forward; // 第i层上obj的前一个节点
    update[i] = x;
    } /* We may have multiple elements with the same score, what we need
    * is to find the element with both the right score and object.
    *
    * 检查找到的元素 x ,只有在它的分值和对象都相同时,才将它删除。
    */
    x = x->level[0].forward; // 指向目标节点
    if (x && score == x->score && equalStringObjects(x->obj,obj)) { // 目标节点与obj一样
    // T = O(1)
    zslDeleteNode(zsl, x, update); // 已知目标节点每一层的前置节点,删除目标节点
    // T = O(1)
    zslFreeNode(x); // 释放目标节点内存
    return 1;
    } else { // 目标节点与obj不匹配
    return 0; /* not found */
    } return 0; /* not found */
    } // update数组存储着要删除的节点x的前置节点
    void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
    int i; // 更新所有和被删除节点 x 有关的节点的指针,解除它们之间的关系
    // T = O(1)
    for (i = 0; i < zsl->level; i++) {
    if (update[i]->level[i].forward == x) { // update[i]是第i层在x前面的节点而且是前置节点
    update[i]->level[i].span += x->level[i].span - 1; // 更新前置节点的span
    update[i]->level[i].forward = x->level[i].forward; // 更新前置节点的forward
    } else { // update[i]是第i层在x前面的节点,没有和x建立连接
    update[i]->level[i].span -= 1; // 减去中间少的1个
    }
    } // 更新被删除节点 x 的前进和后退指针
    if (x->level[0].forward) {
    x->level[0].forward->backward = x->backward;
    } else { // x是尾部节点
    zsl->tail = x->backward;
    } // 更新跳跃表最大层数(只在被删除节点是跳跃表中最高的节点时才执行)
    // T = O(1)
    while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
    zsl->level--; // 跳跃表节点计数器减一
    zsl->length--;
    }

redis源码学习-skiplist的更多相关文章

  1. Redis源码学习:字符串

    Redis源码学习:字符串 1.初识SDS 1.1 SDS定义 Redis定义了一个叫做sdshdr(SDS or simple dynamic string)的数据结构.SDS不仅用于 保存字符串, ...

  2. Redis源码学习:Lua脚本

    Redis源码学习:Lua脚本 1.Sublime Text配置 我是在Win7下,用Sublime Text + Cygwin开发的,配置方法请参考<Sublime Text 3下C/C++开 ...

  3. redis源码学习之slowlog

    目录 背景 环境说明 redis执行命令流程 记录slowlog源码分析 制造一条slowlog slowlog分析 1.slowlog如何开启 2.slowlog数量限制 3.slowlog中的耗时 ...

  4. 柔性数组(Redis源码学习)

    柔性数组(Redis源码学习) 1. 问题背景 在阅读Redis源码中的字符串有如下结构,在sizeof(struct sdshdr)得到结果为8,在后续内存申请和计算中也用到.其实在工作中有遇到过这 ...

  5. __sync_fetch_and_add函数(Redis源码学习)

    __sync_fetch_and_add函数(Redis源码学习) 在学习redis-3.0源码中的sds文件时,看到里面有如下的C代码,之前从未接触过,所以为了全面学习redis源码,追根溯源,学习 ...

  6. redis源码学习之工作流程初探

    目录 背景 环境准备 下载redis源码 下载Visual Studio Visual Studio打开redis源码 启动过程分析 调用关系图 事件循环分析 工作模型 代码分析 动画演示 网络模块 ...

  7. redis源码学习之lua执行原理

    聊聊redis执行lua原理 从一次面试场景说起   "看你简历上写的精通redis" "额,还可以啦" "那你说说redis执行lua脚本的原理&q ...

  8. Redis源码学习-Master&Slave的命令交互

    0. 写在前面 Version Redis2.2.2 Redis中可以支持主从结构,本文主要从master和slave的心跳机制出发(PING),分析redis的命令行交互. 在Redis中,serv ...

  9. Redis源码学习1-sds.c

    https://github.com/huangz1990/redis-3.0-annotated/blob/unstable/src/sds.c#L120 /* SDSLib, A C dynami ...

随机推荐

  1. Write Markdown Syntax Online Document with Sphinx and Pandoc

    There is no doubt that we have to write doc while we are developing software. But How do you write d ...

  2. web-day12

    第12章WEB12-JSP&EL&JSTL篇 今日任务 商品信息的显示 教学导航 教学目标 掌握JSP的基本的使用 掌握EL的表达式的用法 掌握JSTL的常用标签的使用 教学方法 案例 ...

  3. 牛客网2018暑期训练 第三场 a题

    #include <bits/stdc++.h> using namespace std; vector<int> path; ; short dp[maxn][maxn][m ...

  4. codeforces 925 c big secret

    题意: 给你n个数,b[1],b[2],b[3].......,让你重新排列,使a[i]的值递增 a[i]和b的关系: a[i] = b[1]^b[2]^b[3]^....^b[i]; 首先说异或   ...

  5. 12.DataGrid的columns的特性

  6. node-lessons

    教程:https://github.com/alsotang/node-lessons 0 nvm 的全称是 Node Version Manager,之所以需要这个工具,是因为 Node.js 的各 ...

  7. Linux vi 文本代码时显示行号或不显示行号

    Linux vi 文本代码时显示行号或不显示行号 前提  安装了vim $vi ~/.vimrc 显示的话加上 set nu 不想显示的话可以注释掉 "set nu 之后 $source ~ ...

  8. 1.虚拟机中安装ubuntu

    1.VMware安装很简单,全部默认安装即可. 2.安装完VMware之后,打开VMware,点击创建虚拟机 典型安装易出问题,所以这里选择自定义安装 安装过程选项配置如下 处理器数,核数,内存都可以 ...

  9. Android-Java-封装

    先看一个未封装的Demo案例一: package android.java.oop03; class Person { int age; } public class PottingDemo { pu ...

  10. maven-java包管理工具-01

    maven只用来管理java项目,也是用java开发的 传统的项目因为包的管理有很多问题,所以才有的maven的诞生: 1. 项目开始的时候,确定项目中可能要使用到的包,然后下载包,复制粘贴到项目里面 ...