十分钟弄懂:数据结构与算法之美 - 时间和空间复杂度 (2)

嵌套代码求乘积:比如递归、多重循环等。

function cal(n) { let ret = 0; let i = 1; for (; i < n; ++i) { ret = ret + f(i); // 重点为 f(i) } } function f(n) { let sum = 0; let i = 1; for (; i < n; ++i) { sum = sum + i; } return sum; }

方法 cal 循环里面调用 f 方法,而 f 方法里面也有循环。

所以,整个 cal() 函数的时间复杂度就是,T(n) = T1(n) * T2(n) = O(n*n) = O(n2) 。

多个规模求加法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相加

function cal(m, n) { let sum_1 = 0; let i = 1; for (; i < m; ++i) { sum_1 = sum_1 + i; } let sum_2 = 0; let j = 1; for (; j < n; ++j) { sum_2 = sum_2 + j; } return sum_1 + sum_2; }

以上代码也是求和 ,求 sum_1 的数据规模为 m、求 sum_2 的数据规模为 n,所以时间复杂度为 O(m+n)。

公式:T1(m) + T2(n) = O(f(m) + g(n)) 。

多个规模求乘法:比如方法有两个参数控制两个循环的次数,那么这时就取二者复杂度相乘

function cal(m, n) { let sum_3 = 0; let i = 1; let j = 1; for (; i <= m; ++i) { j = 1; for (; j <= n; ++j) { sum_3 = sum_3 + i * j; } } }

以上代码也是求和,两层 for 循环 ,求 sum_3 的数据规模为 m 和 n,所以时间复杂度为 O(m*n)。

公式:T1(m) * T2(n) = O(f(m) * g(n)) 。

3.4 常用的时间复杂度分析

多项式阶:随着数据规模的增长,算法的执行时间和空间占用,按照多项式的比例增长。

包括 O(1)(常数阶)、O(logn)(对数阶)、O(n)(线性阶)、O(nlogn)(线性对数阶)、O(n2) (平方阶)、O(n3)(立方阶)。

除了 O(logn)、O(nlogn) ,其他的都可从上面的几个例子中看到。

下面举例说明 O(logn)(对数阶)

let i=1; while (i <= n) { i = i * 2; }

代码是从 1 开始,每次循环就乘以 2,当大于 n 时,循环结束。

其实就是高中学过的等比数列,i 的取值就是一个等比数列。在数学里面是这样子的:

20 21 22 ... 2k ... 2x = n

所以,我们只要知道 x 值是多少,就知道这行代码执行的次数了,通过 2x = n 求解 x,数学中求解得 x = log2n 。所以上面代码的时间复杂度为 O(log2n)。

实际上,不管是以 2 为底、以 3 为底,还是以 10 为底,我们可以把所有对数阶的时间复杂度都记为 O(logn)。为什么呢?

因为对数之间是可以互相转换的,log3n = log32 * log2n,所以 O(log3n) = O(C * log2n),其中 C=log32 是一个常量。

由于 时间复杂度 描述的是算法执行时间与数据规模的 增长变化趋势,所以 常量、低阶、系数 实际上对这种增长趋势不产生决定性影响,所以在做时间复杂度分析时 忽略 这些项。

因此,在对数阶时间复杂度的表示方法里,我们忽略对数的 “底”,统一表示为 O(logn)

下面举例说明 O(nlogn)(对数阶)

function aFun(n){ let i = 1; while (i <= n) { i = i * 2; } return i } function cal(n) { let sum = 0; for (let i = 1; i <= n; ++i) { sum = sum + aFun(n); } return sum; }

aFun 的时间复杂度为 O(logn),而 cal 的时间复杂度为 O(n),所以上面代码的时间复杂度为 T(n) = T1(logn) * T2(n) = O(logn*n) = O(nlogn) 。

非多项式阶:随着数据规模的增长,算法的执行时间和空间占用暴增,这类算法性能极差。

包括 O(2n)(指数阶)、O(n!)(阶乘阶)。

O(2n)(指数阶)例子:

aFunc( n ) { if (n <= 1) { return 1; } else { return aFunc(n - 1) + aFunc(n - 2); } }

参考答案:
显然运行次数,T(0) = T(1) = 1,同时 T(n) = T(n - 1) + T(n - 2) + 1,这里的 1 是其中的加法算一次执行。
显然 T(n) = T(n - 1) + T(n - 2) 是一个斐波那契数列,通过归纳证明法可以证明,当 n >= 1 时 T(n) < (5/3)n,同时当 n > 4 时 T(n) >= (3/2)n。
所以该方法的时间复杂度可以表示为 O((5/3)n),简化后为 O(2n)。
可见这个方法所需的运行时间是以指数的速度增长的。
如果大家感兴趣,可以试下分别用 1,10,100 的输入大小来测试下算法的运行时间,相信大家会感受到时间复杂度的无穷魅力。

3.5 时间复杂度分类

时间复杂度可以分为:

最好情况时间复杂度(best case time complexity):在最理想的情况下,执行这段代码的时间复杂度。

最坏情况时间复杂度(worst case time complexity):在最糟糕的情况下,执行这段代码的时间复杂度。

平均情况时间复杂度(average case time complexity),用代码在所有情况下执行的次数的加权平均值表示。也叫 加权平均时间复杂度 或者 期望时间复杂度

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpyffw.html