算法复杂度

算法复杂度

> LeekScript 教程

基本思路

要估算一个函数或一段算法会消耗多少操作数,最好先弄清楚:传给它的参数有多大(或者某个参数有多大),和它最多会执行多少次操作之间,是什么关系。这就是所谓的算法复杂度

先从一个简单的例子说起:一个 for 循环。

for (var i=0; i N 都有 f(n) n² 的增长比函数 n -> 4^n 慢得多:当 MP 变多时,写得不好的可到达格子函数开销会高得离谱,而同样一个函数写得好,开销仍然合理。

实际计算

这时自然会想到一个问题:这些 O 要怎么算?又有哪些值得拿来和我们的代码作比较的函数? 下面我会列出几种经常遇到的复杂度,按从低到高排列,再给出一些估算算法复杂度的计算规则。之后把这些规则组合起来,就能算出一段代码的复杂度了!

层级

--- 经常遇到的复杂度有下面这些,按从低到高的顺序排列:

O(1):“常数复杂度” O(n):“线性复杂度” O(n log(n)):“准线性复杂度” O(n²):“平方复杂度” O(n^k),k 为整数:“多项式复杂度” O(k^n):“指数复杂度” O(n!):“阶乘复杂度(可以证明它和 O(sqrt(2πn)(n/e)^n) 是一回事)”

计算规则

--- // 开销为 O(g(n)) 的代码,接着是 // 开销为 O(h(n)) 的代码

当两段代码前后相接时,保留其中“较大”的那一项:如果 g 的复杂度高于 h,那么这段代码是 O(g(n)) 的;如果 h 的复杂度高于 g,那么这段代码是 O(h(n)) 的。如果两段代码的复杂度相同,那么整块代码也是同样的复杂度。

前面的例子已经让我们看到 for 循环是怎么表现的:

for (var i=0; i 1),它先算出 n-1 个元素的所有连招,然后对找到的每个连招,往最终返回的列表里添加 2 个(一个是该连招的副本,另一个是该连招再加上列表第一个元素的副本)。 于是,如果用 N(n) 表示输入一个 n 元素列表时返回的连招列表里有多少个元素,那么对 n > 1 有 N(n) = 2*N(n-1)。另外 N(1) = 2,因为列表只含一个元素时,返回的列表里有 2 个连招。这样递推下去,就得到 N(n)=2^n。既然最后有 2^n 个元素,那就是做了 2^n 次 push。我们也可以去数函数调用、count 等等的次数:它们都不会比 push 更多。所以这是一个复杂度为 O(2^n) 的函数。

因此,研究递归函数复杂度的思路,就是找出一个递推公式。这里这个公式特别简单,实际中可能会遇到复杂得多的情况!

LeekScript 的函数

--- 最后我来列几个开销随输入变化的 LeekScript 函数(并不完整)。如果没找到你要的函数,就自己测一测,或者到聊天里问问吧!

数组函数:n 是数组的大小

函数 | 开销 ---------|----- arrayFilter | O(n) arrayFoldLeft | O(n) arrayFoldRight | O(n) arrayIter | O(n) arrayMap | O(n) arrayMax | O(n) arrayMin | O(1)(是的,min 和 max 的开销不一样,这确实很离谱……) arrayPartition | O(n) arraySort | O(n log(n)) assocSort | O(n log(n)) average | O(n) fill | 我不知道(背后藏着数组大小的调整) inArray | O(n) join | 呃……这个我也不知道。 keySort | O(n log(n)) pop 和 push | 我不知道(背后藏着数组大小的调整) remove | ? removeElement | ? removeKey | ? reverse | O(n) search | O(n) shift 和 unshift | 我不知道(背后藏着数组大小的调整) shuffle | 理论上是 O(n)。但 LW 的 shuffle 有点可疑,在大数组上的开销还不到 n…… sort | O(n log(n)) sum | O(n)

移动和距离函数:n 是起点到终点之间的 MP 数量

函数 | 开销 ---------|----- getPath | O(n²)(大概吧,我不清楚确切的代码) getPathLength | O(n²)(同样的说明)