3.30--Redis之常用数据结构--跳表之总结篇(总结篇)------加油呀

跳表

跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据

优势是能支持平均 O(logN) 复杂度的节点查找。

只有 Zset 对象的底层实现用到了跳表,zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

这样的好处是既能进行高效的范围查询,也能进行高效单点查询

下图展示了一个层级为 3 的跳表:
3.30--Redis之常用数据结构--跳表之总结篇(总结篇)------加油呀_第1张图片
图中头节点有 L0~L2 三个头指针,分别指向了不同层级的节点,然后每个层级的节点都通过指针连接起来

Zset 对象要同时保存「元素」和「元素的权重」,对应到跳表节点结构里就是 SDS类型的 ele 变量和 double 类型的 score 变量。每个跳表节点都有一个后向指针,指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便

zskiplistLevel 结构体类型的 level 数组:
level 数组中的每一个元素代表跳表的一层,比如 leve[0] 就表示第一层,leve[1] 就表示第二层。zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度用来记录两个节点之间的距离。

跨度实际上是为了计算这个节点在跳表中的排位

计算某个节点排位的时候,从头节点点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位

如图:
3.30--Redis之常用数据结构--跳表之总结篇(总结篇)------加油呀_第2张图片
跳表节点查询过程

查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的 SDS 类型的元素和元素的权重来进行判断,共有两个判断条件:

1.如果当前节点的权重「小于」要查找的权重时,跳表就会访问该层上的下一个节点。
2.如果当前节点的权重「等于」要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。
3.如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的 level 数组里的下一层指针,然后沿着下一层指针继续查找,这就相当于跳到了下一层接着查找。

跳表节点层数设置

跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。

那怎样才能维持相邻两层的节点数量的比例为 2 : 1 呢?

跳表在创建节点的时候,随机生成每个节点的层数

具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数

在第 i 层中的元素按某个固定的概率 p(通常为 ½ 或 ¼ )出现在第 i + 1层中,产生越高的层数,概率越低
元素层数恰好等于 1 的概率为 1 – p
元素层数大于等于 2 的概率为 p,而元素层数恰好等于 2 的概率为 p * (1 – p)
元素层数大于等于 3 的概率为 p^2,而元素层数恰好等于 3 的概率为 p^2 * (1 – p)
元素层数大于等于 4 的概率为 p^3,而元素层数恰好等于 4 的概率为 p^3 * (1 – p)

一个元素的平均层数是 1 / (1 – p)

为什么用跳表而不用平衡树?

1.从内存占用上来比较,跳表比平衡树更灵活一些。平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
2.在做范围查找的时候,跳表比平衡树操作要简单。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
3.从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速

你可能感兴趣的:(redis,数据结构,redis,链表)