一、概述
跳跃表是一个随机化的数据结构,可以看做是二叉树的一个变种,它在性能上和红黑树,AVL树不想上下。目前在Reids和lucene的倒排索引文件中都有应用(后文会介绍跳跃表在这两种情况下的应用)。
二、原理
跳表的原理非常简单,跳表其实就是一种可以进行二分查找的有序链表。
跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了。
为了避免插入操作的时间复杂度是O(N),skiplist每层的数量不会严格按照2:1的比例,而是对每个要插入的元素随机一个层数。
每一次插入一个新结点时,最好的做法就是根据当前表的结构得到一个合适的层数,插入后可以让跳跃表尽量接近理想的结构,但这在实现上会非常困难。Pugh 的论文中提出的方法是根据概率随机为新结点生成一个层数,具体的算法如下:
a.给定一个概率 p(p 小于 1),产生一个 [0,1) 之间的随机数;
b.如果这个随机数小于 p,则层数加 1;
c.重复以上动作,直到随机数大于概率 p(或层数大于程序给定的最大层数限制)。
三、跳表特性
跳跃表是有序集合的底层实现之一;
Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点;
每个跳跃表节点的层高都是1至32之间的随机数;
在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的;
跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序;
跳表是一种实现起来很简单,单层多指针的链表,它查找效率很高,堪比优化过的二叉平衡树,且比平衡树的实现。
四、在redis中的应用
Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点。
zskipList
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int leval;
} zskiplist;
zskiplistNode
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值 权重
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} leval[];
} zskiplistNode;