【问题标题】:When does Big-O notation fail?Big-O 表示法何时失败?
【发布时间】:2009-06-02 18:57:21
【问题描述】:

Big-O 表示法[1] 在实践中失败的例子有哪些?

也就是说:算法的 Big-O 运行时间什么时候会预测算法 A 比算法 B 快,而实际运行时算法 B 会更快?

稍微宽泛一点:关于算法性能的理论预测何时与观察到的运行时间不匹配?非 Big-O 预测可能基于搜索树中的平均/预期旋转次数,或排序算法中的比较次数,表示为因子乘以元素数量。

澄清

尽管有些答案是这样说的,但 Big-O 表示法旨在预测算法性能。也就是说,它是一个有缺陷的工具:它只谈论渐近性能,并且模糊了常数因子。它这样做是有原因的:它旨在预测算法性能,而与您在哪台计算机上执行算法无关。

我想知道的是这个:这个工具的缺陷什么时候表现出来?我发现 Big-O 表示法相当有用,但远非完美。有哪些陷阱、边缘情况和陷阱?

我正在寻找的一个示例:使用斐波那契堆而不是二进制堆运行 Dijkstra 的最短路径算法,得到 O(m + n log n) 时间与 O((m+n) log n) , 对于 n 个顶点和 m 个边。您预计斐波那契堆迟早会提高速度,但在我的实验中从未实现过速度提高。

(没有证据的实验证据表明,在均匀随机边权重上运行的二叉堆花费 O(1) 时间而不是 O(log n) 时间;这是实验的一个大问题。另一个需要计算的问题是对 DecreaseKey 的预期调用次数。

[1] 实际上,失败的不是 notation,而是 notation 所代表的 concepts,以及预测算法性能的理论方法。 反迂腐>

关于接受的答案

我接受了一个答案,以突出我希望得到的答案。存在许多同样好的不同答案:) 我喜欢这个答案的地方在于,它提出了一个通用规则,用于何时 Big-O 表示法“失败”(当缓存未命中主导执行时间时),这也可能增加理解(在某种意义上我不知道如何最好地表达 ATM)。

【问题讨论】:

  • stackoverflow.com/questions/842242/…类似(但不是完全重复)
  • 我不会说 Big-O 有缺陷,只是有限。说它有缺陷就像说
  • 您的说明有误。它不是一个有缺陷的工具,因为这意味着它给出了错误的答案。它没有。这是一个可证明是正确的工具。问题是人们滥用它来回答它无法回答的问题。如果你只用符号来描述它能够描述的东西,那么就没有缺陷。一旦您尝试使用它来描述实际性能,它就会失败,因为它没有所有相关信息。它的目的不是预测算法性能,而是预测算法的可扩展性。当问题规模变大时,算法会变慢多少?
  • 如果你想知道(最坏情况下的)速度,你需要知道大 O、输入的大小、算法的常数因子以及在现实世界中的实现细节。问题实际上应该是“Big-O 什么时候还不够”,而不是“Big-O 什么时候失败”。随着大 O 复杂度之间的差异减小,n 减小,您需要越来越多地关注这些其他因素。
  • 我对您的编辑的问题只是您称其为“有缺陷”。 ;) 我不认为限制是工具的失败。是我正在打字的键盘的限制,我不能用它去法国旅行吗?它只是不是为了那个。如果我想去法国旅行,我会买机票,而不是键盘。我认为这正是该符号有用的原因:因为它区分了不同的与性能相关的问题。它不会告诉您程序将如何执行,它会告诉您算法一个方面的确切属性。

标签: algorithm language-agnostic theory big-o


【解决方案1】:

它仅在一种情况下失败:当人们试图将它用于不适合的事情时。

它告诉您算法如何扩展。它不会告诉你它有多快。

Big-O 符号不会告诉您在任何特定情况下哪种算法会更快。它只是告诉你,对于足够大的输入,一个会比另一个快。

