在任何情况下,您都希望使用较高的big-O时间复杂度算法而不是较低的算法吗?


242

在任何情况下,您都更喜欢O(log n)时间复杂O(1)度而不是时间复杂度吗?还是O(n)O(log n)

你有什么例子吗?


67
如果理解前者而不是后者,我宁愿选择O(log n)算法而不是O(1)算法...
Codor

14
理论计算机科学中有大量使用O(1)操作的不切实际的数据结构。一个示例是位向量上的select(),可以使用5层间接寻址在o(n)个额外空间和每个操作O(1)中支持它。根据简洁数据结构库的
Niklas B的

17
较低的渐近复杂度不能保证更快的运行时间。研究矩阵乘法的一个具体例子。
康纳·克拉克2015年

54
同样...给定足够大的表查找量,任何算法都可以转换为O(1);)
Connor Clark

19
@Hoten-假设表查找为O(1),对于您正在谈论的表的大小,这根本不是给定的!:)
Jander 2015年

Answers:


266

可能有很多原因会优先考虑O时间复杂度较高而不是较低的算法:

  • 在大多数情况下,难以实现较低的big-O复杂度,并且需要熟练的实现,大量的知识和大量的测试。
  • big-O隐藏了有关常量的详细信息10^5从big-O的角度来看,执行in的算法比1/10^5 * log(n)O(1)vs O(log(n)n更好,但是在大多数情况下,第一个算法的执行效果更好。例如,矩阵乘法的最佳复杂度是,O(n^2.373)但是常数是如此之高,以至于(据我所知)没有计算库使用它。
  • 当您计算大型商品时,big-O很有意义。如果需要对三个数字进行排序,则无论使用O(n*log(n))还是O(n^2)算法都无关紧要。
  • 有时,小写时间复杂度的优势实际上可以忽略不计。对于例如有一个数据结构探戈树这给出了一个O(log log N)时间复杂度找到一个项目,但也有它找到了在同一二叉树O(log n)。即使是数量巨大n = 10^20的差异也可以忽略不计。
  • 时间的复杂性不是全部。想象一下一种可以运行O(n^2)并需要O(n^2)内存的算法。当n不是很大时,在O(n^3)时间和O(1)空间上可能更可取。问题是您可以等待很长时间,但高度怀疑您是否可以找到足够大的RAM来与算法配合使用
  • 并行化是我们分布式世界中的一个很好的功能。有些算法很容易并行化,有些根本不并行。有时,在1000台具有较高复杂度的商用机器上运行算法比使用一台具有稍微更好的复杂度的机器有意义。
  • 在某些地方(安全性),可能需要复杂性。没有人希望拥有一种可以快速进行哈希运算的哈希算法(因为其他人可以更快地对您进行暴力破解)
  • 尽管这与复杂性的切换无关,但是应以防止定时攻击的方式编写一些安全功能。它们大多处于相同的复杂度类中,但是以某种方式进行修改,以使其总是在更坏的情况下执行某些操作。一个例子是比较字符串是否相等。在大多数应用程序中,如果前几个字节不同,则可以快速中断,但是在安全性方面,您仍然要等到最后才告诉坏消息。
  • 有人为低复杂度算法申请了专利,对于公司而言,使用更高的复杂度比付钱更经济。
  • 一些算法可以很好地适应特定情况。例如,插入排序的平均时间复杂度为O(n^2),比quicksort或mergesort差,但是作为一种在线算法,它可以在接收到值(作为用户输入)时有效地对值列表进行排序,而大多数其他算法只能有效地进行操作在值的完整列表上。

6
此外,我已经看到几次人们专注于他们的中央算法的big-O,却忽略了设置成本。例如,如果您不需要一遍又一遍地建立哈希表,则比线性地遍历数组要昂贵得多。实际上,由于现代CPU的构建方式,即使是二进制搜索之类的东西,也可以在排序后的数组上像线性搜索一样快-分析是必需的。
a安2015年

@Luaan“实际上,由于现代CPU的构建方式,即使是二进制搜索之类的东西,也可以在排序数组上像线性搜索一样快-进行概要分析是必要的。” 有趣!您能解释一下在现代CPU上二进制搜索和线性搜索如何花费相同的时间吗?
DJG

3
@Luaan-没关系,我发现了这一点:schani.wordpress.com/2010/04/30/linear-vs-binary-search
DJG

2
@DenisdeBernardy:不,实际上不是。它们可能是P中的算法。即使不是这些算法,在对并行化意味着什么的合理定义下,也不意味着P!= NP。还要记住,搜索不确定性图腾机可能运行的空间是相当可并行的。
einpoklum 2015年

228

总是存在隐藏常数,在O(log n)算法中可以更低。因此,对于实际数据,它可以在实践中更快地工作。

还存在空间问题(例如,在烤面包机上运行)。

开发人员还担心时间-O(log n)可能易于实现和验证1000倍。


好的谢谢。我想这也可能是值得考虑O(LOGN)算法,以保证程序的稳定性(例如,在自我平衡二叉树)
V.Leymarie

16
我可以想到一个例子:对于一个小的排序数组,程序员编写一个二进制搜索功能比编写一个完整的哈希图实现并使用它要容易和紧凑。
上校32年

5
复杂性的一个例子:在O(n * log n)中很容易找到未排序列表的中位数,而在O(n)中很难做到。
Paul Draper

1
-1,请不要在您的烤面包机中放原木...开玩笑,这很重要。lg n是如此,如此,如此之kn,以至于大多数操作永远不会注意到差异。
corsiKa 2015年

3
还有一个事实是,大多数人熟悉的算法复杂性并未考虑到缓存的影响。在大多数人看来,在二叉树中查找内容是O(log2(n)),但实际上,由于二叉树的位置不好,这会更加糟糕。
2015年

57

令我惊讶的是,没有人提到内存绑定应用程序。

可能由于其复杂度(即O(1)< O(log n))或由于复杂度前面的常数较小(即2 n 2 <6 n 2)而导致浮点运算较少的算法。无论如何,如果较低的FLOP算法具有更多的内存限制,您可能仍然更喜欢FLOP较高的算法。

我所说的“内存绑定”是指您经常访问不断超出缓存的数据。为了获取此数据,必须先将实际存储空间中的内存拉入缓存,然后才能对其执行操作。此提取步骤通常很慢-比操作本身要慢得多。

因此,如果您的算法需要更多的操作(但是这些操作是对已在缓存中的数据执行的,因此不需要提取),那么它仍然会以更少的操作胜过您的算法(必须在超出次数的情况下执行) -cache数据(因此需要提取))。


