时间复杂度
-
时间复杂度 :是指执行当前算法所消耗的时间,记作: T(n) = O(f(n))。它表示随着 输入大小n 的增大,算法执行需要的时间的增长速度可以用 f(n) 来描述。
常见的时间复杂度量级有:
-
常数阶O(1)
-
对数阶O(logN)
-
线性阶O(n)
-
线性对数阶O(nlogN)
-
平方阶O(n²)
-
立方阶O(n³)
-
K次方阶O(n^k)【n的k次方,符号不会敲】
-
指数阶(2^n)
上面从上至下依次的时间复杂度越来越大,执行的效率越来越低。
-
-
如果一个算法的执行次数是 T(n),那么只保留最高次项,同时忽略最高项的系数后得到函数 f(n),此时算法的时间复杂度就是 O(f(n))。为了方便描述,下文称此为 大O推导法。
由此可见,由执行次数 T(n) 得到时间复杂度并不困难,很多时候困难的是从算法通过分析和数学运算得到 T(n)。对此,提供下列四个便利的法则,这些法则都是可以简单推导出来的,总结出来以便提高效率。
- 对于一个循环,假设循环体的时间复杂度为 O(n),循环次数为 m,则这个
循环的时间复杂度为 O(n×m)。
void aFunc(int n) { for(int i = 0; i < n; i++) { // 循环次数为 n printf("Hello, World!\n"); // 循环体时间复杂度为 O(1) } }
此时时间复杂度为 O(n × 1),即 O(n)。
- 对于多个循环,假设循环体的时间复杂度为 O(n),各个循环的循环次数分别是a, b, c…,则这个循环的时间复杂度为 O(n×a×b×c…)。分析的时候应该由里向外分析这些循环。
void aFunc(int n) { for(int i = 0; i < n; i++) { // 循环次数为 n for(int j = 0; j < n; j++) { // 循环次数为 n printf("Hello, World!\n"); // 循环体时间复杂度为 O(1) } } }
此时时间复杂度为 O(n × n × 1),即 O(n^2)。
- 对于顺序执行的语句或者算法,总的时间复杂度等于其中最大的时间复杂度。
void aFunc(int n) { // 第一部分时间复杂度为 O(n^2) for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { printf("Hello, World!\n"); } } // 第二部分时间复杂度为 O(n) for(int j = 0; j < n; j++) { printf("Hello, World!\n"); } }
此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。
- 对于条件判断语句,总的时间复杂度等于其中 时间复杂度最大的路径 的时间复杂度。
void aFunc(int n) { if (n >= 0) { // 第一条路径时间复杂度为 O(n^2) for(int i = 0; i < n; i++) { for(int j = 0; j < n; j++) { printf("输入数据大于等于零\n"); } } } else { // 第二条路径时间复杂度为 O(n) for(int j = 0; j < n; j++) { printf("输入数据小于零\n"); } } }
此时时间复杂度为 max(O(n^2), O(n)),即 O(n^2)。
斐波那契数列:
long aFunc(int 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(2^n)。对数阶O(nlogN)
int i = 1; while(i<n) { i = i * 2; }
在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 2 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = log2n【这里是log 2的n次方,符号不会敲】
也就是说当循环 log2n【这里是log 2的n次方,符号不会敲】 次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)线性对数阶O(nlogN)线性对数阶O(nlogN) 其实非常容易理解,将时间复杂度为O(logn)的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN)。
就拿上面的代码加一点修改来举例:
for(m=1; m<n; m++) { i = 1; while(i<n) { i = i * 2; } }
时间复杂度分析的基本策略是:从内向外分析,从最深层开始分析。如果遇到函数调用,要深入函数进行分析。
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n) 来定义。
空间复杂度比较常用的有:O(1)、O(n)、O(n²),我们下面来看看:
空间复杂度 O(1)
如果算法执行所需要的临时空间不随着某个变量n的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
举例:int i = 1; int j = 2; ++i; j++; int m = i + j;
代码中的 i、j、m 所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
空间复杂度 O(n)
我们先看一个代码:
int[] m = new int[n] for(i=1; i<=n; ++i) { j = i; j++; }
这段代码中,第一行new了一个数组出来,这个数据占用的大小为n,这段代码的2-6行,虽然有循环,但没有再分配新的空间因此主要看第一行 O(n)
- 对于一个循环,假设循环体的时间复杂度为 O(n),循环次数为 m,则这个