查找算法-跳跃表(SkipList)入门及复杂度分析

什么是跳表

对于单链表这种数据结构,如果我们想查找一个结点需要从头到尾遍历所有结点,时间复杂度为O(n)效率非常低效。

如果我们想办法在单链表的一些结点上加索引呢?如图

图中原始链表每隔2个节点就抽出一个结点作为索引,抽出的这一层我们称之为“索引层”,第一层索引层我们称之为第一层索引。

就比如我们想此时要查找10这个节点,我们需要遍历6个节点就可以找到。

如果再抽取一个索引层,效率会更高,我这里虽然还是遍历了6个节点,不过这是因为例子中的单链表数据量小看不出来。

这种链表+多级索引的结构就是跳表


跳表查询的时间复杂度

假如原始单链表节点数为N,按照之前每两个节点抽取一个索引节点,则第一层索引结点个数约为N/2,第二层索引的节点个数约为N/4,第三层的索引个数为N/8,依次类推,第K层索引的节点个数是第K-1层索引节点个数的1/2,那么第K层索引的节点个数为N/2^k.假设索引层有h层,最高层索引有2个节点,则可以得到N/2^h=2,则h=logN-1,如果包含原始链表层,整个跳表高度是logN.

如果在跳表中每一层索引需要遍历m个节点,则在跳表中查询一个数据的时间复杂度为O(m*logN).

那么m的值是多少?按照每两个节点抽取一个索引节点的逻辑,每一层最多需要遍历3个节点,所以m=3.则跳表的时间复杂度为O(logN).效率的确是很高的。

那么为什么m就等于3呢?

结合下图应该可以看出来,查找10,需要确定9,而确定9需要在第一层索引确定位置,确定9需要确定前驱节点7的位置,确定7则需要在第二层索引确定7的位置,而确定这些索引节点最多每层遍历3个元素。

也就是这些多级索引把一个单链表分成了一个个的区间段,每次索引层的查找都确定了一个小的区间段,于是就提高了效率,但是却浪费了建立索引需要的空间,这就是空间换时间的思路。

跳表的空间复杂度分析

假设单链表的长度为N,第一级索引大概N/2个节点,第二级索引大概N/4个节点,第三级索引大概N/4个节点。依次类推直到剩下2个索引节点。于是组成了一个等比数列:N/2、N/4、N/8、.........8、4、2,这些级的索引加起来等于n-2,所以跳表的空间复杂度O(N),即长度为N单链表在每隔两个节点抽取一个索引节点时,组成的跳表需要额外N个节点来存储索引。

如何降低空间复杂度?很容易想到就是每隔三个或者五个再抽取出一个索引节点,这样的索引节点就会大大减少。

如果每隔三个节点抽取一个索引节点,那么第一层索引节点数大概为N/3,第二层索引的节点数大概为N/9,依次类推假设最后一层索引节点数为1,则组成的等比数列是N/3、N/9、N/27、.......9、3、1.加起来的和为N/2,空间占用差不多减少了一半的存储。

实际上索引占用空间相比原始单链表本身节点小巫见大巫了,毕竟一半节点中存储的是对象,而索引中只是id什么的。占不了多少。

高效的动态插入和删除

跳表不仅支持查找,而且支持动态插入和删除,而且动态插入和删除的时间复杂度也是O(logN).

单链表在确定操作位置时,进行插入和删除的时间复杂度都为O(1),所以通过跳表在O(logN)内确定操作位置后,对原始链表的插入和删除时间复杂度就是O(1),总体时间复杂度为O(logN).

如图是一个插入数据6的过程。

在删除节点的时候,跟插入略有不同,就是如果删除的节点出现在索引中,需要一并删除索引。

跳表增删查操作时间复杂度退化问题

在不停往跳表中插入数据而不更新跳表索引的情况下,很容易造成跳表某个索引区间内数据过多,造成跳表退化成单链表的情况。

所以我们应当在插入时维护原始链表和索引之间的平衡关系,比如可以使用一个随机函数决定插入的数据作为索引插入到哪些索引层中。如图

Redis为何使用跳表实现Zset

首先是跳表支持动态插入和删除以及查询的时间复杂度都是O(logN)表现的很高效,

其次则是跳表的代码实现相对如红黑树来说是比较容易的,虽然跳表的实现也不简单。

其实我这里跳表的代码和红黑树的代码实现都没有去研究,只是介绍了跳表做一个简单的入门。

如果有不明白的可以一起交流,欢迎留言~

发布了222 篇原创文章 · 获赞 805 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/shengqianfeng/article/details/100122195