跳表(skip list)
增删改查时间复杂度都是 O(logn)
:
- 查询时间复杂度:就是跳表的高度
O(logn)
- 增删改时间复杂度:
先查询到目标节点,然后对其增删改,复杂度=查询+增删改
因为是链表,增删改的时间复杂度为常数级别,所有最终增删改总的时间复杂度就是查询的时间复杂度,也就是O(logn)
空间复杂度:O(n)
使用场景:
适合查询多,增删改少的场景,因为增删改需要修改索引
具体用到跳表的地方:
redis的zset数据结构
查询引擎:ElasticSearch
noSQL数据库HBase 、Google 开源的 key/value 存储引擎 LevelDB
设计原理:
给链表加索引,比如下面的例子中,每两个节点取一个索引
第一层索引:1、4、7、9、13、17、
第二层索引:1、7、13、
查询时就从最上面一层索引开始往下查找,比如查找的是5,比1大接着和7比,比7小则往下。
和4比,比4大比7小接着往下,最后找打5.
跳表索引动态更新:随机函数
当我们不停地往跳表中插入数据时,如果我们不更新索引,就有可能出现2 个索引结点之间数据非常多的情况。
极端情况下,跳表还会退化成单链表。
作为一种动态数据结构,我们需要用随机函数来维护索引与原始链表大小之间的平衡
我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,
比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级索引中。
如果K=0则不需要为其建索引
复杂度公式证明
时间复杂度证明
1、跳表的高度 logn
如果链表有n个节点,每2个节点抽取抽出一个节点作为上一级索引的节点,那第1级索引的节点个数大约是 n/2,第2级索引的节点个数大约是 n/4,依次类推,第k级索引的节点个数就是n/(2^ k)
。假设索引有h级别,最高级的索引有2个节点,则有n/(2^h)=2
,得出h=log2n-1
,包含原始链表这一层,整个跳表的高度就是logn
。
2、查询时间复杂度 O(logn)
假设我们在跳表中查询某个数据的时候,如果每一层都遍历 m 个节点,那在跳表中查询一个数据的时间复杂度就是O(m*logn)
那这个 m 是多少呢?m=3
如下图所示,假设我们要查找的数据是 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)
。
空间复杂度证明
1、计算索引的节点总数
如果链表有n个节点,每2个节点抽取抽出一个节点作为上一级索引的节点,那每一级索引的节点数分别为:n/2,n/4,n/8,…,8,4,2
,等比数列求和 n-1
,所以跳表的空间复杂度为O(n)
。
2、 如何优化空间复杂度
如果链表有n个节点,每3或5个节点抽取抽出一个节点作为上一级索引的节点,那每一级索引的节点数分别为(以3为例):n/3,n/9,n/27,…,27,9,3,1
,等比数列求和 n/2
,所以跳表的空间复杂度为O(n)
,和每2个节点抽取一次相比,时间复杂度要低不少。