【讨论】:

  • +1 完全正确。其他人提到了常数因素,但忽略了 big-o 最终是衡量可扩展性的指标。
  • 对。 Big-O 表示法不会失败。人们无法理解并正确应用它。
  • 我不同意“它的意义”。 Big-O 可能是衡量可扩展性的指标,但人们对他们的软件的疑问不是“这有多大的可扩展性”,而是“这有多快”。 Big-O 以及您对各种操作速度的了解和对数据结构的理解,可以帮助您估计它的速度以及如何使其更快。
  • @mquander:如果您需要知道算法 A 在您的特定情况下是否比 B 快,那么 big-o 符号对您没有帮助。然后,您需要测试和基准测试您的实现。 Big-O 表示法只是告诉您随着问题规模的增加,它们各自将如何表现。如果人们的问题是“这有多快”,那么他们不应该使用大 O 表示法来寻找答案。
  • 基准测试和分析器是很好的工具,但拥有用于推理代码和估计如何改进代码的心理工具也很好。算法的大 O 阶是表达算法结构特征的一种简洁方式,在很多情况下对性能有很大影响,它是进行这些估计的重要心理工具。说它没有告诉你“它有多快”感觉就像是一个大挑剔。
【解决方案2】:

当 N 较小时,常数因子占主导地位。在包含五个项目的数组中查找项目可能比在哈希表中查找更快。

