咱们都知道,数据结构和算法自己解决的是“快”和“省”的问题,即如何让代码运行得更快,如何让代码更省存储空间。因此,执行效率是算法一个很是重要的考量指标。那如何来衡量你编写的算法代码的执行效率呢?这里就要用到咱们今天要讲的内容:时间、空间复杂度分析。算法
其实,只要讲到数据结构与算法,就必定离不开时间、空间复杂度分析。复杂度分析是整个算法学习的精髓,只要掌握了它,数据结构和算法的内容基本上就掌握了一半。数组
int cal(int n) { int sum = 0; int i = 1; int j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum = sum + i * j; } } }
我来具体解释一下这个公式。其中,数据结构
这就是大 O 时间复杂度表示法。大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增加的变化趋势,因此,也叫做渐进时间复杂度(asymptotic time complexity),简称时间复杂度。数据结构和算法
大 O 这种复杂度表示方法只是表示一种变化趋势。咱们一般会忽略掉公式中的常量、低阶、系数,只须要记录一个最大阶的量级就能够了。因此,咱们在分析一个算法、一段代码的时间复杂度的时候,也只关注循环执行次数最多的那一段代码就能够了。这段核心代码执行次数的 n 的量级,就是整段要分析代码的时间复杂度。函数
int cal(int n) { int sum = 0; int i = 1; int j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum = sum + i * j; } } }
其中第 二、3 行代码都是常量级的执行时间,与 n 的大小无关,因此对于复杂度并无影响。循环执行次数最多的是第 四、5 行代码,因此这块代码要重点分析。前面咱们也讲过,这两行代码被执行了 n 次,因此总的时间复杂度就是 O(n)。学习
int cal(int n) { int sum_1 = 0; int p = 1; for (; p < 100; ++p) { sum_1 = sum_1 + p; } int sum_2 = 0; int q = 1; for (; q < n; ++q) { sum_2 = sum_2 + q; } int sum_3 = 0; int i = 1; int j = 1; for (; i <= n; ++i) { j = 1; for (; j <= n; ++j) { sum_3 = sum_3 + i * j; } } return sum_1 + sum_2 + sum_3; }
综合这三段代码的时间复杂度,咱们取其中最大的量级。因此,整段代码的时间复杂度就为 O(n^2)。也就是说:等于量级最大的那段代码的时间复杂度。spa
也就是说,假设 T1(n) = O(n),T2(n) = O(n^2),则 T1(n) * T2(n) = O(n^2)。落实到具体的代码上,咱们能够把乘法法则当作是嵌套循环,我举个例子给你解释一下。code
int cal(int n) { int ret = 0; int i = 1; for (; i < n; ++i) { ret = ret + f(i); } } int f(int n) { int sum = 0; int i = 1; for (; i < n; ++i) { sum = sum + i; } return sum; }
咱们单独看 cal() 函数。假设 f() 只是一个普通的操做,那第 4~6 行的时间复杂度就是,T1(n) = O(n)。但 f() 函数自己不是一个简单的操做,它的时间复杂度是 T2(n) = O(n),因此,整个 cal() 函数的时间复杂度就是,T(n) = T1(n) T2(n) = O(nn) = O(n^2)。排序
首先你必须明确一个概念,O(1) 只是常量级时间复杂度的一种表示方法,并非指只执行了一行代码。好比这段代码,即使有 3 行,它的时间复杂度也是 O(1),而不是 O(3)。递归
int i = 8; int j = 6; int sum = i + j;
我稍微总结一下,只要代码的执行时间不随 n 的增大而增加,这样代码的时间复杂度咱们都记做 O(1)。或者说,通常状况下,只要算法中不存在循环语句、递归语句,即便有成千上万行的代码,其时间复杂度也是Ο(1)。
对数阶时间复杂度很是常见,同时也是最难分析的一种时间复杂度。我经过一个例子来讲明一下。
i=1; while (i <= n) { i = i * 2; }
根据咱们前面讲的复杂度分析方法,第三行代码是循环执行次数最多的。因此,咱们只要能计算出这行代码被执行了多少次,就能知道整段代码的时间复杂度。
从代码中能够看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束。还记得咱们高中学过的等比数列吗?实际上,变量 i 的取值就是一个等比数列。若是我把它一个一个列出来,就应该是这个样子的:
因此,咱们只要知道 x 值是多少,就知道这行代码执行的次数了。经过 2^x=n 求解 x 这个问题咱们想高中应该就学过了,我就很少说了。x=log2n,因此,这段代码的时间复杂度就是 O(log2n)。
如今,我把代码稍微改下,你再看看,这段代码的时间复杂度是多少?
i=1; while (i <= n) { i = i * 3; }
根据我刚刚讲的思路,很简单就能看出来,这段代码的时间复杂度为 O(log3n)。
实际上,不论是以 2 为底、以 3 为底,仍是以 10 为底,咱们能够把全部对数阶的时间复杂度都记为 O(logn)。为何呢?
咱们知道,对数之间是能够互相转换的,log(3)(n) 就等于 log(3)(2) log(2)(n),因此 O(log(3)(n)) = = O(C log(2)(n)),其中 C=log(3)(2) 是一个常量。基于咱们前面的一个理论:在采用大 O 标记复杂度的时候,能够忽略系数,即 O(Cf(n)) = O(f(n))。因此,O(log2n) 就等于 O(log3n)。所以,在对数阶时间复杂度的表示方法里,咱们忽略对数的“底”,统一表示为 O(logn)。
若是你理解了我前面讲的 O(logn),那 O(nlogn) 就很容易理解了。还记得咱们刚讲的乘法法则吗?若是一段代码的时间复杂度是 O(logn),咱们循环执行 n 遍,时间复杂度就是 O(nlogn) 了。并且,O(nlogn) 也是一种很是常见的算法时间复杂度。好比,归并排序、快速排序的时间复杂度都是 O(nlogn)。
int cal(int m, int n) { int sum_1 = 0; int i = 1; for (; i < m; ++i) { sum_1 = sum_1 + i; } int sum_2 = 0; int j = 1; for (; j < n; ++j) { sum_2 = sum_2 + j; } return sum_1 + sum_2; }
从代码中能够看出,m 和 n 是表示两个数据规模。咱们没法事先评估 m 和 n 谁的量级大,因此咱们在表示复杂度的时候,就不能简单地利用加法法则,省略掉其中一个。因此,上面代码的时间复杂度就是 O(m+n)。
针对这种状况,原来的加法法则就不正确了,咱们须要将加法规则改成:T1(m) + T2(n) = O(f(m) + g(n))。可是乘法法则继续有效:T1(m)T2(n) = O(f(m) f(n))。
void print(int n) { int i = 0; int[] a = new int[n]; for (i; i <n; ++i) { a[i] = i * i; } for (i = n-1; i >= 0; --i) { print out a[i] } }
跟时间复杂度分析同样,咱们能够看到,第 2 行代码中,咱们申请了一个空间存储变量 i,可是它是常量阶的,跟数据规模 n 没有关系,因此咱们能够忽略。第 3 行申请了一个大小为 n 的 int 类型数组,除此以外,剩下的代码都没有占用更多的空间,因此整段代码的空间复杂度就是 O(n)。
咱们常见的空间复杂度就是 O(1)、O(n)、O(n^2 ),像 O(logn)、O(nlogn) 这样的对数阶复杂度平时都用不到。并且,空间复杂度分析比时间复杂度分析要简单不少。因此,对于空间复杂度,掌握刚我说的这些内容已经足够了。
参考:https://time.geekbang.org/col...
本文做者: 荒古
本文连接: https://haxianhe.com/2019/07/...:如何分析、统计算法的执行效率和资源消耗?/ 版权声明: 本博客全部文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!