1. 概述
从大一到大三,迷迷糊糊看了时间复杂度三年多,今天终于把它搞清楚了。
首先,我们需要知道的是 时间复杂度要计算的是一个程序大致执行了多少个语句(之前我认为是要计算到底执行了多少秒,是我天真了,怎么可能了),时间复杂度的计算允许省去一些影响小的因素(比如说后面会提到的n对n2的影响小,这时我们就只要考虑n2就行)。
2. 时间复杂度的计算步骤
关于时间复杂度的计算可以分为三类:
- 简单由执行次数得到时间复杂度
- 通过分析和数学运算得到时间复杂度
- 递归算法的时间复杂度
2.1 简单由执行次数得到时间复杂度
假设一些简单的算法我们能一眼看出总执行次数,我们拿到总执行次数根据下面的原则可以得到最后的时间复杂度。
- **常数项省略:**我们知道常数项对函数的增长速度影响并不大,所以当 T(n) = c,c 为一个常数的时候,我们说这个算法的时间复杂度为 O(1);如果 T(n) 不等于一个常数项时,可以直接将常数项省略。
- **低次项省略:**我们知道高次项对于函数的增长速度的影响是最大的。n^3 的增长速度是远超 n^2 的,同时 n^2 的增长速度是远超 n 的。 同时因为要求的精度不高,所以我们直接忽略低此项。
- **与最高项的常数乘省略:**因为函数的阶数对函数的增长速度的影响是最显著的,所以我们忽略与最高阶相乘的常数。
总结:如果一个算法的执行次数是 T(n),那么只保留最高次项,同时忽略最高项的系数后得到函数 f(n),此时算法的时间复杂度就是 O(f(n)),我们将这种方法称为大O推导法
2.2 通过分析得到时间复杂度
插播:常见时间复杂度的大小性质和比较:
由执行次数得到时间复杂度很容易,但是大多数情况都不能看出时间复杂度,需要通过一定的分析和数学运算得到执行次数T(n)。对此,我们提供了下面四个便利的法则来推导出T(n)。
- **循环的计算:**对于一个循环,假设循环体的时间复杂度为 O(n),循环次数为 m,则这个
循环的时间复杂度为 O(n×m)。
// 此时时间复杂度O(n*1)
void aFunc(int n) {
for(int i = 0; i < n; i++) { // 循环次数为 n
printf("Hello, World!\n"); // 循环体时间复杂度为 O(1)
}
}
- 多个循环的计算:对于多个循环,假设循环体的时间复杂度为 O(n),各个循环的循环次数分别是a, b, c…,则这个循环的时间复杂度为 O(n×a×b×c…)。分析的时候应该由里向外分析这些循环。
// 此时时间复杂度为O(n*n*1)
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)
}
}
}
- 对于顺序执行的语句或者算法,总的时间复杂度等于其中最大的时间复杂度。
// 时间复杂度为max(O(n^2),O(n)),即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");
}
}
}
2.3 通过数学运算得到时间复杂度
在一些情况下,我们需要使用简单数列求和对数求解来得到时间复杂度
void aFunc(int n) {
for (int i = 2; i < n; i++) {
i *= 2;
printf("%i\n", i);
}
}
/*
假设循环次数为 t,则循环条件满足 2^t < n。
可以得出,执行次数t = log(2)(n),即 T(n) = log(2)(n),可见时间复杂度为 O(log(2)(n)),即 O(log n)。
*/
void aFunc(int n) {
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
printf("Hello World\n");
}
}
}
/*
当 i = 0 时,内循环执行 n 次运算,当 i = 1 时,内循环执行 n - 1 次运算……当 i = n - 1 时,内循环执行 1 次运算。
所以,执行次数 T(n) = n + (n - 1) + (n - 2)……+ 1 = n(n + 1) / 2 = n^2 / 2 + n / 2。
根据上文说的 大O推导法 可以知道,此时时间复杂度为 O(n^2)。
*/
这里涉及到数列求和的运算,对于数列求和的公式为Sn=n*a1+1/2*n(n-1)d。其中d为两个相邻数的等差值
2.4 递归算法的时间复杂度
设递归算法的时间复杂度为T(n),则可以设递归算法内部的递归为T(m) (m为在调用递归时传入的规模大小,常见的m有n-1或n/2等),则可以根据T(m)的运算得到一个T(n)。我们通过一道具体汉若塔问题解释迭代法具体是什么样的。
// 执行次数为T(n)
void hano(char A,char B,char C,int n){
if(n>0){
hano(A,C,B,n-1); //执行次数为T(n-1)
move(A,C); //执行次数为1
hano(B,A,C,n-1); //执行次数为T(n-1)
}
}
根据上面的算法,我们设递归算法的执行次数为T(n)。则有T(n)=2*T(n-1)+1。
(1)迭代法
如果使用迭代法,我们就需要将T(n)=2 * T(n-1)+1展开,得到最终的T(n)=2k*T(n-k)+(2k-1)=>T(n)=(2 * 2^n) - 1=>O(n)=2^n
(2)公式法
在很多情况下,迭代法使用起来比较困难。当递归算法的执行次数函数T(n)满足如下公式时,就可以利用公式法得到算法的时间复杂度:
T(n)=a*T(n/b)+f(n) f(n)值每次递归完毕后,额外的计算执行时间
当参数a、b都确定时,递归部分时间复杂度为:O(n^logba)
那么最后的时间复杂度为多少了?O(n^logba)?不是的,因为f(n)也需要考虑,具体分为三种情况:
- 情况一:当O(nlog<sub>b</sub>a)>f(n)的时候,最终时间复杂度为O(nlogba)
- 情况二:当O(n^logba)<f(n)的时候,最终时间复杂度为f(n)
- 情况三:当O(nlog<sub>b</sub>a)=f(n)的时候,最终的时间复杂度为O(nlogba)logn
本文深入浅出地介绍了时间复杂度的计算方法,包括常数项、低次项和最高项常数乘的省略原则,以及如何通过分析和数学运算得到时间复杂度。覆盖简单执行次数、循环、递归算法的时间复杂度计算。
1122

被折叠的 条评论
为什么被折叠?