【讨论】:

    【解决方案3】:

    简答:当 n 很小时。当您只有三个目的地时,旅行商问题很快就会得到解决(但是,在万亿元素的列表中找到最小的数字可能会持续一段时间,尽管这是 O(n)。)

    【讨论】:

      【解决方案4】:

      典型的例子是快速排序,它的最坏时间是 O(n^2),而堆排序是 O(n logn)。然而在实践中,快速排序通常比堆排序更快。为什么?两个原因:

      • Quicksort 中的每次迭代都比 Heapsort 简单得多。更重要的是,它可以通过简单的缓存策略轻松优化。

      • 最坏的情况很难打。

      但是恕我直言,这并不意味着“大 O 失败”。第一个因素(迭代时间)很容易纳入您的估计中。毕竟,大 O 数应该乘以这个几乎恒定的因子。

      如果你得到摊销数字而不是平均值,第二个因素就会消失。它们可能更难估计,但可以讲述更完整的故事

      【讨论】:

      • 注意:快速排序需要 预期 O(n log n) 时间。使用“预期”是由于分区算法中的随机性。您还可以确定性地在 O(n) 时间内围绕中位数进行分区,以实现快速排序的最坏情况 O(n log n)。
      【解决方案5】:

      Big O 失败的一个领域是内存访问模式。 Big O 只计算需要执行的操作——如果算法导致更多的缓存未命中或需要从磁盘调入的数据,它无法跟踪。对于小的 N,这些影响通常会占主导地位。例如,由于内存访问,通过 100 个整数的数组进行线性搜索可能会击败通过 100 个整数的二叉树进行的搜索,即使二叉树很可能需要更少的操作。每个树节点都会导致缓存未命中,而线性搜索在每次查找时大部分都会命中缓存。

      【讨论】:

      • 这在最坏的情况下是一个常数因子,仅适用于小 n(尽管 n 可能比其他情况大)。
      • @David 毕竟问题是,Big-O 表示法何时无法预测性能。
      • 是的。一般的答案是“对于足够小的 n”。像内存访问模式和簿记这样的事情只会影响太小的 n 的大小。
      • @David - 我的回答中提到了这一点,“对于小 N,这些影响通常会占主导地位。”我想给出一个具体的案例,因为已经有几个答案讨论了多大的 O 只估计函数执行时间的增长。
      • 实际上.. Big-O 表示法也用于磁盘访问。通常它用于指定计算步骤,但整个研究领域都专注于减少磁盘访问量。他们在那里使用 Big-O 来表示磁盘访问量。这方面的课程示例:win.tue.nl/~hermanh/teaching/2IL35
      【解决方案6】:

      Big-O 描述了算法的效率/复杂性,而不一定是给定代码块的实现的运行时间。这并不意味着 Big-O 失败了。它只是意味着它不是用来预测运行时间的。

      查看this question 的答案,了解 Big-O 的详细定义。

      【讨论】:

        【解决方案7】:
        1. 对于大多数算法,都有“平均情况”和“最坏情况”。如果您的数据经常陷入“最坏情况”场景,那么另一种算法(理论上在平均情况下效率较低)可能会证明对您的数据更有效。

        2. 某些算法还具有您的数据可以利用的最佳情况。例如,一些排序算法的理论效率很差,但如果数据已经排序(或接近排序),实际上速度非常快。另一种算法虽然理论上在一般情况下更快,但可能无法利用数据已经排序并且在实践中表现更差的事实。

        3. 对于非常小的数据集,有时具有更好理论效率的算法实际上可能由于“k”值较大而效率较低。

        【讨论】:

        • 请注意,在情况 #2 中,如果您更准确地指定您的问题(“对大部分排序的列表进行排序”)Big-O 会给您正确的答案。
        • @Segfault - 非常正确,但更重要的是算法对其性能有“极端”情况,包括好的极端和坏的极端。因此,一个典型的案例 big-O 并不总是能说明全部情况。了解您的数据和可用算法的边缘情况有时会导致您选择与典型情况下可能最佳的算法不同的算法。
        【解决方案8】:

        一个例子(我不是专家)是线性规划的单纯形算法在任意输入上具有指数最坏情况复杂性,即使它们在实践中表现良好。一个有趣的解决方案是考虑“平滑复杂度”,它通过查看任意输入的小随机扰动来混合最坏情况和平均情况的性能。

        Spielman and Teng (2004) 能够证明阴影顶点单纯形算法具有多项式平滑复杂度。

        【讨论】:

          【解决方案9】:

          Big O 确实 not 说例如算法 A 比算法 B 运行得更快。可以说,当输入增长时,算法 A 使用的时间或空间的增长速度与算法 B 不同。但是,对于任何特定的输入大小,大 O 表示法并不能说明一种算法相对于另一种算法的性能。

          例如,A 每次操作可能较慢,但具有比 B 更好的 big-O。B 对于较小的输入具有更高的性能,但如果数据大小增加,将会有一些截止点 A 变得更快. Big-O 本身并没有说明截止点在哪里。

          【讨论】:

            【解决方案10】:

            一般的答案是,Big-O 通过隐藏常数因素让你变得非常草率。如问题中所述,斐波那契堆的使用就是一个例子。斐波那契堆确实有很好的渐近运行时,但实际上常数因子太大而无法用于现实生活中遇到的数据集的大小。

            斐波那契堆通常用于证明图相关算法的渐近复杂度的良好下界。

            另一个类似的例子是矩阵乘法的Coppersmith-Winograd algorithm。它是目前已知的矩阵乘法渐近运行时间最快的算法,O(n2.376)。然而,它的常数因子太大而无法在实践中使用。与斐波那契堆一样,它经常被用作其他算法中的构建块来证明理论上的时间界限。

            【讨论】:

              【解决方案11】:

              这在一定程度上取决于 Big-O 测量的内容 - 在最坏的情况下,它通常会“失败”,因为运行时性能会比 Big-O 建议的要好得多。如果是一般情况,那么情况可能会更糟。

              如果算法的输入数据有一些先验信息,Big-O 表示法通常会“失败”。通常,Big-O 表示法指的是最坏情况下的复杂性 - 如果数据是完全随机的或完全非随机的,这通常会发生。

              例如,如果您将数据提供给经过分析的算法并且 big-o 基于随机数据,但您的数据具有非常明确的结构,那么您的结果时间可能会比预期的快得多。同样,如果您测量的是平均复杂度,并且您提供的数据非常随机化,那么算法的性能可能会比预期的差得多。

              【讨论】:

              • 根据我所学到的,你永远不能对输入做出假设。如果您在算法中进行一些随机化,您只能谈论平均运行时间(或预期运行时间)。因此,输入中的任何模式都不应影响平均运行时间。
              • 你不能对输入做出假设,这就是为什么它通常是“最坏情况”或“平均”运行时间,但这是我的观点 - 通常,在现实世界中,输入已经遵循某种模式,因此预测的复杂性与实际计算时间不匹配。
              【解决方案12】:
              1. 小 N - 对于当今的计算机而言,100 可能太小而无需担心。
              2. 隐藏乘数 - IE 合并与快速排序。
              3. 病理案例 - 再次,合并 vs 快速

              【讨论】:

              • 隐藏的乘数只影响小到多小。
              • @DavidThornley:废话,如果你有两个 O(nlogn) 算法,但一个有 2x 倍数,另一个有 1x,那么 2x 将慢两倍 对于所有 n.
              【解决方案13】:

              Big-Oh 表示法失败的一个广泛领域是当数据量超过可用 RAM 量时。

              以排序为例,排序所花费的时间不受比较或交换次数的支配(其中分别有O(n log n)和O(n),在最优情况下)。时间的长短取决于磁盘操作的数量:块写入和块读取。

              为了更好地分析处理超出可用 RAM 的数据的算法,I/O 模型应运而生,您可以在其中计算磁盘读取次数。其中,您考虑三个参数:

              • 元素个数,N;
              • 内存量(RAM),M(可以在内存中的元素数量);和
              • 磁盘块的大小,B(每个块的元素数)。

              值得注意的是磁盘空间量;这被视为无限。一个典型的额外假设是 M > B2.

              继续排序示例,您通常倾向于在 I/O 情况下进行归并排序:将元素分成大小为 θ(M) 的块并在内存中对它们进行排序(例如,使用快速排序)。然后,通过将每个块中的第一个块读取到内存中来合并它们中的 θ(M/B),将所有元素填充到一个堆中,并重复选择最小的元素,直到你选择了其中的 B 个。写出这个新的合并块并继续。如果您耗尽了读入内存的块之一,请从同一块中读取一个新块并将其放入堆中。

              (所有表达式都应该被理解为大 θ)。您形成 N/M 个排序的块,然后合并。你合并N/M次日志(base M/B);每次读取和写入所有 N/B 块时,都需要 N/B * (log base M/B of N/M) 时间。

              您可以分析内存中的排序算法(经过适当修改以包括块读取和块写入)并发现它们的效率远低于我介绍的合并排序。

              这些知识来自我的 I/O 算法课程,Arge 和 Brodal (http://daimi.au.dk/~large/ioS08/);我还进行了验证该理论的实验:一旦超出内存,堆排序就会花费“几乎无限”的时间。快速排序变得难以忍受,归并排序几乎无法忍受,I/O 效率高的归并排序表现良好(最好的)。

              【讨论】:

              • 这不是 Big-O 的失败,而是未能正确模拟您的执行时间。
              【解决方案14】:

              我见过一些案例,随着数据集的增长,算法复杂性变得不如内存访问模式重要。在某些情况下,使用智能算法导航大型数据结构可能会导致比使用更糟糕的 big-O 的算法更多的页面错误或缓存未命中。

              对于较小的 n,两种算法可能具有可比性。随着 n 的增加,更智能的算法会表现得更好。但是,在某些时候,n 增长到足以使系统屈服于内存压力,在这种情况下,“更糟糕”的算法实际上可能会执行得更好,因为常量基本上被重置了。

              不过,这并不是特别有趣。当你达到这个反转点时,这两种算法的性能通常都无法接受,你必须找到一种新的算法,它具有更友好的内存访问模式和更好的 big-O 复杂度。

              【讨论】:

                【解决方案15】:

                这个问题就像在问,“一个人的智商什么时候会在实践中失败?”很明显,智商高并不意味着你会在生活中取得成功,智商低并不意味着你会灭亡。然而,我们衡量 IQ 作为评估潜力的一种手段,即使它不是绝对的。

                在算法中,Big-Oh 符号为您提供算法的 IQ。这并不一定意味着该算法将在您的特定情况下表现最佳,但有一些数学基础表明该算法具有一定的潜力。如果 Big-Oh 表示法足以衡量性能,那么您会看到更多它和更少的运行时测试。

                将 Big-Oh 视为一个范围,而不是衡量好坏的具体衡量标准。有最好的情况和最坏的情况,以及介于两者之间的大量情况。根据算法在 Big-Oh 范围内的拟合程度来选择算法,但不要依赖符号作为衡量性能的绝对标准。

                【讨论】:

                  【解决方案16】:

                  当您的数据不适合模型时,big-o 表示法仍然有效,但您会看到最佳和最坏情况的重叠。

                  此外,某些操作针对线性数据访问与随机数据访问进行了调整,因此一种算法虽然在周期方面更胜一筹,但如果调用它的方法从设计中改变,它可能会非常缓慢。同样,如果算法由于访问内存的方式而导致页面/缓存未命中,Big-O 不会准确估计运行进程的成本。

                  显然,我忘记了,当 N 很小时 :)

                  【讨论】:

                  • 我会注意到,虽然我指的是时间,以及慢与快,当然对 O 表示法的很多兴趣来自对代码执行速度的期望,即使 Big -O 符号不直接指速度。
                  【解决方案17】:

                  简短的回答:当您开始使用大量内存时,总是在现代硬件上。教科书假设内存访问是统一的,但现在不再如此。您当然可以对非统一访问模型进行 Big O 分析,但这有点复杂。

                  小 n 例很明显但并不有趣:足够快就是足够快。

                  在实践中,我在使用 Delphi、Java、C# 和 Smalltalk 中包含几百万个对象的标准集合时遇到了问题。而较小的那些被证明是哈希函数或比较的主要因素

                  【讨论】:

                    【解决方案18】:

                    Robert Sedgewick 在他的 Coursera 算法分析课程中谈到了大 O 表示法的缺点。他将特别令人震惊的示例称为银河算法,因为虽然它们可能比它们的前辈具有更好的复杂度等级,但它需要天文规模的输入才能在实践中显示出来。

                    https://www.cs.princeton.edu/~rs/talks/AlgsMasses.pdf

                    【讨论】:

                      【解决方案19】:

                      Big O 和它的兄弟用于比较渐近数学函数的增长。我想强调数学部分。它完全是关于能够将你的问题减少到输入增长的函数,也就是规模。它为您提供了一个很好的图,其中您的输入(x 轴)与执行的操作数(y 轴)相关。这纯粹基于数学函数,因此需要我们将所使用的算法准确地建模为各种多项式。然后是缩放假设。

                      当数据是有限的、固定的和恒定的大小时,Big O 会立即失去相关性。这就是为什么几乎所有的嵌入式程序员甚至都不关心大 O。从数学上讲,这总是 O(1),但我们知道我们需要在大 O 的水平上优化空间和 Mhz 时序预算的代码不起作用。这是优化的顺序,单个组件由于其直接的性能依赖于系统而很重要。

                      Big O 的另一个失败在于它假设硬件差异并不重要。具有 MAC、MMU 和/或位移位低延迟数学运算的 CPU 将优于某些可能被错误地识别为渐近表示法中更高阶的任务。这只是因为模型本身的限制。

                      另一个大 O 变得完全不相关的常见情况是,我们错误地识别了要解决的问题的性质并最终得到一棵二叉树,而实际上解决方案实际上是一个状态机。整个算法方案经常忽略有限状态机问题。这是因为状态机的复杂性是根据状态的数量而不是输入或数据的数量增加的,在大多数情况下是恒定的。

                      这里的另一个方面是内存访问本身,它是与硬件和执行环境断开连接问题的延伸。很多时候,内存优化会提供性能优化,反之亦然。它们不一定是相互排斥的。这些关系不能轻易地建模成简单的多项式。在堆(内存区域而不是算法堆)数据上运行的理论上不好的算法通常会优于在堆栈中的数据上运行的理论上好的算法。这是因为内存访问和存储效率的时间和空间复杂性在大多数情况下不是数学模型的一部分,即使尝试建模也经常被忽略为可能具有高影响的低阶项。这是因为这些将显示为一长串低阶项,当模型忽略的低阶项数量足够多时,这些项可能会产生更大的影响。

                      想象 n3+86n2+5*106n2+109n

                      很明显,具有高倍数的低阶项一起可能比大 O 模型倾向于忽略的最高阶项具有更大的意义。它会让我们忽略除 n3 之外的所有内容。术语“足够大的 n”完全被滥用来想象不切实际的场景来证明算法的合理性。在这种情况下,n 必须非常大,以至于在你不必担心算法本身之前,你就会用完物理内存。算法如果你甚至不能存储数据也没关系。当内存访问被建模时;低阶项可能最终看起来像上面的多项式,具有超过 100 个高度缩放的低阶项。但是,出于所有实际目的,这些项甚至从来都不是算法试图定义的方程式的一部分。

                      大多数科学记数法通常是对数学函数的描述,用于建模。它们是工具。因此,该工具的实用性受到限制,并且仅与模型本身一样好。如果模型无法描述或不适合手头的问题,那么模型根本无法达到目的。在这种情况下,需要使用不同的模型而这不起作用时,直接的方法可能会很好地满足您的目的。

                      此外,许多原始算法是具有完全不同工作机制的图灵机模型,而今天所有的计算都是 RASP 模型。在你进入大 O 或任何其他模型之前,首先问自己这个问题“我是否为手头的任务选择了正确的模型,我是否拥有最实际准确的数学函数?”。如果答案是“不”,那么就凭你的经验和直觉,忽略那些花哨的东西。

                      【讨论】:

                        猜你喜欢
                        • 2023-03-31
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2010-11-24
                        • 2012-03-02
                        相关资源
                        最近更新 更多