如何在 O(n) 时间内对双向链表进行二进制搜索?

Posted

技术标签:

【中文标题】如何在 O(n) 时间内对双向链表进行二进制搜索?【英文标题】:How is it possible to do binary search on a doubly-linked list in O(n) time? 【发布时间】:2013-11-02 11:33:26 【问题描述】:

我听说可以在 O(n) 时间内在双向链表上实现二进制搜索。访问双向链表的随机元素需要 O(n) 时间,而二进制搜索访问 O(log n) 不同的元素,那么运行时间不应该是 O(n log n) 吗?

【问题讨论】:

你可以在 O(n) 中进行线性搜索,那么为什么要进行需要 O(nlogn) 或任何其他超过 O(n) 的算法的二进制搜索?使用 BinarySearch 方法在包(非集合,如数组或链表)上定义的抽象 API 应该简单地将链表的版本实现为线性搜索......调用者无法判断使用哪种算法,其他通过时间它并看到它实际上并不是一个毫无意义的缓慢二进制搜索。实际上,链表上的二分搜索可以通过线性搜索在 O(n) 中实现……名称并没有规定它实际上是做什么的。 优点是,虽然它在遍历列表时做了 O(n) 的工作,但它只进行了 O(log n) 的比较。如果列表中存储了巨大的元素,这可能比进行线性搜索要快得多。 好的,好点...我现在已经阅读了您对问题的回答。声明“技术上说双向链表上的二进制搜索的运行时间是 O(n log n)”是错误的,因为您自己提供了一个 O(n) 算法和 O(logn) 比较。所以你在你听到的问题中所说的是正确的......“可以在 O(n) 时间内对双向链表实现二进制搜索。” ...您应该在答案的顶部修复声明。无论如何,感谢您的算法和分析.. 我正在寻找那个。 P.S.它也适用于单链表,因为您始终拥有两个子列表的头部,并且您可以使用 Floyd 的兔兔把戏 (geeksforgeeks.org/…) 找到中点。 声称二分查找需要时间 O(n log n) 实际上并没有错。这不是一个紧密的界限。例如,我声称我最多 1 公里高是不正确的,尽管实际上我比这要矮得多。另外,感谢您分享该链接!我发布了另一个问题,我将详细介绍该算法背后的细节。 【参考方案1】:

说在双向链表上进行二分查找的运行时间是 O(n log n) 在技术上是正确的,但这并不是一个严格的上限。使用更好的二分搜索实现和更聪明的分析,可以让二分搜索在 O(n) 时间内运行。

二分查找的基本思想如下:

如果列表为空,则搜索的元素不存在。 否则: 查看中间元素。 如果它与相关元素匹配,则返回它。 如果它大于相关元素,则丢弃列表的后半部分。 如果它小于相关元素,则丢弃列表的前半部分。

在双向链表上的二分搜索的简单实现将通过计算索引以在每次迭代中查找(就像在数组情况下一样),然后通过从列表的前面开始并扫描来访问每个索引前进适当的步数。这确实很慢。如果要搜索的元素位于数组的最后,则查找的索引将是 n/2、3n/4、7n/8 等。总结在最坏情况下所做的工作,我们得到

n / 2 + 3n/4 + 7n/8 + 15n/16 + ...(Θ(log n) 项)

≥ n / 2 + n / 2 + ... + n / 2 (Θ(log n) 项)

= Θ(n log n)

n / 2 + 3n/4 + 7n/8 + 15n/16 + ...(Θ(log n) 项)

≤ n + n + ... + n (Θ(log n) 项)

= Θ(n log n)

因此,该算法的最坏情况时间复杂度为 Θ(n log n)。

但是,我们可以通过更聪明的方法来加快速度 Θ(log n) 的速度。前面算法慢的原因是每次我们需要查找一个元素时,我们都是从数组的开头开始查找。但是,我们不需要这样做。在第一次查找中间元素后,我们已经在数组中间,我们知道我们要进行的下一次查找将在位置 n / 4 或 3n / 4,这只是距离我们离开的地方 n / 4 (与 n / 4 或 3n / 4 相比,如果我们从数组的开头开始)。如果我们只是从停止位置 (n / 2) 移动到下一个位置,而不是从列表的最前面重新开始呢?

这是我们的新算法。首先扫描到阵列的中间,这需要 n / 2 步。然后,判断是访问数组前半部分中间的元素还是数组后半部分中间的元素。从位置 n / 2 到达那里只需要 n / 4 步。从那里到包含元素的数组的四分之一的中点只需要 n / 8 步,从那里到包含元素的数组的八分之一的中点只需要 n / 16 步,依此类推。这意味着总步数由下式给出

n / 2 + n / 4 + n / 8 + n / 16 + ...

= n (1/2 + 1/4 + 1/8 + ...)

≤n

这是因为无限几何级数 1/2 + 1/4 + 1/8 + ... 之和为 1。因此,在最坏情况下所做的总功只有 Θ(n),这比之前的 Θ(n log n) 最坏情况要好得多。

最后一个细节:你为什么要这样做? 毕竟,在双向链表中搜索一个元素已经花费了 O(n) 时间。这种方法的一个主要优点是,即使运行时间是 O(n),我们最终也只会进行 O(log n) 的总比较(二进制搜索的每一步)。这意味着,如果比较代价高昂,我们可能最终使用二分查找比进行正常线性搜索做的工作更少,因为 O(n) 来自于遍历列表完成的工作,而不是进行比较完成的工作。

【讨论】:

以上是关于如何在 O(n) 时间内对双向链表进行二进制搜索?的主要内容,如果未能解决你的问题,请参考以下文章

如何在排序链表上应用二进制搜索 O(log n)?

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表

使用 O(m) 空间在 O(n) 时间内对向量<int>(n) 进行排序?

双向链表的原理与实现

在 O(logk) 时间内删除 K 个排序的双向链表的最小值