1
Alistra在谈论“空间问题”时间接地解决了这一问题
Zach Saucier 2015年

2
大量的高速缓存未命中只会使最终执行乘以一个常数(对于具有1.6GHz ram的4核3.2GHz CPU,该值不大于8,通常会低得多),因此在较大的情况下,它被视为固定常数-O表示法。因此,缓存丢失的唯一原因就是将阈值n移到O(n)解开始比O(1)解慢的位置。
玛丽安·斯帕尼克

1
@MarianSpanik您当然是正确的。但这个问题问的情况下,我们宁愿O(logn)O(1)。您可以很容易地想到这样一种情况,即在所有可行的情况下n,即使内存复杂度较高,运行于内存的较少的应用程序也将以更快的运行时间运行。
NoseKnowsAll 2015年

@MarianSpanik不会在300个时钟周期内丢失缓存吗?8是哪里来的?
HopefulHelpful

43

在关注数据安全性的情况下,如果复杂度更高的算法对定时攻击具有更好的抵抗力,则复杂度更高的算法可能会比复杂度较低的算法更可取。


6
尽管您说的是正确的,但在那种情况下,根据定义,在O(1)中执行的算法不受定时攻击的影响。
贾斯汀·莱瑟德

17
@JustinLessard:为O(1)表示存在一定的输入大小,在此之后,算法的运行时将以常量为边界。低于此阈值会发生什么是未知的。而且,对于该算法的任何实际使用,甚至可能都不满足该阈值。该算法可能是线性的,因此例如泄漏有关输入长度的信息。
约尔格W¯¯米塔格

