大O增长:每一类复杂度真正的代价
您的数据
两个算法正面对比
常数就是这套记号扔掉的一切:内存分配、缓存未命中、每一步内部的活儿。给其中一个算法一个更重的常数,看看它从哪里开始不再重要。
这个页面不读你的循环。从源码去猜复杂度,是披着测量外衣的猜测:循环可能提前跳出,里面一次调用就可能藏着真正的开销,而递归很少能照字面读出它在做什么。类别由你来选;从那之后页面算的都是精确的。
结果
| 在多少个元素处一方超过另一方 | — |
| 在上面那个规模下,各自的运算次数 | — |
按本页初始的数字,交叉点在 439 个元素。有人在两百条的列表上换用“更好的类别”,结果只是让程序更慢、代码更难,而记号并没有提醒——因为记号讲的是最终会怎样,不是你这里会怎样。
在这个规模下的每一类
| 类别 | 运算次数 | 时间 | 输入翻倍时乘以多少 |
|---|
最后一列才值得盯着看,因为那是没人会随身记住的一列。线性工作翻一倍。平方工作变四倍。指数工作是被平方:已经有一千个元素时,把输入翻倍会让代价乘以二的一千次方,一个三百零二位的数。
阶乘的糟糕,跟“翻倍”完全是另一回事。多加一个元素,代价就乘以新的个数:第二十一个元素,要花掉整个二十元素问题的二十一倍。所以精确求解二十座城市的旅行商问题是 2.4 百万百万百万步,按每秒十亿次算是七十七年;而同样二十座城市在二的 n 次方下,千分之一秒就结束了。
这也解释了为什么类别之间的差距在小规模上乏味、在大规模上残酷。十个元素时,n log n 和 n 平方相差三倍,没有哪个用户会察觉。一千个元素时相差一百倍,一百万个时相差五万倍。同样两个算法,同样的代码,变的只有输入。
所以,诚实地读一个复杂度类别,它是关于曲线形状的承诺,不是关于速度的承诺。它说的是输入变大时代价如何反应,完全没说在你今天这台机器上、在你真正的规模下它快不快。
复杂度类别更好,就等于程序更快吗?
在你的规模上,未必。类别描述的是输入变大时代价如何反应;它对每一步内部的活儿只字不提,而那正是这套记号扔掉的东西。
按本页初始的数字,常数轻的平方算法,在低于 439 个元素的所有输入上都胜过常数为五十的 n log n。超过之后,更好的那一类会一路拉开,再也不把领先让回去。
为什么这个页面不读我的代码?
因为从源码去猜复杂度,是披着测量外衣的猜测。循环可能提前跳出,里面一次调用就可能藏着真正的开销,而递归很少能照字面读出它在做什么。
所以在这里,类别是你填的,不是页面的看法。由它算出的一切都是精确的算术,页面里没有任何地方假装知道你的循环在做什么。
输入翻倍时,究竟会发生什么?
线性工作翻一倍,平方工作变四倍,这些大多数人心里都有数。指数工作是被平方,这个几乎没人有数:一千个元素时,把输入翻倍会让代价乘以二的一千次方。
阶乘根本不按那个方式走。多加一个元素,代价就乘以新的个数,所以第二十一个元素要花掉整个二十元素问题的二十一倍。
| 类别 | 在一千个元素时,输入翻倍会乘以多少 |
|---|---|
| O(n) | 2 |
| O(n log n) | 2,2 |
| O(n²) | 4 |
| O(n³) | 8 |
| O(2ⁿ) | 1,07 x 10^301 |
为什么二十座城市的旅行商问题无法精确求解?
因为二十的阶乘是 2.4 百万百万百万步,按每秒十亿次运算算,就是七十七年。同样二十个元素在二的 n 次方下,大约千分之一秒就完成了。
这就是指数与阶乘在实务上的区别,也是值得把两者分开、而不是一并归入“不可能”的原因。
反应
0
0 评论
成为第一个发表评论的人