redis跳跃表与二分查找

1,258 阅读5分钟

一 前言

本篇内容主要是讲解redis跳跃表的基础概念,科普一下读者知道有这种随机数据结构的概念,。

公众号:知识追寻者

知识追寻者(Inheriting the spirit of open source, Spreading technology knowledge;)

二 跳跃表

2.1 分查找的思想

说起跳跃表,我们先来回忆一下 二分查找, 这将有助于我们更加容易理解 跳跃表;

一串有序数组如下 , 我们现在想要 以较快的速度查找出该数组的中的125;

1 , 2 , 6, 25 , 32 , 48 , 56 ,73 , 85 , 96, 125 ,135 , 148

首先 125 比中位数56大,向右查找; 剩余如下

.......,73 , 85 , 96, 125 ,135,148

其次 125 比 96 大,向右查找;

...........125 ,135,1148

125 比 135 小, 往左 查找;最终结果125; 故经过4次查找后就找到了本次有序数组中的值; 其时间复杂度未O(logN) ; 如果一个正常的数组进行查找,需要逐个比较,其时间复杂度为 O(N); 明显 二分查找比普通的数组查找快很多;

其java代码实现如下

/**
 * @Author lsc
 * <p> </p>
 */
public class BinaryaFind {

    public static void main(String[] args) {
        BinaryaFind binaryaFind = new BinaryaFind();
        int[] array = {1 , 2 , 6, 25 , 32 , 48 , 56 ,73 , 85 , 96, 125 ,135 , 148};
        int result = binaryaFind.binarySearch(array, 125, 0, array.length - 1);
        // result = 10;  array[result] = 125
        System.out.println(array[result]);
    }

     /* *
     * @Author lsc
     * <p>递归实现二分查找 </p>
     * @Param [array, target, start, end]
     */
    private int binarySearch(int[] array, int target, int start, int end) {
        if (start > end) {
            return -1;
        }
        int mid = start + (end - start) / 2;
        if (array[mid] == target) {
            return mid;
        } else if (target < array[mid]) {
            return binarySearch(array, target, start, mid - 1);
        } else {
            return binarySearch(array, target, mid + 1, end);
        }
    }
}

2.2 跳跃表的概念

跳跃表(skiplist)是一种随机化的数据结构,William Pugh 在论文《Skip lists: a probabilistic alternative to balanced trees》中提出, 跳跃表以有序的方式在层次化的链表中保存元素; 在redis 中的主要应该为zset有序集合的底层实现;

zskiplist结构的定义如下, 其是跳跃表

typedef struct zskiplist {

    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;
    // 表中节点的数量
    unsigned long length;
    // 表中层数最大的节点的层数
    int level;

} zskiplist;

redis.h/zskiplistNode 结构定义如下,其是跳跃表节点

typedef struct zskiplistNode {

    // 后退指针
    struct zskiplistNode *backward;
    // 分值
    double score;
    // 成员对象
    robj *obj;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 跨度
        unsigned int span;
    } level[];

} zskiplistNode;

先不管这段代码你是否读懂,现在我们列出比较重要的概念

  • header:指向跳跃表的表头节点,维护跳跃表节点指针,最高层级为32层
  • tail:指向跳跃表的表尾节点,尾节点全部由null组成
  • level:记录目前跳跃表最大层级;查找时总是由高层往低层级进行查找;
  • length:记录跳跃表的长度
  • zskiplistNode: 节点,保存跳跃表数据信息,前进和后退指针;

其次再看下下图

如果进行查找 member = z , score = 5 , 那么 其查找过程为 从表头 到member = x 的L5 层, spand(跨度) = 1;然后 从 member = x 的 L3 层 到 merber =y 的L3 层; 最后从 merber =y 的L3 层 到 merber =z 的L2层; 跨度代表了2个层级之间的距离,跨度越大,距离越远;

当数据量很大时,通过跳跃表,可以直接通过层级跳跃的方式, 进行查找,有可能 member = x 的 leve l=5,

member = y 的 level =3 ; merber =z 的 level =5 , 此时直接进行查找就只需要通过一次L5 到L5找就可以找到 member = z 的 score; 固总体来说 其查找的时间复杂度为O(logN); heard , 和 tail 直接可以通过表头,表尾定位得到,其时间复杂度为 O(1);

关于插入和删除,也是建立在查找的基础上,固其事件复杂度平均也为平均 O(logN);

2.3 跳跃表API时间复杂度

  • zslCreateNode 创建并返回一个新的跳跃表节点 最坏 O(1)
  • zslFreeNode 释放给定的跳跃表节点 最坏 O(1)
  • zslCreate 创建并初始化一个新的跳跃表 最坏 O(1)
  • zslFree 释放给定的跳跃表 最坏 O(N)
  • zslInsert 将一个包含给定 score 和 member 的新节点添加到跳跃表中 最坏 O(N) 平均 O(logN)
  • zslDeleteNode 删除给定的跳跃表节点 最坏 O(N)
  • zslDelete 删除匹配给定 member 和 score 的元素 最坏 O(N) 平均 O(logN)
  • zslFirstInRange 找到跳跃表中第一个符合给定范围的元素 最坏 O(N) 平均 O(logN)
  • zslLastInRange 找到跳跃表中最后一个符合给定范围的元素 最坏 O(N) 平均 O(logN)
  • zslDeleteRangeByScore 删除 score 值在给定范围内的所有节点 最坏 O(N2)
  • zslDeleteRangeByRank 删除给定排序范围内的所有节点 最坏 O(N2)
  • zslGetRank 返回目标元素在有序集中的排位 最坏 O(N) 平均 O(logN)
  • zslGetElementByRank 根据给定排位,返回该排位上的元素节点 最坏 O(N) 平均 O(logN)

2.4 选择跳跃表的理由

总体来说,其实现方式没有红黑数那么复杂,算法速度较快,平均时间复杂度为O(logN);

三 参考文档

blog.csdn.net/universe_an…

redisbook.readthedocs.io/en/latest/i…

《redis设计与实现》

关注知识追寻者:

tLeP2D.png