12
运行时也可能以不同的方式波动,同时仍然受到限制。如果运行时(n mod 5) + 1与之成正比,那么它仍将O(1)揭示有关的信息n。因此,即使运行时渐近(甚至可能在实践中)较慢,但更复杂,运行时更平滑的算法可能更可取。
Christian Semrau 2015年

基本上这就是为什么bcrypt被认为很好的原因。它使事情变慢了
大卫说恢复莫妮卡的时间

@DavidGrinberg这就是为什么使用bcrypt并适合该问题的原因。但这与讨论定时攻击的答案无关。
Christian Semrau 2015年

37

Alistra钉牢了它,但是没有提供任何示例,所以我会的。

您拥有商店销售的10,000个UPC代码的列表。10位UPC,价格整数(便士价格)和30个字符的收据说明。

O(log N)方法:您有一个排序列表。如果是ASCII,则为44个字节;如果为Unicode,则为84个字节。或者,将UPC视为int64,则得到42和72个字节。10,000条记录-在最高的情况下,您所需要的存储空间不到1兆字节。

O(1)方法:不要存储UPC,而是将其用作数组的入口。在最低的情况下,您需要使用近三分之一的存储空间。

您使用哪种方法取决于您的硬件。在大多数合理的现代配置中,您将使用log N方法。我可以想象第二种方法是正确的答案,如果由于某种原因您正在RAM严重不足但您拥有大量海量存储的环境中运行。磁盘上三分之一的TB没什么大不了的,将数据放在磁盘的一个探针中是值得的。简单的二进制方法平均需要13。(但是请注意,通过将您的键聚类,您可以将其降低到保证的3次读取,实际上,您将缓存第一个。)


2
我在这里有点困惑。您是在谈论创建一个100亿个条目的数组(其中大多数将是未定义的)并将UPC视为该数组的索引吗?
David Z

7
@DavidZ是的。如果使用稀疏数组,则可能不会获得O(1),但只会使用1MB内存。如果使用实际的阵列,则可以保证O(1)访问,但它将使用1/3 TB的内存。
纳文2015年

在现代系统上,它将使用1/3 TB的地址空间,但这并不意味着它将接近分配的大量后备内存。大多数现代OS直到需要时才提交存储用于分配。这样做时,实际上是在OS /硬件虚拟内存系统中隐藏数据的关联查找结构。
Phil Miller

@Novelocrat是的,但是如果您以RAM速度进行操作,则查找时间将无关紧要,没有理由使用40mb而不是1mb。阵列版本仅在存储访问成本很高时才有意义-您将要使用磁盘。
洛伦·佩希特

1
或者,当这不是对性能至关重要的操作,并且开发人员的时间很昂贵时malloc(search_space_size),只需说出并写出返回的内容就可以了。
菲尔·米勒

36

考虑一棵红黑树。它可以访问,搜索,插入和删除O(log n)。与具有访问权限的数组进行比较,O(1)其余操作为O(n)

因此,如果给我们一个比我们访问更频繁地插入,删除或搜索的应用程序,并且仅在这两种结构之间进行选择,我们将更喜欢红黑树。在这种情况下,您可能会说我们更喜欢红黑树的O(log n)访问时间。

为什么?因为访问不是我们最关心的问题。我们正在做出权衡:我们应用程序的性能在很大程度上受到除此因素以外的其他因素的影响。由于我们通过优化其他算法获得了巨大收益,因此我们可以让这种特定算法遭受性能损失。

因此,您的问题的答案很简单:当算法的增长率不是我们要优化的速度时,当我们要优化其他事物时。所有其他答案都是这种情况的特例。有时我们会优化其他操作的运行时间。有时我们会优化内存。有时我们会针对安全性进行优化。有时我们会优化可维护性。有时我们会为开发时间进行优化。当您知道算法的增长速度对运行时间没有最大的影响时,即使最重要的最低常量也正在针对运行时间进行优化。(如果您的数据集超出此范围,则您将针对算法的增长率进行优化,因为它最终将控制该常数。)一切都有代价,并且在许多情况下,我们将较高增长率的代价换为优化其他内容的算法。


