最近,我和我的一位同事就超级简单算法的运行时复杂性进行了非常非常激烈的辩论。最后,我们俩都同意不同意,但是由于我一直在思考,这挑战了我对计算机科学基础知识的基本理解,因此,我必须对此事有更多的了解。
给定以下python,Big-O运行时的复杂性是什么:
1
2for c in"How are you today?":
print c
现在,我立即指出这只是O(n)线性的量级。这意味着它取决于字符串的长度,因此,此循环将随着字符串的长度线性增长。
然后我的同事说:"不,它是常量,因为我们知道(在我们的情况下)对于要处理的所有字符串集,最大字符串始终为255个字符长,因此它必须是常量。"他接着说:"因为我们在字符串的字符长度上具有最大上限,所以这导致O(255)减少为O(1)。"
无论如何,我们又来回了第四,在我们两个人都画了45分钟的草图之后,我们都陷入了僵局。
我的问题是在恒定时间循环之上的循环是哪个世界或什么数学系统?如果我们知道上限是说1,000,000个字符,并且所有字符串的集合都可以在0到1,000,000之间,则该循环显然将显示线性运行时间,具体取决于字符串的大小。
我还问他是否还认为以下代码是O(1)如果n的上限大小已知。这意味着我们可以确定该代码只能在最大255个字符的上限上运行:
1
2
3
4s ="How are you today?"
for c in s:
for d in s:
print c+d
他说这也是恒定的时间...。即使在我解释了这是O(n ^ 2)算法并证明以下代码将产生二次曲线之后,也是如此。
那么,我是否错过了一些理论概念,根据理论的发展情况,以上任何一项都是正确的?为了明确他的理解,如果n不知道,我是正确的。如果始终知道n的上限,则他断言这篇文章中的两种算法都具有恒定的运行时复杂性。
只是想保持自己的理智,但也许如果我错了,我肯定会从中受益。我的好,好同事很有说服力。此外,如果有人对此问题的主题有其他链接或材料,请添加到评论中。
显然,这两个示例的运行时间是输入大小的函数。第一个是O(n),第二个是O(n2)-您的同事正试图用针头算天使!
通过您对他如何将一切归结为O(1)的描述,听起来他似乎不明白大O表示法的实际含义。如果您试图在45分钟内向他解释这件事,而他全神贯注地拒绝承认他对这件事的理解并不完美,那听起来您手上有另一个问题。
为n选择一个值不会立即将任何算法转换为O(1)。
@ TigerhawkT3不是为N选择的值;它是一个选择的C值-与N无关。该值是在先决条件下给出的,因此我可以自由选择它。讨论Big-O本质的唯一方法是考虑N-> Infinity。虽然挂钟时间肯定遵循预期的模式,但这超出了技术定义。
当然,这并不能很好地代表预期的性能。
但是,这并没有使它成为一个不太复杂的算法。它是相同的O(n)算法-最大输入量只是让我们确定此O(n)算法是否适合我们要在其中使用的任何应用程序。
@ TigerhawkT3 Big-O的定义为f(n)= O(g(n))表示存在正常数c和k,使得对于所有n≥k,0≤f(n)≤cg(n)。在这种情况下,有效参数(尽管是书呆子)的n小于256。正确的答案是将某人的头部跳起来并告诉他们不要再费劲了-并让有限的磁带机(计算机)代表一个理想的无限磁带系统,消除了限制,因此可以有效地分析函数的执行时间。 (最好不要使用最大整数来争论这一点。)
@ user2864740-不,约束N不允许您说O(N ^ 2)算法在O(1)时间内运行。对于任何少于255个字符的输入,您的C值为(256 * 256)足够高,但是正如您自己说的那样,Big-O表示法是在N接近无穷大时考虑性能。 N <256的论点无效,因为您根本不讨论Big-O表示法。
@rmunn我完全同意该结论,但适用于所有内容-特别是因为海报中也接受了有限的N假设/约束。尽管扮演了恶魔的拥护者,但我并没有选择一方-好,不是成为拥护者,而是反对不公平和非技术性的"他坦率地拒绝承认他的理解[是错误的]"评论。解释某些东西并不意味着它是正确的;正如艾夫(Ive)所说,从技术上讲这是不正确的。
我投票结束这个问题是因为没有话题,因为它似乎是在问一个纯粹的数学问题,而不是专门针对编程的问题(鉴于该主题(大O表示法),它恰好在Python的上下文中是不相关的)和接受的答案纯粹是数学上的)。更好的网站可能是计算机科学网站(目前处于beta版),Math Overflow或Programmers。
将Big-O表示法应用于所有输入均已知的单个场景是荒谬的。单个案例没有Big-O。
关键是要对n的任意大的未知值进行最坏情况的估计。如果您已经知道确切的答案,那么为什么您会在地球上浪费时间来估算呢?
数学/计算机科学编辑:
Big-O表示法定义为n任意增大:如果g(n)c * f(n),对于任何常数c,对于所有大于nMin的n,f(n)均为O(g(n))。意思是,您的"对手"可以将c设置为"四十亿",这并不重要,因为对于点nMin的"向右"的所有点,"四十亿乘以f(n)"的图将永远低于g(n)...
Example: 2n is less than or equal to n2... for a short segment of the x-axis that includes n = 2, 3, and 4 (at n = 3, 2n is 8, while n2 is 9). This doesn't change the fact that their Big-O relationship is the opposite: O(2n) is much greater than O(n2), because Big-O says nothing about n values less than nMin. If you set nMin to 4 (thus ignoring the graph to the left of 4), you'll see that the n2 line never exceeds the 2n line.
If your"opponent" multiplies n2 by some larger constant c to raise"his" n2 line above your 2n line, you haven't lost yet... you just slide nMin to the right a bit. Big-O says that no matter how big he makes c, you can always find a point after which his equation loses and yours wins, forever.
但是,如果将n约束在右边,则违反了任何类型的Big-O分析的先决条件。在与同事的争论中,你们中的一个人发明了一个nMax,然后另一个人将nMin设置在它的右侧某处---令人惊讶的是,结果是荒谬的。
例如,在一般情况下,您显示的第一个算法确实对长度为n ...的输入确实完成了n次工作。如果我正在建立自己的算法,调用它n次,那么通常情况下,我将不得不考虑使用二次O(n2)算法。
但是,如果我能证明我永远不会调用输入大于10的算法(这意味着我掌握了更多信息,因此可以更精确地估算算法),那么使用Big-O估算算法的性能就可以避免在我关心的情况下,我了解了它的实际行为。相反,我应该用一个适当的大常数替换您的算法---将我的算法从c * n2更改为c * 10 * n ...,这就是cBigger * n。老实说,我的算法是线性的,因为在这种情况下,您的算法图永远不会超过该常数。这不会改变算法的Big-O性能,因为Big-O并未针对此类约束情况进行定义。
总结:一般来说,您展示的第一个算法是按照Big-O标准线性的。在已知最大输入的受限情况下,完全用Big-O术语来谈论它是一个错误。在受约束的情况下,当讨论其他算法的Big-O行为时,可以合法地用某个常数替换它,但这绝对没有说第一个算法的Big-O行为。
结论:当nMax足够小时,O(Ackermann(n))可以正常工作。非常非常小...
我同意这一点并将其提出来,Big-O为我们提供了一个框架,可根据输入量的大小来比较算法。
可笑是对的。对于N的特定有限值,也可以选择有限C(此时,它以有限的挂钟时间运行)。 Big-O的要点是描述N-> Infinity。
@ user2864740:好点,它很好地总结了我在进行输入时所键入的巨大编辑。
我认为从这篇文章中得到的所有答复都可以从这个特定答案中学到一些好东西,但我认为这抓住了我希望能向朋友解释细节的本质。好东西。
就你而言...
我很想说你的朋友是轻率的错。这是因为在O(1)运行时存在相当大的256附加常数。您的朋友说死刑是O(256)。并且因为我们忽略了Big-O中的常量,所以我们简单地将O(256 * 1)称为O(1)。由您决定此常量对您而言是否可以忽略。
我有两个很强的理由说你是对的:
首先,对于n的各种值,您对O(n)的答案(在第一个代码中)给出了运行时间的更好近似值。例如:
对于长度为4的字符串:您说运行时间与4成正比,而您的朋友说运行时间与1(或256)成正比。
对于长度为255的字符串:您说运行时间与255成比例,而您的朋友又说这是恒定时间。
显然,您的答案在每种情况下都更加准确,即使他的答案并非完全错误。
其次,如果您采用朋友的方法,那么从某种意义上来说,您可以作弊并说,由于没有字符串可以超出RAM +磁盘大小,因此所有处理都在O(1)中进行。那时,您朋友推理的谬误就变得显而易见了。是的,他是正确的,运行时间(假设1TB硬盘和8 GB RAM)为O((1TB + 8GB)* 1)= O(1),但是在这种情况下,您根本无法忽略常量的大小。
Big-O复杂度并不能说明实际的执行时间,而只能说明运行时间随n值的增加的简单增长率。
我认为您不能说他是对的(即他的同事是错误的),因为他的估计更好。 Landau符号是理论计算机科学(最初是数学...),它们可用于无限存储。并且因为大O是一个上限,所以O(n)在O(1)起作用时也起作用。我认为此O(1)是因为255很小。在更多情况下,255被认为是小而不是大。
@Sbls:这就是为什么我在回答中说,由他们来决定常量是否可以忽略的原因。第一段,最后一行。
你们俩在某种程度上是对的,但比您的同事更正确。 (编辑:不。再想一想,你是对的,你的同事是错误的。请参阅下面的评论。)问题的实质不是N是否已知,而是N是否可以改变。 s是算法的输入吗?然后是O(N)或O(N ^ 2):您知道此特定输入的N值,但是不同的输入将具有不同的值,因此知道此输入的N无关紧要。
这是两种方法的区别。您正在将这段代码看起来像这样:
1
2
3
4def f(s):
for c in s:
print c
f("How are you today?")
但是您的同事正在这样对待它:
1
2
3
4def f(some_other_input):
for c in"How are you today?":
print c
f("A different string")
在后一种情况下,应将for循环视为O(1),因为它不会随着不同的输入而改变。在前一种情况下,算法为O(N)。
实际上,在重新阅读您的帖子时,我必须撤回我的声明,说您俩都对。你的同事错了。由于您处理不同的字符串,因此s是算法的输入。您知道可以得到多少N的上限的事实与O(N)分析无关。重要的是,随着N的增长,O(N ^ 2)的增长比O(N)快得多。 N的上限为255并不能改变这一事实,这只是意味着您可以在这种特殊情况下使用O(N ^ 2)算法。
OP明确指出最大大小为N。因此,可以选择一个C使其成为O(1),因为Big-O(受学术启发)仅讨论上限,但这种做法被削减了。在有限的限度内短缺;因此,尽管挂钟时间肯定受输入大小的影响(甚至根据预期曲线),但仍可以确定最大值(恒定)。
但这并不能使其成为O(1)算法。想象一下有人使用O(n ^ 2)算法输入用户名,然后告诉他们"如果某人的用户名具有一百万个字符,该怎么办?"他们回答"嗯,我们限制为20个字符。"您是告诉他们"好吧,那么O(n ^ 2)算法就可以了",还是"好吧,那么它实际上就是O(1)算法"?
我认为你们都是对的。
第一种算法的运行时间在其输入大小上是线性的。但是,如果其输入是固定的,则其运行时间也是固定的。
Big O就是关于在输入改变时测量算法行为的方法。如果输入从不改变,那么Big O是没有意义的。
另外:O(n)表示复杂度的上限是N。如果要表示一个严格的界限,则更精确的表示法是Θ(n)(θ表示法)。
所以就像我说的,输入不是固定的。可能是0-255,所以这仍然成立吗?
但是上限是固定的。例如,两个数字相加在技术上是两个数字中位数的O(n)。但实际上,我们通常将其视为O(1),因为我们使用的数字类型在位数(32或64)上有固定的上限。那有意义吗?
所以我同意,如果输入是固定的,则运行时也将固定。但是,当在完全相同的输入上比较这两种算法时,您永远不会说它们都是O(1)。您不能忽略指数。
哦,我完全同意。你是对的。您的同事提出了一个技术上正确的观点,但是在这种情况下,这毫无意义。无论哪种方式,在您描述的问题上都有局限性时,大O并不是比较可能解决方案的有用框架。