我最近与我的一位同事就一个超级简单算法的运行时复杂性进行了非常非常激烈的辩论。最后,我们都同意不同意,但正如我一直在考虑的那样,它挑战了我对计算机科学基础的基本理解,因此我必须对这个问题有更多的了解。
鉴于以下python,Big-O运行时复杂性是什么:
for c in "How are you today?":
print c
现在,我立刻喊出这只是O(n)又称线性的顺序。这意味着它取决于字符串的长度,因此这个循环将随着字符串长度的增长而线性增长。
我的同事然后说,“不,它是常数,因为我们知道对于我们正在处理的所有字符串集合(在我们的例子中),最大字符串总是255个字符长(在我们的例子中),因此它必须保持不变。“他接着说:“因为我们在字符串的字符长度上有一个最大上限,这导致O(255)减少到O(1)。”
无论如何,我们回到了第四,在我们两人画了草图45分钟之后,我们都在这个问题上死锁。
我的问题是在一个恒定时间循环之上的循环是什么世界或什么数学系统?如果我们知道我们的上限是1,000,000个字符,并且所有字符串的集合可以是0到1,000,000之间,那么这个循环显然会表现出线性运行时间,具体取决于字符串的大小。
我还问他,如果n的上限大小已知,他是否也认为以下代码是O(1)。这意味着我们确定这段代码只会在255个字符的最大上限上运行:
s = "How are you today?"
for c in s:
for d in s:
print c+d
他说这也是恒定的时间....即使我解释这是一个O(n ^ 2)算法,并证明以下代码会产生二次曲线。
那么,我是否遗漏了一些理论概念,其中任何一个都是真的,这取决于理论如何发展? 只是要明确他的理解是,如果不知道n,我是正确的。如果n的上限总是已知的,那么他断言这篇文章中的两个算法都具有恒定的运行时复杂性。
只是想保持理智,但也许如果我错了,肯定会有一些额外的学习,我可以从中受益。我的好同事非常有说服力。此外,如果任何人有关于此问题的主题的其他链接或材料,请添加到评论中。
答案 0 :(得分:11)
将Big-O表示法应用于已知所有输入的单个场景是荒谬的。对于单个案例, 没有Big-O。
重点是对 n 的任意大 未知值进行最坏情况估算。如果您已经知道确切的答案,为什么在地球上你会浪费时间去估算它?
Mathy / Computer-Sciencey编辑:
Big-O表示法定义为 n 任意增大:f( n )是O(g( n ))如果g ( n )≥ c * f( n ),对于任何常量 c ,表示所有< em> n 大于某些 nMin 。意思是,你的“对手”可以将 c 设置为“十一 - 四十亿”并且没关系,因为,对于某些点“向右”的所有点 nMin ,“十一 - 四十亿次f( n )”的图表将永远滞后于g( n )......
示例:2 n 小于或等于 n 2 ...对于x轴的一小段,包括 n = 2,3和4(在 n = 3,2 n 是8,而 n 2 是9)。这并没有改变他们的Big-O关系相反的事实:O(2 n )远大于O( n 2 ),因为Big-O说 n 关于 n 的值小于 nMin 。如果将 nMin 设置为4(因此忽略4左侧的图形),您将看到 n 2 行从不超过2 n 行。
如果你的“对手”将 n 2 乘以更大的常数 c 来提升“他的” n 您的2 n 行上方的 2 行,您还没有丢失......您只需将 nMin 拖到右边一点。 Big-O说无论他做多大 c ,你都可以总是找到一个点,在这个点之后他的方程式会失败而你的方程式会永远获胜。
但是,如果您在右侧约束 n ,则违反了任何类型的Big-O分析的先决条件。在与同事的争论中,你们中的一个发明了一个 nMax ,然后另一个发明了 nMin 在它的右边 - 惊喜,结果是无意义的。
例如,在一般情况下,您展示的第一个算法确实对 n 工作的长度为 n 的输入工作。如果我构建自己的算法,称为 n 次,我将不得不考虑我的二次O( n 2 )算法。再次,在一般情况下。
但是,如果我能证明我将永远不会使用大于10的输入调用您的算法(意味着我有更多信息,因此可以更精确地估计我的算法),使用Big-O在我关心的情况下,估计你的算法的性能会丢掉我对实际行为的了解。我应该用适当大的常量替换你的算法---从 c * n 2 中更改我的算法到 c * 10 * n ...这只是 cBigger * n 。我老实说我的算法是线性的,因为在这种情况下,你的算法的图形永远不会超过那个常数值。这会改变 nothing 关于算法的Big-O性能,因为没有为像这样的约束情况定义Big-O。
总结:通常,你展示的第一个算法是Big-O标准的线性算法。在约束情况中,已知最大输入,以Big-O术语来说它是错误的。在一个受约束的情况下,在讨论某些其他算法的Big-O行为时,可以合法地被某个常量值替换,但这对于Big-O完全没有任何意义。第一种算法的行为。
总之:当 nMax 足够小时,O(Ackermann( n ))可以正常工作。非常非常小......
答案 1 :(得分:2)
我很想说你的朋友是错误的。这是因为在O(1)运行时中相当大的额外常数256。你的朋友说执行是O(256)。因为我们忽略了Big-O中的常量,所以我们简单地将O(256 * 1)称为O(1)。你可以决定这个常数是否可以忽略不计。
我有充分的理由说你是对的:
首先,对于 n 的各种值,你对O(n)的回答(在第一个代码中)给出了更好的运行时近似值。例如:
显然,你的答案在每种情况下都更准确,即使他的回答并非完全错误。
其次,如果你按照你朋友的方法去做,那么在某种意义上你可以作弊并说因为没有字符串可以超出你的RAM +磁盘大小,因此所有处理都在O(1)中。那就是你朋友推理的谬误变得明显的时候了。 是的,他是正确的,运行时间(假设1TB硬盘和8 GB RAM)是O((1TB + 8GB)* 1)= O(1),但在这种情况下你根本无法忽略常量的大小的
Big-O复杂性并不能说明执行的实际时间,而是随着n值的增加而增加的运行时间的简单增长率。 strong>
答案 2 :(得分:1)
我认为你是对的。
第一个算法的运行时间在其输入大小上是线性的。但是,如果它的输入是固定的,那么它的运行时也是固定的。
Big O就是测量算法的行为,因为它的输入会发生变化。如果输入永远不会改变,那么Big O就没有意义了。
另外:O(n)表示复杂度的上限为N.如果要表示紧束缚,则更精确的符号为Θ(n )(theta表示法)。
答案 3 :(得分:1)
你在某种程度上都是正确的,但你比你的同事更正确。 (编辑:没有。进一步思考,你是对的,你的同事是错的。请参阅下面的评论。)问题实际上并不是N是否已知< / i>,但N是否可以更改。 s
是算法的输入吗?那么它是O(N)或O(N ^ 2):你知道这个特定输入的N值,但是不同的输入会有不同的值,所以知道这个输入的N是不是相关的。
这是你的两种方法的不同之处。您将此代码视为如下所示:
def f(s):
for c in s:
print c
f("How are you today?")
但是你的同事正在这样对待它:
def f(some_other_input):
for c in "How are you today?":
print c
f("A different string")
在后一种情况下,for循环应该被认为是O(1),因为它不会用不同的输入改变。在前一种情况下,算法是O(N)。