维基百科:跳表是一种数据结构。它使得包含n个元素的有序序列的查找和插入操做的平均时间复杂度都是 O(logn),优于数组的 O(n)复杂度。快速的查询效果是经过维护一个多层次的链表实现的,且与前一层(下面一层)链表元素的数量相比,每一层链表中的元素的数量更少。前端
- 优于数组的插入操做时间复杂度
简单理解跳表是基于链表实现的有序列表,跳表经过维护一个多层级的链表实现了快速查询效果将平均时间复杂度降到了O($log^n$),这是一个典型的异空间换时间数据结构。git
在实际开发中常常遇到须要在数据集中查找一个指定数据的场景,而经常使用的支持高效查找算法的实现方式有如下几种:算法
有序数组。插入时能够先对数据排序,查询时能够采用二分查找算法下降查找操做的复杂度。缺点是插入和删除数据时,为了保持元素的有序性,须要进行大量数据的移动操做。数组
二叉查找树。既支持高效的二分查找算法,又能快速的进行插入和删除操做的数据结构,理想的时间复杂度为 O($log^n$),可是在某些极端状况下,二叉查找树有可能变成一个线性链表,即退化成链表结构。数据结构
平衡二叉树。基于二叉查找树的优势,对其缺点进行了优化改进,引入了平衡的概念。为了维持二叉树的平衡衍生出了多种平衡算法,根据平衡算法的不一样具体实现有AVL树 /B树(B-Tree)/ B+树(B+Tree)/红黑树 等等。可是平衡算法的实现大多数比较复杂且较难理解。优化
针对大致量、海量数据集中查找指定数据有更好的解决方案,咱们得评估时间、空间的成本和收益。3d
跳表一样支持对数据进行高效的查找,插入和删除数据操做时间复杂度能与平衡二叉树媲美,最重要的是跳表的实现比平衡二叉树简单几个级别。缺点就是“以空间换时间”方式存在必定数据冗余。指针
若是存储的数据是大对象,跳表冗余的只是指向数据的指针,几乎能够不计使用的内存空间。code
添加、删除操做都须要先查询出操做数据的位置,因此理解了跳表的查询原理后,剩下的只是对链表的操做。对象
例设原始链表上的有序数据为【9,11,14,19,20,24,27】,若是我要查找的数据是20,只能从头结点沿着链表依次比较查找,如图所示:
链表不能像数组那样经过索引快速访问数据,只能沿着指针方向依次访问,因此不能使用二分查找算法快速进行数据查询。可是能够借鉴建立索引的这种思路,就像图书的目录同样,若是我要查看第六章的内容,直接翻到经过目录查询到的第六章对应页码处就行。
这里的目录就至关于建立的索引,该索引可以缩小咱们查询数据的范围减小查询次数。在原始链表的基础上,咱们增长一层索引链表,假如原始链表的每两个结点就有一个结点也在索引链表当中,如图所示:
当创建了索引后检索数据的方式就发生了变化,当咱们想要定位到DataNode-20
,咱们不须要在原始链表中一个一个结点访问,而是首先访问索引链表:
因为索引链表的结点个数是原始链表的一半,查找结点所需的访问次数也就相应减小了一半,通过两次查询咱们便找到DataNode-20
。
正如图书的目录不止按照“章节”划分,还能够按照“第几部分”、“第几小节”进行划分,链表的索引也同样。咱们能够继续为链表建立更多层索引,每层索引节点为前一层索引(对应图例的下一层)的一半,在数据量比较大时可以大大的提高咱们的查询效率。
如图所示,咱们基于原始链表的第1层索引,抽出了第2层更为稀疏的索引,结点数量是第1层索引的一半。这样的多层索引能够进一步提高查询效率,那么它是如何进行查询的呢?假如此次要查找DataNode-27
,让咱们来演示一下检索过程:
HeadIndex-7
开始查找,HeadIndex-7
指向DataNode-7
比DataNode-27
小,因此继续向右查询找到第二个索引节点IndexNode-20
。IndexNode-20
指向DataNode-20
也比DataNode-27
小,可是此时第二层已经没有后续的索引节点,因此咱们须要顺着IndexNode-20
访问下一层索引,即第一层的IndexNode-20
。从索引节点访问方式可知,索引节点保存着“数据节点”、“下层索引节点”的指针。
IndexNode-20
继续向右检索找到IndexNode-27
便检索到了DataNode-27
。总结:
维基百科:
跳跃列表是按层建造的。底层是一个普通的有序链表。每一个更高层都充当下面列表的“快速通道”,这里在第 i 层中的元素按某个固定的几率 $p$(一般为 $\frac 12$ 或 $\frac 14$ 出如今第 i + 1 层中。每一个元素平均出如今 ${1\over 1-p}$ 个列表中,而最高层的元素(一般是在跳跃列表前端的一个特殊的头元素)在 $log_{1/p}^n$个列表中出现。在查找目标元素时,从顶层列表、头元素起步。算法沿着每层链表搜索,直至找到一个大于或等于目标的元素,或者到达当前层列表末尾。若是该元素等于目标元素,则代表该元素已被找到;若是该元素大于目标元素或已到达链表末尾,则退回到当前层的上一个元素,而后转入下一层进行搜索。每层链表中预期的查找步数最多为$\frac 1p$,而层数为 -${log_p^n}\over{p}$,因为 $p$ 是常数,查找操做整体的时间复杂度为 O($log^n$)。而经过选择不一样 $p$ 值,就能够在查找代价和存储代价之间获取平衡。
上面的查询例子中索引节点已是建立好的,那么原始链表哪些数据节点须要建立索引节点、何时建立?这些问题的答案都要回归到往原始链表添加数据时。
从上面的总结不难理解在向原始链表中插入数据时,当前插入的数据按照某个固定的几率$p$($\frac 12$ 或 $\frac 14$)在每层索引链表中建立索引节点。假设如今插入DataNode-18
,咱们来看看是如何插入和建立索引节点的:
首先咱们按照跳表查找结点的方法,找到待插入结点的前置结点(仅小于待插入结点):
接下来按照通常链表的插入方式,把DataNode-18
插入到结点DataNode-14
的后续位置:
这样数据就插入到了原始链表中,可是咱们的插入操做并无结束。按照定义咱们须要让新插入的结点随机(抛硬币的方式)“晋升”,也就是为DataNode-18
建立索引节点,正是采起这种简单的随机方式,跳表也被称为一种随机化的数据结构。
假设第1、第二次随机的结果都是晋升成功,那么咱们须要为DataNode-18
建立索引节点,插入到第一层和第二层索引的对应位置,而且向下指向原始链表的DataNode-18
。
在索引链表中插入新建立的索引节点时须要注意几点:
- 找到待插入索引节点的前置索引节点指向新索引节点,新索引节点指向前置节点以前指向的索引节点。(也就是链表的插入操做)
- 随机的结果是“晋升成功”就能够继续向上一层建立索引,直到假设随机的结果是“晋升失败”或者“新增索引层”。
- 每层是否建立索引节点能够一次性抛几回硬币,而不是添加一层索引后再进行投币。(这样作的目的是为了更好的用代码实现)。
新建的索引节点何如衔接到前置索引节点以及如何用代码实现,这个咱们在下篇文章“SkipList 代码实现”去解析。
若是在第二层(目前索引最大层级)建立索引节点后,下一次随机的结果仍然是晋升成功,这时候该怎么办呢?这个时候咱们就须要添加一层索引层:
能够看到此时第三层只有HeadIndex-7
和IndexNode-18
,此时不会继续向上层建立索引,由于就算继续建立仍只有HeadIndex-7
和IndexNode-18
,这显得毫无心义。至此跳表的插入操做包括索引的建立过程已经解析完,跳表的删除过程正好和插入是相反的思路。
假设咱们要删除刚才插入的DataNode-18
,首先咱们要按照跳表查找结点的方法找到待删除的DataNode-18
,固然若是没有找到对应的数据直接返回进行。
接下来按照链表的删除方式,把DataNode-18
从原始链表当中删除
同插入数据同样,删除工做并无就此完成,咱们须要将DataNode-18
在索引层对应的IndexNode-18
也一 一删除:
同插入索引节点同样,删除索引节点时也须要维护前置节点的指向关系。这里须要特别注意最上层索引(第三层),当删除IndexNode-18
后该层只剩下HeadIndex-7
,这个时候须要将该索引层也一同删除。
至此整个删除操做就算完成了,此时跳表的结构就和咱们以前插入以前保持一致了:
总结
- 简单对比了跳表和其余几种高效查找算法的优缺点。
- 跳表是基于链表实现的,是一种“以空间换时间”的“随机化”数据结构。
- 跳表引入了索引层的概念,有了它才有了时间复杂度为O($logn$)的查询效率,从而实现了增删操做的时间复杂度也是O($logn$)。
- 跳表拥有平衡二叉树相同的查询效率,可是跳表对于树平衡的实现是基于一种随机化的算法的,相对于AVL树/B树(B-Tree)/B+树(B+Tree)/红黑树的实现简单得多。
可耻的贴个我的Git地址:SkipList原理篇