关于大O:有没有哪种情况下你会更喜欢大O时间复杂度算法而不是低O时间复杂度算法?

Are there any cases where you would prefer a higher big-O time complexity algorithm over the lower one?

有没有哪种情况下,您更喜欢O(log n)时间复杂度而不是O(1)时间复杂度?还是从O(n)O(log n)

你有什么例子吗?


与较低的时间复杂度相比,选择大o时间复杂度较高的算法有很多原因:

  • 大多数情况下,较低的大O复杂性更难实现,需要熟练的实现、大量的知识和大量的测试。
  • big-o隐藏了常数的细节:从big-o的角度来看,在10^5中执行的算法比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),比快速排序或合并排序更差,但作为一种在线算法,它可以在接收到值时(作为用户输入)对值列表进行有效排序,其中大多数其他算法只能对完整的值列表进行有效操作。


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

还有空间方面的问题(例如在烤面包机上运行)。

还有开发人员时间问题——O(log n)可能1000×更容易实现和验证。


我很惊讶还没有人提到内存绑定的应用程序。

可能有一种算法由于其复杂性(即o(1)

我所说的"内存绑定"是指您经常访问不断超出缓存的数据。为了获取这些数据,必须先将实际内存空间中的内存拉到缓存中,然后才能对其执行操作。这个提取步骤通常非常慢——比您的操作本身慢得多。

因此,如果您的算法需要更多的操作(但这些操作是对已经在缓存中的数据执行的[因此不需要提取]),那么就实际的壁时间而言,它仍然会以更少的操作(必须对缓存外的数据执行的操作[因此需要提取])来执行您的算法。


在数据安全性受到关注的情况下,如果更复杂的算法具有更好的抗定时攻击能力,则更复杂的算法可能比不复杂的算法更好。


阿利斯特拉说得很清楚,但没有提供任何例子,所以我会的。

你有一张10000个UPC代码的清单,上面列出了你的商店销售的商品。10位UPC,整数表示价格(单位为便士),30个字符表示收据说明。

O(log n)方法:您有一个排序列表。如果是ASCII,则为44字节;如果是Unicode,则为84字节。或者,将upc视为Int64,得到42&72字节。10000条记录——在最高的情况下,您看到的是一个兆字节的存储空间。

o(1)方法:不要存储upc,而是将其用作数组的条目。在最低的情况下,您将看到几乎三分之一兆字节的存储空间。

您使用哪种方法取决于您的硬件。在大多数合理的现代配置中,您将使用log n方法。如果出于某种原因,您运行的环境中RAM非常短,但您有大量的存储空间,那么我可以想象第二种方法是正确的答案。磁盘上三分之一兆字节并不重要,在磁盘的一个探针中获取数据是值得的。简单的二进制方法平均需要13个。(但是,请注意,通过集群化您的密钥,您可以将其降至保证的3次读取,实际上,您可以缓存第一次读取。)


考虑一棵红黑相间的树。它可以访问、搜索、插入和删除O(log n)。与可以访问O(1)的数组相比,其余的操作是O(n)

因此,如果应用程序的插入、删除或搜索频率高于访问频率,并且只在这两种结构之间进行选择,那么我们更喜欢红黑树。在这种情况下,您可能会说我们更喜欢红黑树的更繁琐的O(log n)访问时间。

为什么?因为访问不是我们最关心的问题。我们正在权衡:我们的应用程序的性能受除此之外的其他因素的影响更大。我们允许这个特定的算法受到性能的影响,因为我们通过优化其他算法获得了很大的收益。

所以你的问题的答案很简单:当算法的增长率不是我们想要优化的,当我们想要优化其他东西的时候。所有其他答案都是这方面的特例。有时我们会优化其他操作的运行时间。有时我们会优化记忆。有时我们会优化安全性。有时我们优化可维护性。有时我们会优化开发时间。当您知道算法的增长率对运行时间的影响不是最大的时候,即使覆盖常数足够低,也需要对运行时间进行优化。(如果您的数据集超出此范围,您将针对算法的增长率进行优化,因为它最终将主导常量。)每件事都有成本,在许多情况下,我们将较高增长率的成本与算法进行权衡,以优化其他内容。


对。

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

我们使用了一个std::map、一个std::unordered_map和一个哈希(哈希在字符串长度上最多采样10倍)(我们的键趋向于guid类型,所以这是很好的),以及一个哈希(在理论上减少了冲突),一个未排序的向量(在这里我们进行==比较),以及(如果我记得正确的话)一个未排序的向量,其中we还存储一个哈希,首先比较哈希,然后比较字符。

这些算法的范围从EDOCX1(无序映射)到EDOCX1(线性搜索)。

对于中等大小的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)算法(如散列映射)很容易具有类似或更大的常量因子。


