大家如果觉得文章有帮助的话,请帮忙分享跟按赞,谢谢你们!
我们都知道链表的查询速度为 O(n),那是因为我们必须从头开始一个接一个节点往下查询。但是如果我们的节点是有序的,能否利用这有序的性质来提升我们的查询速度呢?
思考:单向链表能不能使用二分查找法?
如果一个集合里的元素是有序的,那么我们可以利用数组的随机访问 (Random Access, 即通过下标 index 来访问元素) 性质,或者我们可以通过二叉树的二分结构性质来使用二分查找法。
那么我们可以在单向链表中使用二分查找法吗?
答案是不能。
因为在一般的链表当中,我们没有办法直接访问到中间的元素,因此无法使用二分查找法。然而,我们可以通过改变链表的结构,给链表加上不同的层次来达到二分查找的效果。这种改变结构的加强版链表,我们称之为跳跃链表 (Skip List)。
跳跃链表 (Skip List)
从图中可以看到,跳跃链表中的元素是有序的,并且上层的元素分布比较稀疏,越往下层则元素数量越多越密集,最底下一层拥有全部元素。
当我们在查找元素的时候,先从最上面第一层开始查找。由于上层比下层稀疏,我们可以“跳跃”大部分的元素,如果当前这层没有我们要找的目标元素,则到下一层去寻找,每一层都可以帮我们跳跃掉部分元素,节省查找时间。
插入跳跃链表
那么我们具体该如何来建造这个跳跃链表呢?
当我们插入新元素的时候,可以用概率来决定是否往当前层次插入元素。根据计算,当概率为0.5的时候,跳跃链表的搜索效率最高,后面我们会详细讨论。因此,我们可以用投掷硬币的方式来模拟0.5概率。
需要注意的是,所有元素都必须百分之百存在于最底层次作为保底。
// 如果硬币为正面,则往当前层次插入新元素,接着向上层移动,不停重复,
// 直到硬币为反面为止。
// 任何元素必须至少存在于最底层
create(element);
moveUp();
coin = tossCoin();
// 当硬币为正面时,创建元素,向上层移动,再次投掷硬币。
while (coin.face == “head”) {
create(element);
moveUp();
coin = tossCoin();
}
搜索跳跃链表
我们可以通过比较目标值与下一个节点的值,如果:
-
目标值 > 下个节点值
表示在当前节点与下个节点之间不包含目标值,因此向右移动 -
目标值 < 下个节点值
表示当前节点与下个节点之间可能包含了目标值,因此向下移动
if (targt > next.val) {
// 目标值大于下一个节点值,向右移动
current = next;
} else if (target < next.val) {
// 目标值小于下一个节点值,向下移动
current = current.down;
} else {
// 找到目标值
return true;
}
搜索效率分析
跳跃链表的搜索效率是:O(log n)
跳跃链表的搜索效率取决于我们如何插入新的元素。
由于我们总是从最底下一层开始投掷硬币,并且只有在硬币为正面的时候我们才会向上层移动,如果硬币为反面,则该元素的插入操作结束。因此,我们可以得出该元素在每一个层次里出现的概率表:
层次 | 概率公式 | 概率 | 预期元素数量 |
---|---|---|---|
第一层 | 1 | 1 | n |
第二层 | 0.5 | 0.5 | n / 2 |
第三层 | 0.5 * 0.5 | 0.25 | n / 4 |
第四层 | 0.5 * 0.5 * 0.5 | 0.125 | n / 8 |
… | … | … | … |
第 log n 层 | 0.5^(log n - 1) | 0.5^(log n - 1) | ~= 1 |
直观一点的说法就是,每往上一层,我们预期会排除掉一半的元素,因为上一层生存下来的元素,在这一层里只有一半的几率可以继续存活。
看到这里,是不是觉得这和我们的二分搜索算法很相像?二分搜索算法也是每一次都会排除一半的元素。
由于每往上移动一层,我们都预期排除掉一半的元素,因此跳跃链表的高度预期是 log n,n 代表元素总数量。并且我们预期每层最多只会遍历两个元素便需要向上移动或找到目标,于是我们得出搜索效率为 O (log n)。
为什么每一层预期只会最多遍历两个元素呢?推导公式如下:
设 C(j) 为向上走 j 个层次的预期步数
则
// 我们有 0.5 几率向上,也有 0.5 几率不向上走,取决于我们如何插入新元素
// 此处的 1 代表的是当前停留的元素,每一层至少要比较一个元素
C(j) = 1 + 0.5*C(j) + 0.5*C(j-1)
重新整理公式得到
C(j) - 0.5*C(j) = 1 + 0.5*C(j-1)
0.5*C(j) = 1 + 0.5*C(j-1)
C(j) = 2 + C(j-1)
因此,第 j 层最多遍历两个元素,剩下的步数属于 C(j-1) 层
删除跳跃链表元素
删除元素就比较简单了,定位到元素之后,像一般的单向链表一样,删除该节点,接着向下方移动,继续删除节点,直到我们删掉最底层的节点为止。
跳跃链表优缺点
跳跃链表的优点在于搜索效率的提升,我们可以将搜索效率提升到 O (log n),跟数组与二叉树的二分搜索法一样高效。
缺点也是显而易见的,我们需要耗费更多的空间来储存各个节点,并且在插入与删除的操作上面,相比于数组来说会比较复杂一些。但是,我个人认为与二叉树的插入和删除操作复杂程度差不多。