jiayuqicz 2020-02-02
对于一个问题,经常有多种不同的求解算法,这时候我们就需要一个对算法进行评估的标准,找出最优的方案,评估一个算法有以下几个维度:
时间复杂度:在给定输入(问题规模)下,算法的计算量。
所以说,求一个算法的时间复杂度,就是求这个算法在给定问题规模下的计算量,那么问题来了:如何求算法的计算量?
算法计算量的求法规则如下:
我们以一个例子来说明,求如下表达式的值:
// 阶乘的和 1! + 2! + 3! + ... + n!
我们可以写出如下程序(js代码):
function factorial (n) { var s = 0, temp = 1 for (var i = 1; i <= n; i++) { temp = 1 for (var j = 1; j <= i; j++) { temp *= j } s += temp } return s }
我们根据之前总结的算法计算量的求法规则可知,求解一个算法的计算量分为三个步骤,第一步:确定基本操作,对于上面的代码我们所挑选的基本操作如下:
var s = 0 temp = 1
当我们的输入规模即 n
变化时,这两条语句的执行次数没有变,始终是 2
次。
第二部分赋值语句:
for (var i = 1; i <= n; i++) { temp = 1 ... }
第一层循环里的 temp = 1
,该语句的执行次数等于输入规模 n
。
乘法计算语句:
for (var j = 1; j <= i; j++) { temp *= j }
第二层循环里的 temp *= j
,该语句的执行次数,当 n = 1
时执行 1 次,当 n = 2
时执行 1 + 2
次,当 n = 3
时执行 1 + 2 + 3
次,所以该语句的执行次数与输入规模 n
的关系是 1 + 2 + 3 + ... + n = n(n + 1) / 2
。
for (var i = 1; i <= n; i++) { ... s += temp }第一层循环里的加法赋值语句,该语句的执行次数等于输入规模
n
。综上所述,根据我们选择的“基本操作”,可以计算出该算法的基本操作次数与输入规模 n
的关系如下:
T(n) = 2 + n + n(n + 1) / 2 + n = 1/2n^2 + 3/2n + 2
当 n
足够大时,n^2
起支配作用,使用 O(n^2)
表示 T(n)
的近似值,这种表示法成为 大 O 表示法
。
一般我们认为一个算法的时间复杂度为指数阶的时候,该算法是实际不可运算的,大家可以想象一下,如果一个算法的时间复杂度为 O(2^n)
当 n = 1000
时,这个数值是何等恐怖。更何况我们的输入规模 n
很可能远大于 1000。
另外我们认为时间复杂度为 O(n)
、O(log2N)
、O(n^2)
是高效的算法。对于合理大的 n
,O(n^3)
也是可以接受的。
空间复杂度:给定输入(问题规模)下,算法运行时所占用的临时存储空间。
一个算法执行期间所占用的存储量分为三部分:
由于实现不同算法所需的代码不会有数量级的差别,所以算法本身代码所占用的空间我们可以不考虑
输入的数据所占用的空间是由问题决定的,与算法无关,所以我们也不需要考虑
我们需要考虑的只有一个:程序执行期间,辅助变量所占用的空间。
计算方法类似于计算算法的时间复杂度,空间复杂度我们用 S(n)
来表示,它同样是输入数据规模 n
的函数,用大 O 表示法记为:
S(n) = O(g(n))
其中 g(n)
是一个关于 n
的函数,如:g(n) = n
、g(n) = n^2
、g(n) = log2N
等等。
假设我们有一个数组,该数组有100个元素,写一个转置该数组的算法:
function reverse (arr) { for (var i = 0; i <= arr.length / 2 - 1; i++) { var temp = arr[i] arr[i] = arr[arr.length - i - 1] arr[arr.length - i - 1] = temp } }
上面的算法中,我们采用中间变量 temp
对数组的值进行逐个对应的首尾交换,最终达到转置的目的,我们可以看到,辅助变量只有一个即 temp
,该变量存储一个数字类型的值,temp
所占用的内存不会随输入数组规模的增大而增大,所以上面算法的控件复杂度为 O(1)
,是常数阶,即上面算法的空间复杂度 S(n) = O(1)
。
from: http://blog.poetries.top/js-knowledge-note/#/note/algorithm/time-space