【问题标题】:Shouldn't the time complexity of binary search be O(ceil(logn))?二分查找的时间复杂度不应该是O(ceil(logn))吗?
【发布时间】:2021-09-24 22:33:13
【问题描述】:

这似乎是一个常见问题,可以在任何地方找到答案,但事实并非如此:我在 Internet 上的任何地方都找不到答案。关键是,我从来没有见过任何地方问过关于时间复杂度是否可能是O(ceil(logn)) 的问题,我想不通,所以我决定在这里问一个问题。

首先,假设我有一个包含n 数字的排序数组,并且我想使用二进制搜索算法在其中搜索一个值。下面列出了在最坏情况下所需的步骤数:

n steps
1 1
2 2
3 2
4 3
5 3
6 3
7 3
8 4
9 4
10 4

如您所见,n 个数字的数组所需的步骤是ceil(log2n)ceil(log2n) 表示大于或等于log2n 的最小整数)。所以我觉得二分查找的时间复杂度应该是O(ceil(logn)),但是根据Wikipedia,时间复杂度应该是O(logn),为什么呢?有什么问题吗?

【问题讨论】:

  • 问题是天花板还是地板都没有关系。 Big-Oh 符号只是对运行时间复杂度的估计,这就是为什么我们在计算它时删除常量和低阶项的原因。即使你说的是对的,也可以这样想O(ceil(logn)) <= O(logn) + 1,现在你去掉常量1
  • 在 Sedgewick 的“算法分析”中,他准确计算操作,计算天花板和楼层,就像您在问题中所做的那样。例如,计算归并排序的确切比较次数(这会在图中产生有趣的细节)。大 O 抛弃了很多细节,在 Sedgewick 的一些工作中(例如与 Flageolet 的分析组合),他们认为大 O 太粗略且使用范围太广。但要回答你的问题,正如其他人指出的那样 O(log n) = O(lg n) = O(ceil(log n)) = O(floor(log n))。
  • 为了证明复杂性是一样的,我建议你直接使用大O的定义,而不是像丢弃常量和低阶项那样使用“经验法则”。这两条规则是无害的,但人们遵循的其他经验法则并不完全正确,或者仅适用于某些情况。最好坚持定义,因为这样你总是在坚实的理论基础上。

标签: algorithm time-complexity big-o binary-search


【解决方案1】:

正如我已经在其他两个答案中解释的那样(参见 herehere),Big-O 符号并不是大多数人认为的那样。它既不告诉你任何关于算法的速度,也不告诉你处理步骤的数量。

Big-O 唯一告诉您的是,如果输入元素的数量发生变化,算法的处理时间将如何变化。它保持不变吗?它是线性上升的吗?它是否以对数方式上升?它是二次上升的吗?这是 Big-O 唯一回答的问题。

因此O(5)O(1000000) 相同,因为两者都只是表示常量,通常写为O(1)。而O(n + 100000)O(5n + 8) 相同,都是简单平均线性,通常写为O(n)

我知道很多人会说“是的,但是 O(5n)O(2n) 更陡峭”,这是绝对正确的,但它们仍然是线性的,Big-O 并不是要比较两个具有线性复杂度的函数其他,但关于将功能分类为粗略的类别。人们只是对这些类别以数学函数命名这一事实感到困惑,因此他们认为任何函数都可能适用于 Big-O 表示法,但事实并非如此。只有具有不同特征的函数才会有自己的 Big-O 表示法。

以下概述远未完成,但在实践中主要涉及以下 Big-O 符号:

  • O(1) - 常量
  • O(log log n) - 双对数
  • O(log n) - 对数
  • O((log n)^c), c > 1 - 多对数
  • O(n^c), 0 < c < 1 - 分数幂
  • O(n) - 线性
  • O(n log n) = O(log n!) - 线性的
  • O(n^2) - 二次方
  • O(n^c), c > 1 - 多项式
  • O(c^n), c > 1 - 指数
  • O(n!) - 阶乘

除了将它们写成函数之外,还可以只给它们每个命名,但将它们写成函数有两个优点:具有一定数学背景的人会立即在脑海中浮现出图形的图像,而且很容易介绍只要你能用数学方法描述它们的图形,就不用想出花哨的名字。

【讨论】:

    【解决方案2】:

    O(ceil(log n))O(log n) 都表示相同的渐近复杂度(对数复杂度)。

    或者简单地说:O(ceil(log n)) = O(log n)

    【讨论】:

      【解决方案3】:

      您在这里看到的是量化算法工作量的两种不同方法之间的区别。第一种方法是准确计算某事发生的次数,从而得出答案的精确表达式。例如,在这种情况下,您注意到二进制搜索的轮数是⌈lg (n+1)⌉。这精确计算了在最坏情况下在二分搜索中完成的轮数,如果您需要知道确切的数字,这是一个很好的信息。

      但是,在许多情况下,我们对算法所做的确切工作量并不感兴趣,而是更感兴趣的是随着输入变大该算法将如何扩展。例如,假设我们在一个大小为 103 的数组上运行二分查找,发现运行需要 10μs。我们应该期望它在大小为 106 的输入上运行需要多长时间?我们可以通过将 n = 103 代入公式 (11) 来计算在最坏情况下二分搜索将执行的轮数,然后我们可以尝试计算出我们需要多长时间认为这些迭代中的每一个都需要(10μs / 11 轮 ≈ 0.91 μs / 轮),然后计算出在 n = 106 (21) 时我们将进行多少轮并乘以以每轮成本计算的轮数估计约为 19.1μs。

      这是一项繁重的工作。有没有更简单的方法来做到这一点?

      这就是 big-O 表示法的用武之地。当我们说在最坏情况下二分搜索的成本是 O(log n) 时,我们不是说“确切的工作量二进制搜索确实由表达式 log n 给出。”相反,我们所说的是*二分查找的运行时间,在最坏的情况下,其缩放方式与函数 log n 的缩放方式相同。”通过对数的性质,我们可以看到 log n2 = 2 log n,因此如果将输入的大小平方成对数增长的函数,则可以合理猜测该函数的输出将大致翻倍。

      在我们的例子中,如果我们知道对大小为 103 的输入进行二分搜索的运行时间为 10μs,并且我们很想知道在大小为 10 的输入上的运行时间是多少6 = (103)2,我们可以相当合理地猜测它大约是 20μs,因为我们已经对输入进行了平方.如果你看看我上面做的数学运算,这与我们通过更详细的分析得到的数字非常接近。

      所以从这个意义上说,说“二分查找的运行时间是 O(log n)”是一个很好的简写方式,它表示“无论函数运行时间的确切公式是什么,它都会随着输入。”这使我们能够从现有数据点进行推断,而无需计算出领先的系数或类似的东西。

      当您考虑到即使二分搜索恰好进行 ⌈lg (n+1)⌉ 轮次时,这尤其有用,但该表达式并不能准确捕获执行二分搜索的全部成本。根据比较的方式,每一轮可能需要稍微不同的时间,并且可能有一些设置工作会给出一个附加常数,并且每一轮在实际计算时间方面的成本不能纯粹确定从代码本身。这就是为什么我们在量化算法的运行速度时经常使用大 O 表示法 - 它让我们能够捕捉到最显着的细节(事物将如何扩展),而无需确定所有的数学细节。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2018-08-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多