数据结构之跳表
跳表可以说是B+树的一种拓展
1. 什么是跳表
在介绍跳表的定义之前,我们先来了解一下链表,对于一个单链表来说,即便链表中存储的数据是有序的,如果我们要想在七种查找某个数据,只能从头到尾遍历链表,查找的效率很低,查找的时间复杂度为 O(n);
由于单链表的查找效率太慢,我们想,可以通过什么方式能够提高链表的查找效率呢?
我们试着对链表中建立索引
,使用索引来标记和记录数据的位置。如下图
如上图所示,我们给单链表
建立了一个索引层,当我们想查找某个数据(16
)时,我们可以先在索引层遍历,当遍历到索引层中值为13
,我们发现下一个节点是17
,那么我们要查找的节点16
必定在这两个索引之间,此时,我们可以通过down指针
下降到单链表
这一层,继续遍历,我们很快就能查找到我们所需要的节点。
无索引方式需要遍历
10
个结点有索引的方式需要遍历
7
个结点
可以看出,我们通过增加一层索引层的方式,能够有效的减少链表查找数据时,遍历的次数。也就是查找的效率增高了。(空间换时间)
既然我们能够通过添加索引的方式,提高链表查找数据的效率,那么我们是否能够通过建立多层索引的方式,提升更多的查找效率呢?
可以看出,当我们建立二层索引时,查找16
时,需要遍历的次数更少了。遍历6次
当链表的长度n比较大时,在构建索引之后,查找效率的提升就会非常明显。
如果,这种链表加多层索引的结构,就是跳表
2. 跳表查询时间复杂度
跳表中数据查询的时间复杂度为 logn
3. 跳表的空间复杂度
我们分别通过索引点的数量来比较空间的占用量
- 每2个节点抽一个作为索引
通过等比数列计算公式得知:索引的结点综合为 n/2+n/4+n/8…+8+4+2=n-2。所以跳表的空间复杂度为 O(n);
-
每3个节点抽一个作为索引
通过等比数列求和公式,总的索引结点大约就是 n/3+n/9+n/27+…+9+3+1=n/2。尽管
空间复杂度还是 O(n),但比上面的每两个结点抽一个结点的索引构建方法,要减少了一半
的索引结点存储空间
4. 跳表高效的动态插入和删除
跳表数据插入和删除的时间复杂度为 O(logn)
4.1 跳表的插入操作
我们知道,在单链表中,如何我们清楚的知道数据插入的具体位置,单链表的插入复杂度为O(1)。当我们无法确定插入位置时,我们需要先遍历链表(查找操作),找到对应的位置,然后再执行插入操作,时间 复杂度为 O(n)。
同理,我们在跳表中执行插入操作的时间复杂度为 O(logn)。因为我们需要先通过查找(上面我们已经分析过,跳表的查找时间复杂度为O(logn))找到数据插入的位置。
当我们不停的往跳表中插入数据时,如果我们不更新索引,就会出现某两个索引节点间数据特多的情况,极端情况下,出现跳表退化成单链表的情况。(如果链表中结点多了,索引结点就相应地增加一些,避免复杂度退化,随机索引的方式添加索引节点的数量
)
当插入一条数据时,我们通过一个随机函数,来决定将这个结点插入到哪几级索引中,比如随机函数生成了值 K,那我们就将这个结点添加到第一级到第 K 级这 K 级索引中。
4.2 跳表的删除操作
跳表中删除的两种情况:
- 待删除的节点不在索引中
- 待删除的节点在索引中
4.2.1 待删除的节点不在索引中
直接删除
4.2.2 待删除的节点在索引中
当待删除的节点在索引中时,我们除了要删除原始链表中的节点,还需要删除索引中的节点
单链表删除时,我们需要通过遍历拿到删除节点的前驱节点;
双向链表删除时,就不需要考虑遍历的问题了