“对于较小的n值,可以将O(n)视为O(1)”


26

我已经听过好几次了,对于足够小的n值,可以将O(n)视为/视为O(1)。

范例

这样做的动机是基于这样一个错误的观念:O(1)总是比O(lg n)更好,总是比O(n)更好。仅当在实际条件下问题的大小实际变大时,运算的渐近顺序才有意义。如果n保持很小,那么每个问题都是O(1)!

什么足够小?10个?100?一千 您在什么时候说“我们不能再将其视为免费手术了”?有经验法则吗?

这似乎可能是特定于域或特定于案例的,但是关于如何考虑这一点是否有一般的经验法则呢?


4
经验法则取决于您要解决的问题。快与嵌入式系统n100?发表复杂性理论?
拉斐尔

3
再多考虑一下,感觉根本不可能凭一条经验法则,因为性能要求取决于您的域及其业务要求。在非资源受限的环境中,n可能会很大。在严重受限的环境中,它可能很小。事后看来,这似乎显而易见。
rianjs 2014年

12
@rianjs您似乎免费误会O(1)了。前几句话背后的原因是常量,有时可能会很慢。无论输入如何,耗时一万亿年的计算都是一种计算。O(1)O(1)
Mooing Duck

1
有关为什么我们首先使用渐近线的相关问题
拉斐尔

