选摘自:https://time.geekbang.org/column/article/42896
ps: ZSET是基于【压缩列表】或者【跳跃表+字典】
一、跳表介绍
首先,跳表是一种各方面性能都比较优秀的 动态数据结构,可以支持快速的插入、删除、查找操作,写起来也不复杂,甚至可以替代红黑树(Red-black tree)。
对于一个单链表而言,即便要从链表查找一个数据,也要经过一遍遍历,复杂度为O(n);试想如果对于一个顺序链表,如果有类似索引,比如就像书的章节似的,那么遍历次数可能会减少。对于数据量比较大的存储结构,可能存在多级索引。
这种 链表+多级索引的结构,就是跳表。
下图为正常顺序表:
那怎么来提高查找效率呢?如果像下图那样,对链表建立一级“索引”,查找起来是不是就会更快一些呢?每两个结点提取一个结点到上一级,我们把抽出来的那一级叫作索引或索引层。图中的 down 表示 down 指针,指向下一级结点。
跟前面建立第一级索引的方式相似,我们在第一级索引的基础之上,每两个结点就抽出一个结点到第二级索引。
下图为64 个结点的链表,按照前面讲的这种思路,建立了五级索引。
所以,当链表的长度 n 比较大时,比如 1000、10000 的时候,在构建索引之后,查找效率的提升就会非常明显。
二、用跳表查询到底有多快?
假如每两个节点产生一个索引,每两个一级索引节点产生一个二级索引节点,那么每两个结点会抽出一个结点作为上一级索引的结点,那第一级索引的结点个数大约就是 n/2,第二级索引的结点个数大约就是 n/4,第三级索引的结点个数大约就是 n/8,依次类推,也就是说,第 k 级索引的结点个数是第 k-1 级索引的结点个数的 1/2,那第 k级索引结点的个数就是 n/(2k)。
假设有h层高,最高级的索引有 2 个结点,那么,2^(h+1)=n,即h=log2n-1,即logn级。每层遍历复杂度为m,那么在跳表中查询一个数据的复杂度就为O(m*logn)
假设我们要查找的数据是 x,在第 k 级索引中,我们遍历到 y 结点之后,发现 x 大于 y,小于后面的结点 z,所以我们通过 y 的 down 指针,从第 k 级索引下降到第 k-1 级索引。在第 k-1 级索引中,y 和 z 之间只有 3 个结点(包含 y 和 z),所以,我们在 K-1 级索引中最多只需要遍历 3 个结点,依次类推,每一级索引都最多只需要遍历 3 个结点。
通过上面的分析,我们得到 m=3,所以在跳表中查询任意数据的时间复杂度就是 O(logn)。
这个查找的时间复杂度跟二分查找是一样的。换句话说,我们其实是基于单链表实现了二分查找。前提是建立了很多级索引,空间换时间。