人们过去常常在想(至少是我),不知道允许您使用带有O(1)查找和更新O(n)的数组的操作与红黑树相对应。大多数时候,我会首先考虑基于键的红黑树查找。但是要与数组匹配,应该有一点不同的结构,即保留较高节点中的子节点数量,以便在插入时提供基于索引的查找和重新索引。尽管我同意可以使用红黑平衡来保持平衡,但是如果您想模糊相应操作的细节,可以使用平衡树。
ONY

@ony红黑树可用于定义map / dictionary类型结构,但不是必须的。节点可以只是元素,实质上实现了排序列表。
jpmc26

定义元素顺序的排序列表和数组具有不同的信息量。一种是基于元素和集合之间的顺序,另一种是定义任意序列,而不必定义元素之间的顺序。另一件事是什么是您声明为O(log n)“红黑树”的“访问”和“搜索”?5在数组的位置2 插入[1, 2, 1, 4]将导致[1, 2, 5, 1 4](元素4将使索引从3更新为4)。如何在O(log n)引用为“排序列表”的“红黑树”中获得这种行为?
ONY

@ony“定义元素顺序的排序列表和数组具有不同数量的信息。” 是的,这就是为什么它们具有不同的性能特征的一部分。您错过了重点。一个并不是在所有情况下都能替代另一种。他们优化了不同的事物做出了不同的权衡,而问题的关键在于,开发人员正在不断做出有关这些权衡的决策。
jpmc26 '19

@ony在算法性能的上下文中,访问,搜索,插入和删除具有特定含义。Access正在按位置获取元素。搜索是按值定位元素(该值仅在非地图结构的包含检查中有任何实际应用)。但是,插入和删除应该很简单。示例用法可以在这里看到。
jpmc26 '19

23

是。

在实际情况下,我们对使用短字符串键和长字符串键进行表查找进行了一些测试。

我们使用了std::mapa std::unordered_map和a ,其中的哈希值在字符串的长度上最多采样10次(我们的键通常像guid一样,所以这很不错),而哈希值则对每个字符进行采样(理论上减少了冲突),我们进行==比较的未排序向量,以及(如果我没记错的话)我们也存储哈希的未排序向量,首先比较哈希,然后比较字符。

这些算法的范围从O(1)(unordered_map)到O(n)(线性搜索)。

对于中等大小的N,通常O(n)胜过O(1)。我们怀疑这是因为基于节点的容器需要我们的计算机在内存中跳得更多,而基于线性的容器却没有。

O(lg n)两者之间存在。我不记得是怎么回事。

性能差异不是很大,在较大的数据集上,基于哈希的数据集的性能要好得多。因此,我们坚持使用基于散列的无序映射。

实际上,对于合理大小的n,O(lg n)O(1)。如果您的计算机在表中仅可容纳40亿个条目,则该O(lg n)范围的上限为32。(lg(2 ^ 32)= 32)(在计算机科学中,lg是基于日志2的简写)。

实际上,lg(n)算法比O(1)算法要慢,这不是因为对数增长因子,而是因为lg(n)部分通常意味着算法存在一定程度的复杂性,并且该复杂性会增加大于lg(n)项中任何“增长”的常数因子。

但是,复杂的O(1)算法(例如哈希映射)可以很容易地具有相似或更大的常数因子。


21

并行执行算法的可能性。

我不知道是否有关于类O(log n)和的示例O(1),但是对于某些问题,当算法更易于并行执行时,您选择了具有较高复杂性类的算法。

有些算法无法并行化,但是复杂度很低。考虑另一种算法,它可以达到相同的结果,并且可以很容易地并行化,但是具有更高的复杂度等级。在一台机器上执行时,第二种算法较慢,但在多台机器上执行时,实际执行时间越来越短,而第一种算法无法加快速度。


但是并行化所做的只是减少其他人谈论的恒定因素,对吗?
gengkev