3
@rianjs:注意“五角形大约是一个圆,对于足够大的5值”这样的笑话。您要询问的句子很有意义,但是由于它引起了您的一些困惑,因此您可能会想问埃里克·利珀特(Eric Lippert),这种确切的措词选择在多大程度上起到了幽默的作用。他本可以说:“如果上有任何上限,n那么每个问题都是O(1 ”,并且在数学上还是正确的。“小”不是数学的一部分。
史蒂夫·杰索普

Answers:


21

所有数量级都涉及常数,实际上其中有几个。当项目数足够大时,常数就无关紧要了。问题是项目的数量是否足够小,以至于该常数不能成为主导。C

这是一种思考的视觉方式。

在此处输入图片说明

它们都有一个启动常数,该常数确定它们在Y轴上的起点。每个参数都有一个临界常数它们将增加多快。C

  • 对于C确定时间。O(1)C
  • 实际上是 C × n,其中 C决定角度。O(n)C×nC
  • 实际上是C × n 2,其中 C确定曲线的清晰度。O(n2)(C×n)2C

为了确定应该使用哪种算法,您需要估计运行时相交的位置。例如,启动时间较长或C较高的解决方案将在相当多的项目上输给启动时间较短且C较低的O n 解决方案。O(1)CO(n)C

这是一个真实的例子。您必须在院子里移动一堆砖头。您可以用手一次移动它们几下,也可以去找一头巨大的,缓慢的反铲,将它们抬起并推动一次。如果有三块砖,您的答案是什么?如果有三千,你的答案是什么?

这是一个CS示例。假设您需要一个始终排序的列表。您可以使用一棵树,该树将保持自身的顺序为。或者,您可以在O n log n每次插入或删除后使用未排序的列表并重新排序。因为树操作很复杂(它们有一个高常数),排序很简单(低常数),所以列表很可能会赢得成百上千个项目。O(logn)O(nlogn)

您可以关注这种事情,但最终基准是要做什么。您还必须考虑一下通常将要拥有的物品数量,并减轻被多交付的风险。您还需要记录您的假设,例如“性能会在项目上迅速下降”或“我们假设最大设置大小为X ”。XX

由于这些要求可能会发生变化,因此将此类决策置于界面后面很重要。在上面的树/列表示例中,请勿公开树或列表。这样,如果您的假设被证明是错误的,或者您找到了更好的算法,则可以改变主意。您甚至可以进行混合操作,并随着项目数量的增加动态地切换算法。


是没有意义的。你真正的意思是说,如果运行时间牛逼= Ô 1 ,然后(在许多情况下)牛逼Ç。如果Ť = Ö Ñ 然后在许多情况下Ť Ç Ñ,或更正式Ť = Ç Ñ + Ö Ñ 。等等。但是请注意,在其他情况下,常数CO(1)=O(C)T=O(1)TCT=O(n)TCnT=Cn+o(n)C,在一定范围内。n
Yuval Filmus 2014年

@YuvalFilmus这就是为什么我喜欢图表。
Schwern 2014年

到目前为止,这是最好的答案,关键是功能如何快速发展。
里卡多

1
不错的图形,但轴确实应该标记为“时间”,而不是“速度”。ÿ
Ilmari Karonen 2014年

1
行真是一个抛物线,在那里?小n看起来很平坦,大n看起来很陡。O(n2)nn
David Richerby 2014年

44

这很大程度上是基于已经发布的答案,但可能会提供不同的观点。

揭示该问题讨论了“足够小的n值”。Big-O的全部要点是描述处理如何根据所处理的内容而增长。如果要处理的数据很小,则讨论Big-O无关紧要,因为您对增长不感兴趣(这没有发生)。

换句话说,如果您要沿着街道走很短的距离,走路,骑自行车或开车可能同样快。如果需要一段时间才能找到您的车钥匙,或者您的车需要加油等,走路甚至可能会更快。

对于小n,请使用任何方便的方法。

如果您要进行越野旅行,则需要研究优化驾驶,油耗等的方法。


5
“对于小n,请使用任何方便的方式。” -如果您经常执行操作,请选择最快的(对于您的)。另请参阅此处n
拉斐尔

4
伟大的隐喻!
Evorlor

1
从纯粹的数学观点来看,渐近复杂性什么时候都不会告诉您n < infinity
戈登·古斯塔夫森

15

报价含糊不清。至少有三种相关的方式可以解释它。

它背后的字面数学意义是,如果您仅对大小达到某个限制的实例感兴趣,那么只有有限的许多实例。例如,最多一百个顶点上只有有限的多个图。如果只有有限数量的实例,则原则上您可以通过仅构建所有可能实例的所有答案的查找表来解决问题。现在,您可以通过首先检查输入是否太大来找到答案(这需要固定时间:如果输入长于  k,则无效),然后在表格中查找答案(这需要花费固定的时间:表格中的条目数是固定的)。但是请注意,表的实际大小可能太大。我说过在一百个顶点上只有有限数量的图,这是事实。只是有限数量大于可观察宇宙中的原子数量。

更为实际的一点是,当我们说算法的运行时间为,这仅意味着Θ(n2)  对于某些常数C渐近步  。也就是说,有一些恒定Ñ 0,使得对于所有Ñ Ñ 0,则该算法需要大约Ç Ñ 2步骤。但也许ñ 0 = 100 000 000cn2Cn0nn0cn2n0=100,000,000而您只对尺寸远小于此尺寸的实例感兴趣。渐近二次边界甚至可能不适用于您的小实例。您可能很幸运,在输入较小的内容时可能会更快(或者可能很不幸,但输入速度会更慢)。例如,对于小  n 2 < 1000 n,因此您宁愿运行具有良好常数的二次算法,而不是具有不良常数的线性算法。一个现实的例子是,由于Strassen的O,实际上很少使用渐近最有效的矩阵乘法算法(Coppersmith–Winograd的变体,在时间O n 2.3729)上运行)。nn2<1000nO(n2.3729) 算法会更快,除非您的矩阵很大。O(n2.8074)

第三点是,如果  小,则n 2甚至n 3  小。例如,如果您需要对几千个数据项进行排序,而只需要对它们排序一次,那么任何排序算法都已经足够好:Θ n 2nn2n3Θ(n2)该算法仍然只需要几千万条指令来对数据进行排序,而在每秒能执行数十亿条指令的CPU上,这根本就不花很多时间。好的,也有内存访问,但是即使是慢速算法也将花费不到一秒钟的时间,因此使用简单,慢速算法并正确处理它可能比使用复杂,快速的算法快如闪电但存在错误,实际上并不能正确地对数据进行排序。


4
虽然观点正确无误,但我认为您错过了这一点。看来,这意味着有时对于足够小的n s ,使用的算法的性能要优于使用O 1 的算法。发生这种情况,例如当前者的运行时间为10 n + 50时O(n)O(1)n10n+50,而后者的运行时间为。然后,对于足够小的n,实际上使用O n 协议会更快。100000nO(n)
Ran G.

@RanG。那不是我的第二种情况吗?(特别是如果我编辑它时说的更像是“具有良好常数的线性算法可能会击败具有不良常数的常数/对数算法”?)
David Richerby 2014年