并行执行算法的可能性。

我不知道类O(log n)O(1)是否有一个例子,但是对于一些问题,当算法更容易并行执行时,您可以选择一个复杂度更高的类。

有些算法不能并行化,但复杂度很低。考虑另一种算法,它可以获得相同的结果,并且易于并行化,但具有更高的复杂性。当在一台机器上执行时,第二个算法会变慢,但当在多台机器上执行时,实际执行时间会越来越短,而第一个算法无法加速。


假设您正在嵌入式系统上实现黑名单,其中0到1000000之间的数字可能被列入黑名单。这给你两个可能的选择:

  • 使用1000000位的位集
  • 使用已排序的黑名单整数数组并使用二进制搜索访问它们
  • 对位集的访问将保证恒定访问。就时间复杂性而言,它是最优的。无论从理论上还是从实际的角度来看(它是O(1),具有非常低的恒定开销)。

    不过,您可能希望使用第二种解决方案。特别是如果您希望黑名单整数的数量非常小,因为它将更节省内存。

    即使您不为内存不足的嵌入式系统开发,我也可以增加1000000到10000000000的任意限制,并提出相同的论点。然后位集需要大约125g内存。有了O(1)最坏情况下的复杂性保证可能无法说服您的老板为您提供如此强大的服务器。

    在这里,比起O(1)位集,我更喜欢二进制搜索(O(logn))或二进制树(O(logn))。而且,一个最坏情况下复杂度为O(n)的哈希表在实践中可能会击败所有哈希表。


    这里我的答案是快速随机加权选择随机矩阵的所有行是一个例子,当m不太大时,复杂度为o(m)的算法比复杂度为o(log(m))的算法更快。


    人们已经回答了你的确切问题,所以我将处理一个稍有不同的问题,当人们来到这里时,他们可能会真正想到这个问题。

    许多"O(1)时间"算法和数据结构实际上只需要预期的O(1)时间,这意味着它们的平均运行时间是O(1),可能只有在某些假设下。

    常见示例:哈希表,"数组列表"的扩展(也就是说,动态调整数组/向量大小)。

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


    一个更普遍的问题是,在某些情况下,即使g(n) << f(n)作为n趋向于无穷大,人们还是更喜欢O(f(n))算法而不是O(g(n))算法。正如其他人已经提到的,在f(n) = log(n)g(n) = 1的情况下,答案显然是"是"。有时是的,即使在f(n)是多项式,但g(n)是指数的情况下。一个著名而重要的例子是求解线性规划问题的单纯形算法。在20世纪70年代,它被证明是O(2^n)。因此,更糟的情况是不可行的。但是——它的平均情况行为是非常好的,即使对于具有数万个变量和约束的实际问题也是如此。在20世纪80年代,人们发现了线性规划的多项式时间算法(如Karmarkar的内点算法),但30年后,simplex算法似乎仍然是首选算法(除了某些非常大的问题)。这是明显的原因,平均事例行为通常比糟糕的事例行为更重要,但也有一个更微妙的原因,即单纯形算法在某种意义上更具信息性(例如,敏感性信息更容易提取)。


    把我的2美分放进去:

    有时,当算法在某个硬件环境中运行时,会选择更复杂的算法来代替更好的算法。假设我们的O(1)算法不按顺序访问一个非常大的固定大小数组的每个元素来解决我们的问题。然后把这个阵列放在机械硬盘或磁带上。

    在这种情况下,O(logn)算法(假设它按顺序访问磁盘)变得更加有利。


    简单地说:因为系数——与设置、存储以及该步骤的执行时间相关的成本——在一个较小的大O问题上可能比在一个较大的问题上大得多。big-o只是算法可伸缩性的一个度量。

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

  • Permute the array randomly using a quantum process,
  • If the array is not sorted, destroy the universe.
  • All remaining universes are now sorted [including the one you are in].
  • (来源:http://catb.org/~esr/jargon/html/b/bogo sort.html)

    请注意,该算法的大O是O(n),它比任何已知的排序算法都要好。线性阶跃的系数也很低(因为它只是一个比较,而不是线性交换)。事实上,类似的算法可以在多项式时间内解决np和co-np中的任何问题,因为每个可能的解(或可能的证明没有解)都可以使用量子过程生成,然后在多项式时间内进行验证。

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


    对于使用O(log(n))算法而不是许多其他答案都忽略的O(1)算法来说,有一个很好的用例:不变性。散列映射有O(1)个PUT和GET,假设散列值分布良好,但它们需要可变状态。不可变树映射有O(log(n))放置和获取,这是渐进地慢。但是,不变性可能非常有价值,足以弥补更差的性能,在需要保留多个版本的映射的情况下,不变性允许您避免复制映射,即O(N),因此可以提高性能。


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


    在需要公司上限的实时情况下,您可以选择Heapsort,而不是Quicksort,因为Heapsort的平均行为也是其最坏的行为。


    一个实用的例子是哈希索引和Postgres数据库中的B树索引。

    哈希索引形成一个哈希表索引来访问磁盘上的数据,而btree(如名称所示)使用btree数据结构。

    在大O时期,这是O(1)对O(logn)。

    目前,Postgres不鼓励使用哈希索引,因为在现实生活中,尤其是在数据库系统中,实现无冲突哈希非常困难(可能导致最坏情况下的O(N)复杂性),因此,更难保证它们的崩溃安全(在Postgres中称为提前写入日志-wal)。

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


    n很小,O(1)很慢时。


  • 当o(1)中的"1"工作单元相对于o(log n)中的工作单元非常高,并且预期的集合大小很小时。例如,如果只有两个或三个项,计算字典哈希代码可能比迭代数组慢。
  • 当O(1)算法中的内存或其他非时间资源需求相对于O(logn)算法特别大时。

  • 当重新设计一个程序时,发现一个程序是用O(1)而不是O(lgn)来优化的,但是如果它不是这个程序的瓶颈,并且很难理解O(1)算法。那么你就不用使用O(1)算法了
  • 当O(1)需要很多您无法提供的内存时,而O(lgn)的时间可以被接受。

  • 对于安全应用程序来说,这种情况通常是这样的,我们希望设计一些算法缓慢的问题,以便阻止某人快速获得问题的答案。

    下面是一些我头脑中的例子。

    • 密码散列有时会任意变慢,以便更难用暴力猜测密码。这个信息安全站有一个要点(以及更多)。
    • 比特币使用一个可控的慢问题来解决计算机网络,以便"挖掘"硬币。这使得货币可以由集体系统以可控的速度开采。
    • 非对称密码(如RSA)的设计目的是为了在没有密钥的情况下进行解密,从而防止没有私钥的其他人破解加密。这些算法被设计成在希望的O(2^n)时间内被破解,其中n是密钥的位长度(这是蛮力)。

    在CS中,最坏的情况下,快速排序是O(n^2),但一般情况下是O(n*log(n))。因此,在分析算法效率时,"大O"分析有时不是你唯一关心的事情。