1
是的,但是每次将执行机器的数量加倍时,并行算法就可以将常数因子除以2。另一种单线程算法可以以一种恒定的方式仅一次减小恒定因子。因此,使用并行算法,您可以动态地响应n的大小,并缩短挂钟执行时间。
Simulant 2015年

15

假设您要在嵌入式系统上实现黑名单,其中可能会将0到1,000,000之间的数字列入黑名单。剩下两个可能的选择:

  1. 使用1,000,000位的位集
  2. 使用列入黑名单的整数的排序数组,并使用二进制搜索来访问它们

访问该位集将保证有恒定的访问权限。就时间复杂度而言,它是最佳的。从理论和实践角度来看(它都是O(1),常量开销都非常低)。

尽管如此,您可能仍希望使用第二种解决方案。尤其是如果您希望将列入黑名单的整数的数量很少,因为这样可以提高内存效率。

即使您不为内存不足的嵌入式系统而开发,我也可以将1,000,000的任意限制增加到1,000,000,000,000,并进行相同的论证。那么该位集将需要大约125G的内存。确保O(1)的最坏情况复杂性可能不会说服您的老板为您提供如此强大的服务器。

在这里,我非常希望使用二进制搜索(O(log n))或二进制树(O(log n)),而不是O(1)位集。而且,在最坏情况下复杂度为O(n)的哈希表在实践中可能会击败所有哈希表。



12

人们已经回答了您的确切问题,所以我将解决一个稍微不同的问题,人们在来到这里时可能会真正想到。

实际上,许多“ O(1)时间”算法和数据结构仅占用预期的 O(1)时间,这意味着它们的平均运行时间为O(1),可能仅在某些假设下。

常见示例:哈希表,“数组列表”的扩展(又名动态大小的数组/向量)。

在这种情况下,您可能更喜欢使用时间可以保证绝对对数限制的数据结构或算法,即使它们的平均性能可能较差。
因此,一个示例可能是平衡的二进制搜索树,其运行时间平均较差,但在最坏的情况下则更好。


11