1
n较小时,最好明确提及常数的重要性。以前从未听说过的人可能不会发生。
罗伯·瓦茨

9

Big-O表示法实际上仅说明了任意大n的行为。例如,表示存在常数c> 0和整数f(n)=O(n2),使得对于每 n > n 0 f n < c n 2n0f(n)<cn2n>n0

在许多情况下,您可以找到一个常数c并说“对于每n> 0,f(n)大约为 ”。有哪些有用的信息。但是在某些情况下,情况并非如此。如果f(n)= n 2 + 10 18,那么这完全是误导。因此,仅因为某些事物为O(n ^ 2)并不意味着您可以关闭大脑并忽略实际功能。cn2n2+1018

另一方面,如果您只遇到过值n = 1、2和3,则在实践中,对于n≥4,f(n)不会产生什么影响,因此您最好考虑f( n)= O(1),c = max(f(1),f(2),f(3))。这就是足够小的意思:如果f(n)= O(1)的主张不会误导您,而您遇到的f(n)的唯一值是“足够小”。


5

如果不增长,则为O(1)

作者的说法有点公理。

增长顺序描述了随着N增长您必须完成的工作量将会发生什么。如果您知道那N不会增加,那么您的问题就有效了O(1)

请记住,O(1)这并不意味着“快速”。始终需要1万亿步才能完成的算法是O(1)。该算法需要1-200个步骤,但绝不超过O(1)。[1]

如果你的算法到底需要N ^ 3几步之遥,你知道N不能超过5个,它永远不能超过125步,所以它是有效的O(1)

但同样,O(1)不一定意味着“足够快”。那是一个独立的问题,取决于您的上下文。如果要花一周的时间来完成某件事,那么您可能不在乎它是否是技术上的O(1)


[1]例如,O(1)即使哈希冲突意味着您可能必须浏览一个存储桶中的多个项目,但在该存储桶中可以有多少个项目存在硬性限制的情况下,在哈希中的查找仍为。


1
听起来所有这些都是有效的,除了:“如果您的算法恰好需要N ^ 3个步骤,并且您知道N不能超过5个步骤,那么它绝不能超过125个步骤,所以它是O(1)。” 。同样,如果算法采用整数,而我的最大整数支持为32767,它是否为O(1)?明显不是。Big-O不会根据参数限制进行更改。即使您知道 0 <n <3 也是O(n),因为n = 2花费的时间是n = 1的两倍。
JSobell 2014年

3
@JSobell但这是O(1)。如果存在限制您的n限制f(n),则表示它不能无限期地增长。如果您的n受2 ^ 15限制,则您的n ^ 2函数实际上是g(n) = min(f(2^15), f(n))-在O(1)中。也就是说,在实践中,常数确实非常重要,并且显然n可以变得足够大,以至于渐进分析是有用的。
Voo

2
@JSobell这类似于以下问题:计算机在技术上不能具有无限的存储空间,因此它们是否真的是“完整的”。从技术上讲,计算机不是一台“真正的”图灵机。实际上,没有“无限磁带”之类的东西,但是硬盘驱动器足够接近。
凯尔·斯特兰德

几年前,我写了一个涉及n ^ 5矩阵操作的金融风险系统,因此在资源成为问题之前,实际限制为n = 20。
JSobell 2014年

抱歉,太早按下Enter键。几年前,我写了一个涉及n ^ 5矩阵操作的金融风险系统,因此在资源成为问题之前,实际限制为n = 20。根据这个有缺陷的逻辑,因为我的界线为20,所以创建的函数为O(1)。当客户端说“嗯,也许我们应该将其移动到40作为极限...是的,算法是O(1 ),这样就没问题了。。。这就是为什么输入的边界没有意义的原因。函数是O(n ^ 5),而不是O(1),这是为什么Big-O独立于边界的一个实际示例。
JSobell 2014年

2

现在,我可以使用哈希表,并进行O(1)查找(不考虑哈希表的具体实现),但是如果我拥有例如列表,则可以进行O(n)查找。给定这个公理,如果集合足够小,则这两个是相同的。但是在某些时候他们分歧...那是什么意思?

