复杂度分析(上):如何分析、统计算法的执行效率和资源消耗

复杂度分析是什么?

复杂度分析就是分析执行一个给定算法需要消耗的计算资源数量(例如计算时间,存储器使用等)的过程。

为什么要学习复杂度分析?

没有复杂度分析怎么得到算法执行的时间和占用的内存大小

把代码运行一遍,通过统计、监控,就能得到算法执行的时间和占用的内存大小。

该方法的缺点在于:

1、测试结果非常依赖测试环境

拿同样一段代码,在 Intel Core i9 处理器上运行的速度肯定要比 Intel Core i3 快得多。同一段代码,在不同机器上运行,也可能会有截然相反的结果。

2、测试结果受数据规模的影响很大

以排序算法举个例子。对同一个排序算法,待排序数据的有序度不一样,排序的执行时间就会有很大的差别。极端情况下,如果数据已经是有序的,那排序算法不需要做任何操作,执行时间就会非常短。除此之外,如果测试数据规模太小,测试结果可能无法真实地反应算法的性能。比如,对于小规模的数据排序,插入排序可能反倒会比快速排序要快!

使用复杂度分析有什么好处

不需要用具体的测试数据来测试,就可以粗略地估计算法的执行效率。

怎么学习复杂度分析?

大O复杂度表示法

下面有一段代码,来估算一下这段代码的执行时间:

1 int cal(int n) {
2   int sum = 0;
3   int i = 0;
4   int j = 0;
5   for (; i < n; ++i) {
6     j = 1;
7     for (; j < n; ++j) {
8       sum = sum +  i * j;
9     }
10   }
11 }

 

假设每行代码执行的时间都一样,为 unit_time,在这个假设的基础之上,我们可以看出:第 2、3、4 行代码分别需要 1 个 unit_time 的执行时间,第 5、6 行都运行了 n 遍,所以需要 2n*unit_time 的执行时间,第 7、8 行代码循环执行了 n^2遍,所以需要 2n^2 * unit_time 的执行时间。所以,整段代码总的执行时间 T(n) = (2n^2+2n+3)*unit_time。

通过这段代码执行时间的推导过程,我们可以知道,所有代码的执行时间 T(n) 与每行代码的执行次数 n 成正比。用大 O 复杂度表示法可以这样表示:T(n) = O(f(n))

其中,T(n) 表示代码执行的时间;n 表示数据规模的大小;f(n) 表示每行代码执行的次数总和。公式中的 O,表示代码的执行时间 T(n) 与 f(n) 表达式成正比。

时间复杂度分析

分析一段代码的时间复杂度三个比较实用的方法:

1、只关注循环执行次数最多的一段代码

在进行时间复杂度分析的时候,我们通常会忽略掉公式中的常量、低阶、系数,只记录一个最大阶的量级。所以,我们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就可以了。

2、加法法则:总复杂度等于量级最大的那段代码的复杂度

假设有这样三段代码,每段代码是一个for循环,第一段代码循环了 100 次,第二段代码循环了 n 次,第三段代码循环了 n^2 次。

那么,第一段代码的时间复杂度是一个常量,跟 n 的规模无关。第二段代码和第三段代码的时间复杂度分别为 O(n) 和 O(n^2)。

因为总的时间复杂度就等于量级最大的那段代码的时间复杂度,所以,整段代码的时间复杂度为 O(n^2)。那我们将这个规律抽象成公式就是:

如果 T1(n)=O(f(n)),T2(n)=O(g(n));

那么 T(n)=T1(n)+T2(n)=max(O(f(n)), O(g(n))) =O(max(f(n), g(n)))

3、乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

复杂度分析中还有一个乘法法则。

如果 T1(n)=O(f(n)),T2(n)=O(g(n));

那么 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n))。

常见的嵌套循环,其时间复杂度就可以用乘法法则进行计算 。

几种常见时间复杂度实例分析

常见的复杂度量级并不多,大概就是下图中包含的:

《复杂度分析(上):如何分析、统计算法的执行效率和资源消耗》

复杂度量级可以分为多项式量级和非多项式量级。其中,非多项式量级只有两个:O(2^n) 和 O(n!)。

对于多项式量级,随着数据规模的增长,算法的执行时间和空间占用统一呈多项式规律增长;而对于非多项式量级,随着数据规模的增长,其时间复杂度会急剧增长,执行时间无限增加;

这里主要来看几种常见的多项式时间复杂度:

1、O(1)

O(1) 只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码。只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。

2、O(logn)、O(nlogn)

最难分析的是对数阶时间复杂度,这里通过一个例子来说明一下。

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

我们已经知道,只要关注循环执行次数最多的那一段代码就可以了,这里循环执行次数最多的是第 3 行代码,所以,我们只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。 从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。观察变量 i 的增长规律,我们可以发现,它的取值是一个等比数列。如果我们把它一个一个列出来,就应该是这个样子的:

《复杂度分析(上):如何分析、统计算法的执行效率和资源消耗》

理解了 O(logn),那 O(nlogn) 就很容易理解了。前面我们讲了时间复杂度分析的乘法法则,如果一段代码的时间复杂度是 O(logn),我们循环执行 n 遍,那么时间复杂度就是 O(nlogn) 了。(常见的归并排序、快速排序的时间复杂度都是 O(nlogn))

3、O(m+n)、O(m*n)

来看这样一段代码:

 1 int cal(int m, int n) {
 2   int sum_1 = 0;
 3   int i = 0;
 4   for (; i < m; ++i) {
 5     sum_1 = sum_1 + i;
 6   }
 7 
 8   int sum_2 = 0;
 9   int j = 0;
10   for (; j < n; ++j) {
11     sum_2 = sum_2 + j;
12   }
13 
14   return sum_1 + sum_2;
15 }

从代码中可以看出,m 和 n 是表示两个未知的数据规模,其大小关系也无法确定,所以我们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。所以,上面代码的时间复杂度就是 O(m+n)。

针对这种情况,原来的加法法则就不正确了,我们需要将加法规则改为:T1(m) + T2(n) = O(f(m) + g(n))。但是乘法法则继续有效:T1(m)*T2(n) = O(f(m) * f(n))。

空间复杂度分析

相比起时间复杂度分析,空间复杂度分析方法学起来就非常简单了。

这里也举一个例子进行说明:

1 void print(int n) {
2   int i = 0; 3 int[] a = new int[n]; 4 for (i; i <n; ++i) { 5 a[i] = i * i; 6 } 7 }

这里第 3 行申请了一个大小为 n 的 int 类型数组,除此之外,其余代码所占空间都是可以忽略的,所以整段代码的空间复杂度就是 O(n)。

我们常见的空间复杂度就是 O(1)、O(n)、O(n^2 ),像 O(logn)、O(nlogn) 这样的对数阶空间复杂度平时都用不到,相比起来,空间复杂度分析比时间复杂度分析要简单很多。

内容小结

总结一下:复杂度包括时间复杂度和空间复杂度,用来分析算法执行效率与数据规模之间的增长关系,越高阶复杂度的算法,执行效率越低。常见的复杂度从低阶到高阶有:O(1)、O(logn)、O(n)、O(nlogn)、O(n^2 )。

《复杂度分析(上):如何分析、统计算法的执行效率和资源消耗》

 

    原文作者:hardyyao
    原文地址: https://www.cnblogs.com/hardyyao/p/9744418.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