一个更笼统的问题是,即使在趋向于无穷大的情况下,是否存在人们更喜欢O(f(n))算法而不是O(g(n))算法的情况。正如其他人已经提到的,在和的情况下,答案显然是“是” 。即使是多项式但是指数的情况,有时也可以。一个著名且重要的例子是用于解决线性规划问题的单纯形算法。在1970年代被证明是。因此,其最坏情况的行为是不可行的。但是,即使对于具有成千上万个变量和约束的实际问题,其平均情况下的行为也非常好。在1980年代,多项式时间算法(例如g(n) << f(n)nf(n) = log(n)g(n) = 1f(n)g(n)O(2^n)人们发现了用于线性规划的Karmarkar的内点算法(),但是30年后,单纯形算法似乎仍然是首选算法(除了某些非常大的问题)。这是因为显而易见的原因是平均情况下的行为通常比较差情况下的行为更重要,而且还出于更细微的原因,在某种意义上,单纯形算法更具参考价值(例如,更容易提取敏感度信息)。


10

投入2美分:

当算法在特定的硬件环境上运行时,有时会选择复杂度较差的算法来代替更好的算法。假设我们的O(1)算法非顺序访问非常大且固定大小的数组的每个元素来解决我们的问题。然后将该阵列放在机械硬盘驱动器或磁带上。

在这种情况下,O(logn)算法(假设它顺序访问磁盘)变得更受欢迎。


在这里我可能要补充一点,在顺序访问驱动器或磁带上,O(1)算法改为变为O(n),这就是为什么顺序解决方案变得更受欢迎的原因。许多O(1)操作都依赖于加和索引查找是一种恒定时间算法,它不在顺序访问空间中。
TheHansinator 2015年

9

有一个很好的用例,它使用O(log(n))算法代替O(1)算法,而其他许多答案都忽略了它:不变性。假设哈希值分布良好,则哈希映射具有O(1)放置和获取,但它们需要可变状态。不可变的树图具有O(log(n))放置和获取,这在渐近速度上较慢。但是,不变性可能足以弥补较差的性能,并且在需要保留地图的多个版本的情况下,不变性可让您避免复制地图(即O(n)),从而可以改善性能。


9

简而言之:因为系数(与设置,存储和该步骤的执行时间相关的成本)对于big-O较小的问题可能比较大的问题大得多。Big-O仅是算法可伸缩性的度量。

考虑一下《黑客词典》中的以下示例,提出了一种基于量子力学多重世界解释的排序算法:

  1. 使用量子过程随机排列阵列,
  2. 如果数组未排序,请销毁Universe。
  3. 现在将对所有剩余的Universe进行排序(包括您所在的Universe)。

(来源:http : //catb.org/~esr/jargon/html/B/bogo-sort.html

请注意,此算法的big-O为O(n),它击败了迄今为止在通用商品上所有已知的排序算法。线性步长的系数也非常低(因为它只是一个比较,不是交换,而是线性完成的)。实际上,可以使用类似的算法来解决多项式时间内NP共NP中的任何问题,因为可以使用量子过程生成每个可能的解(或没有解的可能证明),然后在多项式时间。

但是,在大多数情况下,我们可能不想冒多个世界可能不正确的风险,更不用说实施步骤2的行为仍然“留给读者练习”。


7

在任何时候,当n有界且O(1)算法的常数乘数高于log(n)的界。 例如,将值存储在哈希集中为O(1),但可能需要对哈希函数进行昂贵的计算。如果可以比较数据项(相对于某个顺序),并且对n的限制是log n显着小于任何一项的哈希计算,那么存储在平衡二叉树中可能比存储在二叉树中更快。哈希集。


6

在需要固定上限的实时情况下,您将选择(例如)堆排序而不是快速排序,因为堆排序的平均行为也是其最坏情况的行为。


6

再加上已经很好的答案。一个实际的例子是postgres数据库中的Hash索引与B-tree索引。

哈希索引形成哈希表索引以访问磁盘上的数据,而名称顾名思义的btree使用Btree数据结构。

在Big-O时间内,它们是O(1)vs O(logN)。

目前在postgres中不建议使用散列索引,因为在现实生活中,尤其是在数据库系统中,要实现无冲突的散列非常困难(可能导致O(N)最坏情况的复杂性),因此,制作起来更加困难它们崩溃安全(称为预写日志记录-postgres中的WAL)。

在这种情况下要进行权衡,因为O(logN)对于索引来说已经足够好了,而O(1)的实现非常困难,并且时间差实际上并不重要。



3
  1. 当O(1)中的“ 1”工作单位相对于O(log n)中的工作单位非常高且预期的设置大小很小时。例如,如果只有两个或三个项目,则计算Dictionary哈希码可能比迭代数组要慢。

要么

  1. 当O(1)算法中的内存或其他非时间资源需求相对于O(log n)算法而言异常大时。

3
  1. 重新设计程序时,发现使用O(1)而不是O(lgN)对过程进行了优化,但是如果这不是该程序的瓶颈,则很难理解O(1)alg。这样就不必使用O(1)算法
  2. 当O(1)需要大量您无法提供的内存时,可以接受O(lgN)的时间。

1

对于安全应用程序,通常会遇到这种情况,我们希望设计算法故意过慢的问题,以阻止某人过快地获得问题的答案。

这是我脑中浮现的几个例子。

  • 有时会使密码散列速度变慢,以使其更难于通过蛮力猜测密码。此信息安全帖子对此有一个项目符号要点(以及更多)。
  • 比特币对计算机网络使用可控的慢速问题来解决,以便“挖掘”硬币。这允许通过集体系统以受控的速率开采货币。
  • 非对称密码(如RSA)旨在在不故意使密钥变慢的情况下进行解密,以防止没有私钥的其他人破解加密。该算法被设计在希望被破解O(2^n),时间n是关键的比特长度(这是蛮力)。

在CS的其他地方,“快速排序” O(n^2)在最坏的情况下是,但在一般情况下是O(n*log(n))。因此,在分析算法效率时,您并不是唯一关心“ Big O”分析的人。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.