实际上,在这一点上,构建哈希表所产生的收益要比您从改进的查找中获得的收益更多。这将基于如何有很大的差异往往你正在做的查找,与你做其他事情多久。如果执行一次,O(1)vs O(10)并不重要。如果您每秒执行数千次操作,即使如此也很重要(尽管至少以线性增长的速度很重要)。


如果您想确定的话,请做一些实验,看看哪种数据结构更适合您的参数。
Yuval Filmus 2014年

@Telastyn Yuval Filmus是对的,如果您真的想确定的话。我知道一个人叫吉姆,他的参数还可以。但是他没有听尤法的建议。您应该真正听Yuval来确保安全。
通知2014年

2

虽然报价是正确的(但含糊),但也存在危险。另外,您应该在应用程序的任何阶段着眼于复杂性。

太容易说了:嘿,我只有一个小列表,如果我想检查项目A是否在列表中,我将编写一个简单的循环遍历该列表并比较这些项目。

然后,您的buddyprogrammer就会需要使用列表,看到您的功能,就像:嘿,我不想列表中有任何重复项,因此他将函数用于添加到列表中的每个项目。

(请注意,这仍然是一个小清单。)

三年后,我走了过来,老板也大赚了一笔:我们的软件将由一家大型的全国性零售商使用。在我们只为小商店服务之前。现在我的老板骂我,大声疾呼,为什么一直“运行良好”的软件现在是如此缓慢。

原来,该列表是客户列表,而我们的客户大概只有100个客户,所以没人注意到。填充列表的操作基本上是O(1)操作,因为它花费的时间不到一毫秒。好吧,当要添加10.000个客户端时,它的数量就不多了。

在最初做出糟糕的O(1)决定后数年,该公司几乎失去了一个大客户。都是由于几年前的一个小设计/假设错误。


但这也说明了许多现实世界系统的重要特征:作为本科生学习的“算法”实际上是构成真正的“算法”的部分。这通常是暗示的;例如,大多数人都知道,当分区变得足够小时,快速排序通常被编写为回退为插入排序,而二进制搜索通常被编写为回退为线性搜索。但是没有多少人意识到合并排序可以从某种二进制搜索中受益。
别名2014年

1

这样做的动机是基于这样一个错误的观念:O(1)总是比O(lg n)更好,总是比O(n)更好。仅当在实际条件下问题的大小实际变大时,运算的渐近顺序才有意义。

如果我现在有两种算法:

  • log(n)+10000
  • n + 1

然后存在它们交叉的某个点。对于n小于中,“线性”算法更快,并且n比大时,“对数”算法更快。许多人错误地认为对数算法更快,但对于小算法n则不是。

如果n保持很小,那么每个问题都是O(1)!

推测这里的意思是,如果n受限,那么每个问题都是O(1)。例如,如果要对整数排序,则可以选择使用快速排序。 O(n*log(n))明显。但是,如果我们决定不能超过2^64=1.8446744e+19整数,那么我们知道n*log(n)<= 1.8446744e+19*log(1.8446744e+19)<= 1.1805916e+21。因此,该算法将始终花费少于1.1805916e+21“时间单位”的时间。由于这是一个恒定的时间,我们可以说算法始终可以在恒定的时间->中完成O(1)。(请注意,即使这些时间单位为纳秒,也总计超过37411年)。但是还是O(1)


0

我怀疑其中许多答案都缺少一个基本概念。O(1):O(n)与f(1):f(n)不同,其中f是相同的函数,因为O不代表单个函数。甚至Schwern的漂亮图形也是无效的,因为它的所有线条都具有相同的Y轴。要使所有轴都使用相同的轴,线必须是fn1,fn2和fn3,其中每条线都是一个函数,其性能可以直接与其他函数进行比较。

我听过好几次了,对于足够小的n值,可以将O(n)视为/视为O(1)

好吧,如果n = 1,它们是完全一样的吗?不需要。允许可变迭代次数的函数与那些不需要的函数没有什么共同之处,big-O表示无关,我们也不应该。

Big-O表示法只是用来表示当我们进行迭代过程时会发生什么,以及性能(时间或资源)如何随着“ n”的增加而降低。

因此,要回答实际问题……我想说的是,那些主张的人不能正确理解Big-O表示法,因为这是不合逻辑的比较。

