跳跃表
当一个有序集合包含的元素数量多,又或者有序集合中的元素的成员是比较长的字符串时,Redis就会使用跳跃表作为有序集合键的底层实现。
简介
如果我们要实现按照成绩对学生进行排名,可以选择数组、链表、平衡树或红黑树来实现,数组的插入和删除效率低,链表查询的效率低,平衡树或红黑树虽然效率高但是实现复杂。跳跃表(后面简称跳表)就是一种改造了链表,有点像“二分”查找的结构。
下图是一个有序链表:
如果将有序链表的部分节点分层,每层都是一个有序链表。查找时优先从最高层开始向后查找,当到达某节点时,如果next节点值大于要查找的值或next指针指向NULL,则从当前节点下降一层继续向后找,这种“跳过一些节点”的方式就是跳表查找的思路。如下图所示是一个有序分层链
- 除第0层外的其他层所有节点均有一个down指针指向下层节点
- 第0层包含了所有数据
- 其他层的每个节点一定都是从其下一层中选出来的
这种链表加多级索引的结构就是跳表。
查找过程
上图中,如果要查找值为51的节点,步骤如下:
(1)从最高层即第2层开始,1节点比51小,继续向后找
(2)21节点比51小,且21节点的next指针为null,跳到第1层查找
(3)41节点比51节点小,继续向后查找
(4)61节点比51节点大,跳到第1层查找
(5)51节点等于51,找到返回
跳表的思想就是:将有序集合的部分节点分层,由最上层一次向后查找,如果本层的next节点大于要查找的节点,或next值为null,则向下一层查找,直到找到或返回空。
Redis有序集合中的跳表
下图就是一个Redis有序集合的跳表结构:
跳表节点的结构体定义如下:
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
- ele:存储字符串类型的数据,如学生姓名
- score:用于存储排序的分值,如学生成绩
- backward:指向当前节点的前一个节点,头结点的backward值为null,用于从后往前遍历跳跃表
- level:柔性数组,forward指向该节点对应的下一层节点
- span:forward指向的节点与本节点之间的元素个数,span值越大,跳过的节点数越多
跳表结构:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
- header:指向跳表的头节点,头结点的level为64层,是Redis跳表的最大层数
- tail:指向跳表的尾节点
- length:存储跳表节点个数,不包括头结点
- level:跳跃表的高度,即最大层数,不包括头结点
跳表的插入
【注:图片来自 https://blog.csdn.net/universsky2015/article/details/102728114】
上图展示了跳表插入的一个插入过程,跳表的插入过程也需要像平衡树一样维护“平衡性”,如果所有插入的数据都在11到37之间,则跳表的查询时间复杂度很快就会退化成在单链表上的查询时间复杂度了。在Redis有序集合中维护跳表“平衡性”采用的是产生一个随机数k,将要添加的节点添加至第1层至第k层,上图元素区域被拉长就是表明该元素同时也插入到了其他高层上。即zskiplistNode的level层数是随机产生的一个值。
时间复杂度分析
如果链表里有n个结点,会有多少级索引呢?
假设平均每隔两个结点会抽出一个结点作为上一级索引的结点,那第一级索引的结点个数大约就是n/2,第二级索引的结点个数大约就是n/4,第三级索引的结点个数大约就是n/8,依次类推,也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^ k)。
假设索引有h级,最高级的索引有2个结点,则n/(2^ h)=2得到h=log2n-1,所以包含原始数据这一层,跳表的高度就是log2n。假设每层都要遍历m个节点,则在跳表中查找一个数据的时间复杂度就是O(m*logn)。那m的值是多少呢?
按照上面的说法“假设平均每隔两个结点会抽出一个结点作为上一级索引的结点”,可以得到下面这张图:
假设我们要查找的数据是x。在第k级索引中遍历到y结点之后,发现x大于y,小于后面的结点z,所以我们通过y的down指针,从第k级索引下降到第k-1级索引。在第k-1级索引中,y和z之间只有3个结点(包含y和z),所以,我们在K-1级索引中最多只需要遍历3个结点,依次类推,每一级索引都最多只需要遍历3个结点。
因此,跳表查找的平均时间复杂度就是O(logn),和二分查找的平均时间复杂度一样。
空间复杂度分析
按照分析时间复杂度的假设: “假设平均每隔两个结点会抽出一个结点作为上一级索引的结点”,原始数据有n个,则第1层索引有n/2个节点、第2层索引有n/4个节点…、第k层有n/(2^ k)个节点,依次累加,得到n-2。所以跳表的空间复杂度是O(n),即n个数据用跳表的结构存储,还需要额外n个节点的存储空间。