redis跳跃表与二分查找
一 前言
本篇内容主要是讲解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);
三 参考文档
https://blog.csdn.net/universe_ant/article/details/51134020
https://redisbook.readthedocs.io/en/latest/internal-datastruct/skiplist.html
《redis设计与实现》