这是一个类似的问题:如果我遍历一个字符串,并且我知道我的字符串通常少于10个字符,我可以说它等于O(1),但是如果我的字符串更长,那么我会说是O(n)吗?

否,因为10个字符的字符串所花的时间是1个字符的字符串的10倍,但比1000个字符的字符串要少100倍!是O(n)。


实际上,如果您知道输入的字符数最多为10个,则可以编写一个Ø1个算法。首先,检查最多10个字符;如果没有,您可以拒绝,而无需查看其余部分。现在,如果需要F一世 处理输入的步骤 一世 个字符,您剩余的运行时间由 最高{F0F10}。那是一个常数Ø1个
David Richerby 2014年

是的,这是一个通常会误解Big-O表示法的示例。根据您的论点,如果我知道n的最大值是1,000,000,那么我的函数就是O(1)。实际上,我的函数可能最好为O(1),最坏为O(n)。这种表示法用于描述算法的复杂性,而不是具体的实现,并且我们总是使用最昂贵的方法来描述场景,而不是最好的情况。实际上,根据您的说法,每个允许n <2的函数都是O(1)!:)
JSobell 2014年

不可以。Big-O表示法用于描述功能的增长率。这些功能可以用来测量所有东西。而且我最肯定也没有争辩说,“ñ<2 [这意味着]是 Ø1个。”事实上,我认为任何具有以下特性的函数 FñF10 对所有人 ñØ1个,这是真的。
David Richerby 2014年

抱歉,但是如果您说知道n的上限会产生一个函数O(1),那么您是说符号表示形式与n的值直接相关,而与n的值无关。您提到的其他所有内容都是正确的,但暗示由于n有界,所以O(1)是不正确的。实际上,在某些地方您的描述可能是可观察到的,但是我们在这里使用的是Big-O表示法,而不是功能编码。再说一遍,为什么您建议n的最大值为10使其变为O(1)?为什么是10?为什么不65535或2 ^ 64?
JSobell 2014年

话虽如此,如果您编写一个将字符串
填充

0

我相信您所引用的文字是不准确的(除非您提供上下文:使用时间,空间等,否则使用“更好”一词通常是没有意义的)无论如何,我相信最简单的解释是:

如果执行时间随着输入的大小而增长,那肯定不是 Ø1个 这应该很清楚。 Ø1个并不意味着。仅仅意味着(就时间复杂度而言)执行时间具有恒定的上限

现在,让我们以相对较小的10个元素为一组,并使用一些算法对其进行排序(仅作为示例)。假设我们将元素保留在一个结构中,该结构还为我们提供了能够在恒定时间内对元素进行排序的算法。假设我们的排序算法可以具有以下复杂性(使用big-O表示法):

  1. Ø1个
  2. Øñ
  3. ØñØGñ
  4. Øñ2

您会选择哪种算法?我想到的第一个答案可能是“我当然会使用Ø1个一个!”,但这不一定正确。当您这样想时,您会忘记的是big-O符号隐藏了常数因子。如果您知道集合很小,那么这个常数因子可能会很大比数量更为重要渐进复杂性。

现在,让我们“揭示”上述排序算法的真正复杂性(其中“ true”表示不隐藏常量),以完成所需的步骤数表示(并假设所有步骤花费相同的时间):

  1. 200 脚步
  2. 11ñ 脚步
  3. 4ñØGñ 步骤(以2为底的对数)
  4. 1个ñ2 脚步

如果我们输入的大小为10,则这些是上述每种算法的准确步数:

  1. 200 脚步
  2. 11×10=110 脚步
  3. 4×10×3.32134 脚步
  4. 1个×100=100 脚步

如您所见,在这种情况下,最糟糕的算法具有渐近复杂性 Øñ2 是最快的,击败算法 Ø1个ØñØñØGñ渐进的复杂性。big-O标记隐藏的常数在这里很重要。我认为这并不意味着我们可以治疗Øñ2 优于 Ø1个 (这到底意味着什么?)这意味着对于足够小的输入(如您在示例中所看到的), Øñ2 可能仍然比 Ø1个因为隐藏的常数。而且,如果常量与输入的大小相比相对较大,那么它可能比渐近复杂性更为重要。

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.