你们想必都知道,数组和链表的搜索操做的时间复杂度都是O(N)的,在数据量大的时候是很是耗时的。对于数组来讲,咱们能够先排序,而后使用二分搜索,就可以将时间复杂度下降到O(logN),可是有序数组的插入是一个O(N)级别的操做。而链表的插入性能相对优秀,却不能使用二分搜索快速查询。那么是否有一种数据结构,即可以像链表同样快速插入数据,又支持相似于二分搜索这样的查询算法呢?答案是确定的。William Pugh教授在1990发表的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》中提出的跳表就是这样一种有趣的数据结构。java
跳表的核心思想是经过创建索引层来缩短链表的搜索路径,以达到快速搜索的目的。
假设咱们从链表中的每两个节点中提取出一个创建一级索引,而后再从每两个一级索引中提取一个创建二级索引,以此类推,就能够获得以下图所示的结构,其中绿色节点表示索引。 node
在William Pugh的论文中使用了数组加链表的组合来实现跳表,就如上图所示,每一列索引具备相同的key,使用一个数组来表示。还可使用纯链表的形式来实现跳表,我以为这种方式更有助于理解跳表的原理,以下图所示。 git
跳表的搜索须要从高层索引开始向下逐层搜索,每一层的搜索方式和普通链表是同样的,当后继节点的关键字大于搜索关键字时结束本层的搜索,进入下一层继续搜索。下图展现了跳表搜索关键字 22 的过程,其中红色部分就是搜索的路径。 github
跳表的多层索引结构使它的搜索方式很是灵活且强大
好比咱们可能有这样的需求,若是key不存在,咱们须要知道这个key邻近的nearKey是什么,这用跳表很容易实现算法
跳表还能够很容易的搜索一个关键字区间[fromKey, toKey]
,这点和B+树很相似,先搜索fromKey,而后向后遍历链表,取出全部小于等于toKey的数据便可数组
到如今为止,本文描述的都是理想状态下的跳表,事实上,咱们不会严格的为跳表的每m个低级索引创建高级索引,由于这样作复杂并且低效。因此William Pugh在他的论文中采用一种随机算法来为每一个新增的节点随机创建索引,下面是我用Java实现的版本。安全
int randomLevel(int m, int maxLevel) {
ThreadLocalRandom r = ThreadLocalRandom.current();
int level = 1;
while (r.nextInt(m) == 0 && level < maxLevel)
level++;
return level;
}
复制代码
经过这种随机算法,生成第i级索引的几率为
因此可以保证每一层索引的数量都接近于 ,这正好符合咱们前面提到的索引层的性质。
Doug Lea大佬在Java的ConcurrentSkipListMap
中使用了另一种更加炫酷的随机算法的实现方式,使用随机数末尾连续为1的位数做为索引的等级,显然这种方式生成第i级索引的几率为 ,代码以下所示。数据结构
int rn = ThreadLocalRandom.current().nextInt();
// 只有最高位和最低位都为0时,才创建索引,至关于为4个node创建一个索引
if ((rn & 0x80000001) == 0) {
int level = 1;
// 创建索引的等级等于rn末尾连续为1的位数
while (((rn >>>= 1) & 1) != 0)
level++;
}
复制代码
经过随机函数生成一个随机的索引等级以后,建立一个新的索引列,并将每一层的新索引连接到它的前驱索引的后面,若是生成的随机等级大于当前跳表的最大索引等级,须要添加一层新的索引。以下图所示,其中红色虚线箭头表示从新创建的连接。 dom
跳表的删除操做比较简单,先查询删除的关键字,若是在索引层匹配到了关键字,就向下删除全部的索引和数据节点,若是没有匹配到索引,只须要删除数据节点便可。其中有一点须要注意的是,在删除索引后须要检测一下,若是当前层的HEAD索引的后继索引为NIL,则表示这一层已经没有索引了,须要删除这个索引层。以下图所示,红色箭头表示从新创建的连接。 函数
跳表的实现相对AVL树、红黑树等平衡二叉树来讲简单了不少,William Pugh的论文《Skip Lists: A Probabilistic Alternative to Balanced Trees》中提供了使用数组加链表实现跳表的伪代码,我写了一个Java版本的纯链表实现的跳表,并上传到了个人GitHub上,有兴趣的朋友能够看一下。若是你须要在开发中使用跳表的话,java.util.concurrent.ConcurrentSkipListMap
是一个强大的实现,并且它仍是线程安全的。