在任何情况下,您更喜欢较高的大 O 时间复杂度算法而不是较低的时间复杂度算法?
Posted
技术标签:
【中文标题】在任何情况下,您更喜欢较高的大 O 时间复杂度算法而不是较低的时间复杂度算法?【英文标题】:Are there any cases where you would prefer a higher big-O time complexity algorithm over the lower one? 【发布时间】:2016-03-14 19:04:05 【问题描述】:在某些情况下,您更喜欢 O(log n)
时间复杂度而不是 O(1)
时间复杂度?还是O(n)
转O(log n)
?
你有例子吗?
【问题讨论】:
如果理解前者而不是后者,我更喜欢O(log n)
算法而不是O(1)
算法......
理论计算机科学中有大量具有 O(1) 运算的不切实际的数据结构。一个例子是位向量上的 select(),它可以支持 o(n) 额外空间和 O(1) 每次操作,使用 5 层间接。根据Succinct Data Structure Library 的作者的说法,简单的二分搜索结合 O(1) rank() 在实践中会更快。
较低的渐近复杂度并不能保证更快的运行时间。研究一个具体例子的矩阵乘法。
另外...任何算法都可以转换为 O(1),只要有足够大的表查找 ;)
@Hoten -- 假设表查找为 O(1),对于您正在谈论的表的大小,这根本不是给定的! :)
【参考方案1】:
总是有隐藏常数,在 O(log n) 算法上可以更低。因此,它可以在实际数据中更快地工作。
还有空间问题(例如在烤面包机上运行)。
还有开发人员时间问题 - O(log n) 可能更容易实现和验证 1000 倍。
【讨论】:
很好,谢谢。我在想也值得考虑使用 O(logn) 算法来确保程序稳定性(例如在自平衡二叉树中) 我能想到的一个例子:对于一个小的排序数组,程序员实现一个二分查找函数比编写一个完整的哈希映射实现并使用它更容易和更紧凑. 一个复杂的例子:找到未排序列表的中位数在 O(n * log n) 中很容易做到,但在 O(n) 中很难做到。 -1,不要把原木放在你的烤面包机里……开玩笑的,这是正确的。lg n
如此,如此,如此接近 k
对于大型 n
,大多数操作都不会注意到差异。
还有一个事实是,大多数人熟悉的算法复杂性并没有考虑缓存效应。根据大多数人的说法,在二叉树中查找某些东西是 O(log2(n)),但实际上它更糟糕,因为二叉树的局部性很差。【参考方案2】:
在关注数据安全的情况下,如果更复杂的算法对timing attacks 具有更好的抵抗力,则更复杂的算法可能比不太复杂的算法更可取。
【讨论】:
虽然你说的是真的,但在这种情况下,在 O(1) 中执行的算法根据定义是不受时间攻击的。 @JustinLessard:O(1) 意味着存在一定的输入大小,之后算法的运行时间受常数限制。低于这个阈值会发生什么是未知的。此外,对于算法的任何实际使用,甚至可能无法满足阈值。例如,该算法可能是线性的,因此会泄漏有关输入长度的信息。 运行时间也可能以不同的方式波动,但仍受限制。如果运行时间与(n mod 5) + 1
成正比,它仍然是O(1)
,但会显示有关n
的信息。因此,运行时更平滑的更复杂算法可能更可取,即使它可能渐近(甚至在实践中)更慢。
这基本上就是为什么 bcrypt 被认为是好的;它使事情变慢
@DavidGrinberg 这就是使用 bcrypt 的原因,并且符合这个问题。但这与这个答案无关,后者谈论的是定时攻击。【参考方案3】:
我很惊讶还没有人提到内存受限的应用程序。
可能有一种算法由于其复杂性而具有较少的浮点运算(即 O(1) O(log n)) 或者因为复杂度前面的常数更小(即2n2n2 )。无论如何,如果较低的 FLOP 算法更受内存限制,您可能仍然更喜欢具有更多 FLOP 的算法。
我所说的“内存受限”是指您经常访问不断超出缓存的数据。为了获取此数据,您必须先将内存从实际内存空间中提取到缓存中,然后才能对其执行操作。这个获取步骤通常很慢 - 比您的操作本身慢得多。
因此,如果您的算法需要更多操作(但这些操作是对已经在缓存中的数据执行 [因此不需要提取]),它仍然会以更少的操作(必须在缓存外数据[因此需要获取])就实际挂起时间而言。
【讨论】:
Alistra 在谈到“空间问题”时间接解决了这个问题 大量缓存未命中只会将最终执行乘以一个恒定值(对于 4 核 3.2GHz CPU 和 1.6GHz 内存,它不大于 8,通常要低得多)所以它是在 big-O 表示法中算作固定常数。因此,缓存未命中的唯一原因是将 n 的阈值移动到 O(n) 解决方案开始比 O(1) 解决方案慢的位置。 @MarianSpanik 你当然是正确的。但是这个问题要求我们更喜欢O(logn)
而不是O(1)
。您可以很容易地想象这样一种情况:对于所有可行的n
,内存限制较少的应用程序将在更快的墙上运行时间,即使在更高的复杂性下也是如此。
@MarianSpanik 不是高达 300 个时钟周期的缓存未命中吗? 8 来自哪里?【参考方案4】:
我在Fast random weighted selection across all rows of a stochastic matrix 的回答是一个例子,当m
不太大时,复杂度为 O(m) 的算法比复杂度为 O(log(m)) 的算法快。
【讨论】:
【参考方案5】:考虑一棵红黑树。它可以访问、搜索、插入和删除O(log n)
。对比一个数组,访问权限为O(1)
,其余操作为O(n)
。
因此,如果应用程序插入、删除或搜索的次数比访问次数多,并且只能在这两种结构之间进行选择,我们更喜欢红黑树。在这种情况下,您可能会说我们更喜欢红黑树更繁琐的O(log n)
访问时间。
为什么?因为访问不是我们最关心的问题。我们正在做一个权衡:我们的应用程序的性能更受除此之外的因素的影响。我们允许这种特定算法的性能受到影响,因为我们通过优化其他算法获得了巨大的收益。
所以你的问题的答案很简单:当算法的增长率不是我们想要优化的时候,当我们想要优化其他东西。所有其他答案都是这种情况的特例。有时我们会优化其他操作的运行时间。有时我们会针对内存进行优化。有时我们会针对安全性进行优化。有时我们会优化可维护性。有时我们会优化开发时间。当您知道算法的增长率不是对运行时间的最大影响时,即使是压倒一切的常数足够低,也可以优化运行时间。 (如果你的数据集在这个范围之外,你会优化算法的增长率,因为它最终会支配常数。)一切都有成本,在很多情况下,我们用更高增长率的成本来换取算法来优化其他东西。
【讨论】:
不确定允许您使用具有 O(1) 查找和更新 O(n) 的数组的操作如何对应于红黑树,人们曾经考虑过(至少我是这样)。大多数时候,我首先会考虑基于键的红黑树查找。但是为了与数组匹配,它应该是一个稍微不同的结构,在上层节点中保留大量的子节点,以提供基于索引的查找和插入时的重新索引。虽然我同意红黑可以用来保持平衡,但如果你想对相应操作的细节模糊,你可以使用平衡树。 @ony 红黑树可用于定义映射/字典类型结构,但并非必须如此。节点可以只是元素,本质上实现了一个排序列表。 定义元素顺序的排序列表和数组具有不同的信息量。一种是基于元素和集合之间的顺序,另一种是定义任意序列,而不必定义元素之间的顺序。另一件事是您声明为“红黑树”的O(log n)
的“访问”和“搜索”是什么?在数组[1, 2, 1, 4]
的位置2 中插入5
将导致[1, 2, 5, 1 4]
(元素4
将索引从3 更新到4)。您将如何在您引用为“排序列表”的“红黑树”中的O(log n)
中获得这种行为?
@ony "定义元素顺序的排序列表和数组具有不同的信息量。"是的,这就是它们具有不同性能特征的部分原因。你没有抓住重点。一个并不是在所有情况下都可以替代另一个。他们优化不同的东西并做出不同的权衡,关键是开发人员不断地就这些权衡做出决策。
@ony 访问、搜索、插入和删除在算法性能的上下文中具有特定的含义。 Access 正在按位置获取元素。搜索是按值定位元素(仅作为非地图结构的包含检查具有任何实际应用)。不过,插入和删除应该很简单。用法示例可见here。【参考方案6】:
Alistra 成功了,但没有提供任何示例,所以我会提供。
您有一个包含 10,000 个 UPC 代码的列表,用于您的商店销售的商品。 10 位 UPC,整数表示价格(以美分表示的价格)和 30 位收据描述字符。
O(log N) 方法:您有一个排序列表。 ASCII 44 字节,Unicode 84 字节。或者,将 UPC 视为 int64 并获得 42 和 72 字节。 10,000 条记录——在最高情况下,您看到的存储空间略低于 1 MB。
O(1) 方法:不要存储 UPC,而是将其用作数组的条目。在最低的情况下,您会看到将近三分之一 TB 的存储空间。
您使用哪种方法取决于您的硬件。在大多数合理的现代配置中,您将使用 log N 方法。如果由于某种原因您在 RAM 非常短但您有大量大容量存储的环境中运行,我可以认为第二种方法是正确的答案。磁盘上 1/3 TB 没什么大不了的,将数据放入磁盘的一次探测中是值得的。简单的二进制方法平均需要 13 个。 (但是请注意,通过对密钥进行聚类,您可以将其降低到保证 3 次读取,并且实际上您会缓存第一个。)
【讨论】:
我在这里有点困惑。您是在谈论创建一个包含 100 亿个条目的数组(其中大部分是未定义的)并将 UPC 视为该数组的索引吗? @DavidZ 是的。如果您使用稀疏数组,您可能不会得到 O(1),但它只会使用 1MB 内存。如果您使用实际数组,则可以保证 O(1) 访问,但它将使用 1/3 TB 内存。 在现代系统上,它将使用 1/3 TB 的地址空间,但这并不意味着它会接近分配的那么多后备内存。大多数现代操作系统在需要之前不会提交存储分配。这样做时,您实际上是在操作系统/硬件虚拟内存系统中隐藏了数据的关联查找结构。 @Novelocrat 是的,但是如果您以 RAM 速度执行此操作,则查找时间无关紧要,没有理由使用 40mb 而不是 1mb。阵列版本仅在存储访问成本高昂时才有意义——您将使用磁盘。 或者当这不是一个对性能至关重要的操作,并且开发人员的时间很昂贵时 - 说malloc(search_space_size)
并订阅它返回的内容是很容易的。【参考方案7】:
并行执行算法的可能性。
不知道有没有O(log n)
和O(1)
这两个类的例子,但是对于一些问题,当算法更容易并行执行时,你会选择复杂度更高的算法。
有些算法不能并行化,但复杂度很低。考虑另一种算法,它可以实现相同的结果并且可以轻松并行化,但具有更高的复杂度等级。在一台机器上执行时,第二种算法速度较慢,但在多台机器上执行时,实际执行时间越来越短,而第一种算法无法加速。
【讨论】:
但是并行化所做的只是减少其他人所说的常数因子,对吧? 是的,但是每次执行机器的数量增加一倍时,并行算法可以将常数因子除以 2。另一种单线程算法可以以恒定的方式只减少一次常数因子。因此,使用并行算法,您可以动态响应 n 的大小,并在挂钟执行时间上更快。【参考方案8】:是的。
在一个真实的案例中,我们对使用短字符串和长字符串键进行表查找进行了一些测试。
我们使用了一个std::map
,一个std::unordered_map
,其哈希值最多采样超过字符串长度的 10 倍(我们的键往往类似于 guid,所以这很不错),以及一个采样的哈希值每个字符(理论上减少了冲突),一个未排序的向量,我们在其中进行==
比较,以及(如果我没记错的话)一个未排序的向量,我们还存储了一个哈希,首先比较哈希,然后比较字符。
这些算法的范围从O(1)
(无序映射)到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 是 log based 2 的简写)。
在实践中,lg(n) 算法比 O(1) 算法慢不是因为对数增长因子,而是因为 lg(n) 部分通常意味着算法存在一定程度的复杂性,并且与 lg(n) 项的任何“增长”相比,复杂性增加了一个更大的常数因子。
但是,复杂的 O(1) 算法(如哈希映射)很容易具有相似或更大的常数因子。
【讨论】:
【参考方案9】:一个更普遍的问题是,即使g(n) << f(n)
因为n
趋于无穷大,在某些情况下人们是否更喜欢O(f(n))
算法而不是O(g(n))
算法。正如其他人已经提到的那样,在f(n) = log(n)
和g(n) = 1
的情况下,答案显然是“是”。即使在f(n)
是多项式但g(n)
是指数的情况下,有时也是如此。一个著名且重要的例子是用于解决线性规划问题的Simplex Algorithm。在 1970 年代,它显示为 O(2^n)
。因此,它的最坏情况行为是不可行的。但是——它的平均情况行为非常好,即使对于具有数万个变量和约束的实际问题也是如此。在 1980 年代,发现了用于线性规划的多项式时间算法(例如 Karmarkar's interior-point algorithm),但 30 年后,单纯形算法似乎仍然是首选算法(某些非常大的问题除外)。这是因为一般情况下的行为通常比最坏情况下的行为更重要,但也有一个更微妙的原因,即单纯形算法在某种意义上信息量更大(例如,敏感性信息更容易提取)。
【讨论】:
【参考方案10】:假设您要在嵌入式系统上实施黑名单,其中 0 到 1,000,000 之间的数字可能会被列入黑名单。这留下了两个可能的选择:
-
使用 1,000,000 位的位集
使用黑名单整数的排序数组并使用二进制搜索来访问它们
对位集的访问将保证持续访问。就时间复杂度而言,它是最优的。从理论和实践的角度来看(它是 O(1),具有极低的常数开销)。
不过,您可能更喜欢第二种解决方案。特别是如果您希望列入黑名单的整数的数量非常少,因为这样会更节省内存。
即使您不为内存稀缺的嵌入式系统进行开发,我也可以将 1,000,000 的任意限制增加到 1,000,000,000,000 并提出相同的论点。那么 bitset 将需要大约 125G 的内存。保证 O(1) 的最坏情况复杂度可能无法说服您的老板为您提供如此强大的服务器。
在这里,我更喜欢二叉搜索 (O(log n)) 或二叉树 (O(log n)) 而不是 O(1) 位集。并且,在实践中,最坏情况复杂度为 O(n) 的哈希表可能会击败所有这些。
【讨论】:
【参考方案11】:使用 O(log(n)) 算法而不是许多其他答案都忽略的 O(1) 算法有一个很好的用例:不变性。散列图有 O(1) 的 put 和 get,假设散列值分布良好,但它们需要可变状态。不可变的树图有 O(log(n)) 的 put 和 get,渐近地变慢。然而,不变性可能足以弥补较差的性能,并且在需要保留多个版本的地图的情况下,不变性允许您避免复制地图,这是 O(n),因此可以 提高性能。
【讨论】:
【参考方案12】:人们已经回答了你的确切问题,所以我将解决一个稍微不同的问题,人们来到这里时可能会真正想到。
许多“O(1) 时间”的算法和数据结构实际上只需要 预期 O(1) 时间,这意味着它们的平均 运行时间为 O(1),可能仅在某些假设下。
常见示例: 哈希表、“数组列表”(也称为动态大小的数组/向量)的扩展。
在这种情况下,您可能更喜欢使用能够保证时间绝对以对数方式限制的数据结构或算法,即使它们的平均性能可能更差。 因此,一个示例可能是平衡二叉搜索树,它的平均运行时间更差,但在最坏的情况下更好。
【讨论】:
【参考方案13】:可能有很多理由更喜欢大 O 时间复杂度较高的算法而不是较低的算法:
大多数时候,较低的 big-O 复杂度更难实现,需要熟练的实施、大量知识和大量测试。 big-O 隐藏了有关常量的详细信息:从 big-O 的角度来看,在10^5
中执行的算法比 1/10^5 * log(n)
更好(O(1)
与 O(log(n)
),但对于大多数合理n
第一个会表现更好。例如,矩阵乘法的最佳复杂度是 O(n^2.373)
,但该常数非常高,以至于(据我所知)没有计算库使用它。
big-O 在计算大数据时很有意义。如果您需要对三个数字组成的数组进行排序,那么使用O(n*log(n))
还是O(n^2)
算法并不重要。
有时小写时间复杂度的优势可以忽略不计。对于example there is a data structure tango tree,它给出了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)
,比快速排序或归并排序差,但作为online algorithm,它可以有效地对接收到的值列表(作为用户输入)进行排序,而大多数其他算法只能对完整的值列表有效地操作。
【讨论】:
另外,我见过几次人们专注于他们的中心算法的大 O,但忽略了设置成本。例如,如果您不需要一遍又一遍地构建哈希表,则可能比线性遍历数组更昂贵。事实上,由于现代 CPU 的构建方式,即使是二进制搜索之类的东西在排序数组上的速度也可以与线性搜索一样快 - 分析是必要的。 @Luaan “事实上,由于现代 CPU 的构建方式,即使像二分搜索这样的东西在排序数组上的速度也可以与线性搜索一样快 - 分析是必要的。”有趣的!你能解释一下二分搜索和线性搜索如何在现代 CPU 上花费相同的时间吗? @Luaan - 没关系,我发现了这个:schani.wordpress.com/2010/04/30/linear-vs-binary-search @DenisdeBernardy:不,实际上没有。它们可能是 P 中的算法。即使这些不是,在对并行化意味着什么的合理定义下,这也不意味着 P!= NP。还要记住,搜索非确定性图灵机可能运行的空间是相当可并行的。【参考方案14】:把我的 2 美分放进去:
当算法在特定硬件环境上运行时,有时会选择更复杂的算法来代替更好的算法。假设我们的 O(1) 算法非顺序地访问一个非常大的、固定大小的数组的每个元素来解决我们的问题。然后将该阵列放在机械硬盘或磁带上。
在这种情况下,O(logn) 算法(假设它顺序访问磁盘)变得更有利。
【讨论】:
我可以在这里补充一点,在顺序存取驱动器或磁带上,O(1) 算法反而变成了 O(n),这就是顺序解决方案变得更有利的原因。许多 O(1) 操作依赖于添加和索引查找,这是一种恒定时间算法,它不在顺序访问空间中。【参考方案15】:在需要确定上限的实时情况下,您可以选择例如堆排序而不是快速排序,因为堆排序的平均行为也是其最坏情况的行为。
【讨论】:
【参考方案16】:在 n 有界且 O(1) 算法的常数乘数高于 log(n) 的界限时。 例如,将值存储在哈希集中是 O(1 ),但可能需要对哈希函数进行昂贵的计算。如果可以简单地比较数据项(关于某些顺序)并且 n 上的界限使得 log n 明显小于任何一项的哈希计算,那么存储在平衡二叉树中可能比存储在平衡二叉树中更快一个哈希集。
【讨论】:
【参考方案17】:简单地说:因为系数(与该步骤的设置、存储和执行时间相关的成本)在较小的 big-O 问题中比在较大的问题中可能要大得多。 Big-O 只是算法可扩展性的衡量标准。
考虑以下来自黑客词典的示例,它提出了一个依赖于Multiple Worlds Interpretation of Quantum Mechanics 的排序算法:
使用量子过程随机排列数组, 如果数组未排序,则销毁 Universe。 所有剩余的宇宙现在都已排序 [包括您所在的宇宙]。
(来源:http://catb.org/~esr/jargon/html/B/bogo-sort.html)
请注意,该算法的大 O 是 O(n)
,它击败了迄今为止对通用项目的任何已知排序算法。线性步骤的系数也非常低(因为它只是比较,而不是交换,是线性完成的)。事实上,类似的算法可以用于在多项式时间内解决NP 和co-NP 中的任何问题,因为可以使用量子过程生成每个可能的解决方案(或没有解决方案的可能证明),然后在多项式时间内验证。
但是,在大多数情况下,我们可能不想冒多重世界可能不正确的风险,更不用说实施步骤 2 的行为仍然“留给读者作为练习”。
【讨论】:
【参考方案18】:-
当 O(1) 中的“1”工作单元相对于 O(log n) 中的工作单元非常高并且预期的集合大小很小时。例如,如果只有两个或三个项目,计算 Dictionary 哈希码可能比迭代数组要慢。
或
-
当 O(1) 算法中的内存或其他非时间资源需求相对于 O(log n) 算法异常大时。
【讨论】:
【参考方案19】:添加到已经很好的答案。一个实际的例子是哈希索引与 postgres 数据库中的 B-tree 索引。
哈希索引形成一个哈希表索引来访问磁盘上的数据,而btree顾名思义使用的是Btree数据结构。
在 Big-O 时间中,这些是 O(1) 与 O(logN)。
目前不鼓励在 postgres 中使用哈希索引,因为在现实生活中,尤其是在数据库系统中,实现无冲突的哈希非常困难(可能导致 O(N) 最坏情况复杂度),因此,它甚至更更难使它们崩溃安全(称为预写日志记录 - postgres 中的 WAL)。
在这种情况下进行了权衡,因为 O(logN) 对索引来说已经足够好,而实现 O(1) 非常困难,而且时间差并不重要。
【讨论】:
【参考方案20】:当n
很小,而O(1)
一直很慢。
【讨论】:
【参考方案21】:-
在重新设计程序时,发现一个程序用 O(1) 而不是 O(lgN) 进行优化,但如果不是这个程序的瓶颈,很难理解 O(1) 算法。那么你就不必使用 O(1) 算法
当 O(1) 需要很多您无法提供的内存时,而 O(lgN) 的时间可以接受。
【讨论】:
【参考方案22】:对于我们想要设计算法缓慢的问题的安全应用程序来说,通常是这种情况,以阻止某人过快地获得问题的答案。
这里有几个我想不到的例子。
密码散列有时会变得任意缓慢,以使通过蛮力猜测密码变得更加困难。这个Information Security post 有一个关于它的要点(以及更多)。 Bit Coin 使用一个可控的慢速问题来解决计算机网络以“挖掘”硬币。这允许集体系统以可控的速度开采货币。 非对称密码(如RSA)旨在在没有密钥的情况下故意减慢解密速度,以防止没有私钥的其他人破解加密。这些算法被设计为有望在O(2^n)
时间被破解,其中n
是密钥的位长(这是蛮力)。
在 CS 的其他地方,快速排序在最坏的情况下是 O(n^2)
,但在一般情况下是 O(n*log(n))
。出于这个原因,“Big O”分析有时并不是您在分析算法效率时唯一关心的事情。
【讨论】:
以上是关于在任何情况下,您更喜欢较高的大 O 时间复杂度算法而不是较低的时间复杂度算法?的主要内容,如果未能解决你的问题,请参考以下文章