在学习数据结构阶段,认识的第一个概念应该就是时间复杂度了。时间复杂度是一个数学上的一个函数式,该函数式计算的基本执行次数,就是时间复杂度。通俗的说,时间复杂度就是衡量这个算法的运行时间长短,效率高低的一个手段,它是通过粗略的计算算法的执行次数来衡量的。那时间复杂度该怎么计算呢?
一、计算算法的时间复杂度:
下面先看这段代码:
//判断算法的时间复杂度
#include<stdio.h>
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
int main()
{
int N = 0;
scanf("%d", &N);
Func1(N);
return 0;
}
上面的代码中,Func1函数效率的高低,就能衡量这个函数的好坏。观察出该函数有四个循环,前两个循环是嵌套的形式,每个循环的循环次数是N次,所以一共是N×N次。第三个循环的循环次数是2×N次,第四个循环的循环次数是10次,所以该函数一共执行了N×N+2×N+10次,写成数学表达式就是Func1(N) = N² + 2×N + 10。
带入具体值是:
N值 | 准确值(Func1(N) = N² + 2 × N + 10) | 估算值(Func1(N) = N²) |
N = 1 | Func1(N) = 13 | Func1(N) = 1 |
N = 10 | Func1(N) = 130 | Func1(N) = 100 |
N = 100 | Func1(N) = 10210 | Func1(N) = 10000 |
N = 1000 | Func1(N) = 1002010 | Func1(N) = 1000000 |
N = 10000 | Func1(N) = 100020010 | Func1(N) = 100000000 |
N = 100000 | Func1(N) = 10000200010 | Func1(N) = 10000000000 |
N值越大,估算值与准确值之间相差的就越少,也就是说估算值就越接近准确值,那么就可以用估算值代替准确值,得到的执行次数就是算法Func1的大致执行次数。
实际上这样做有些过于细节了,时间复杂度求的是一个算法的大致运行次数,所以并不是精确到到底是多少次,那么时间复杂度就会存在着等级,比如O(1)、O(N)、O(2×N)、O(N²)、O(log₂N)、O(N!)、O(Nlog₂N)等,N就代表该算法大致执行了多少次,1代表该算法执行的是常数次,也就是确定的次数,这种表示的方法是大O渐进表示法。
上面的Func1函数的时间复杂度是O(N²),也就是大致执行了N²次,比如当N = 10时,Func1(N) = 100,这显然是一个大致的数字,之所以舍弃后面的次数,是因为当N无限大时,能决定具体的执行次数的是N²,不是2×N+10,所以会舍弃后面的执行次数,这点在当N无限大时就很明显。
如果算法执行次数是确定的呢,那时间复杂度又是多少?其实,不管这个确定的执行次数有多大,时间复杂度都是O(1),“1”也代表常数次,比如下面的代码:
//判断算法的时间复杂度
#include<stdio.h>
void Func3(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
int main()
{
int N = 0;
scanf("%d", &N);
Func3(N);
return 0;
}
代码就是循环100次,所以时间复杂度是O(1)。
只要是O(1)就说明该算法执行次数是常数次,不管确定的常数值多大或多小都是O(1)。这是因为CPU的处理速度已经非常快了,所以即使几十亿次的执行,也不过是几秒钟而已,当然如果一个O(1)的算法需要执行几百万亿甚至几千万亿次,这也需要很长时间的,即使这种算法的时间复杂度是O(1),实际上也是不太可能存在的。
时间复杂度大小关系为:O(log₂N) < O(N) < O(Nlog₂N) < O(N²) < O(2^N) < O(N!)。
二、时间复杂度的量级:
从上面的分析就可以看出,时间复杂度是分等级的,最快的也是最优的是O(1)、O(log₂N),其次是O(N)、O(Nlog₂N),这些时间复杂度都是比较不错的。然后是O(N²),这种时间复杂度的算法效率就很低了,在实际生活中一般是不用的,最后是O(N!),这种时间复杂度的算法也就是看看而已,很少很少存在,因为它的效率实在是太低了。
只是这么说可能不太直观的感受,下面使用具体数值举例,时间复杂度每个量级之间的差距:
O(log₂N) | O(N) | O(Nlog₂N) | O(N²) | O(2^N) | O(N!) |
1 | 1 | 1 | 1 | 1 | 1 |
7 | 100 | 700 | 10,000 | 2^100 | 100! |
14 | 10,000 | 140,000 | 100,000,000 | 没有计算必要了 | 没有计算必要了 |
17 | 100,000 | 1,700,000 | 10,000,000,000 | 没有计算必要了 | 没有计算必要了 |
20 | 1,000,000 | 20,000,000 | 没有计算必要了 | 没有计算必要了 | 没有计算必要了 |
24 | 10,000,000 | 240,000,000 | 没有计算必要了 | 没有计算必要了 | 没有计算必要了 |
27 | 100,000,000 | 2,700,000,000 | 没有计算必要了 | 没有计算必要了 | 没有计算必要了 |
30 | 1,000,000,000 | 30,000,000,000 | 没有计算必要了 | 没有计算必要了 | 没有计算必要了 |
这下就可以看出效率了,时间复杂度为O(N)的算法还算可以,之后的算法就比较拉胯了,到了O(N²)的算法基本没人使用了。
三、求时间复杂度时的预期管理:
有如下程序:
//判断算法的时间复杂度
#include<stdio.h>
const char* strchr(const char* str, int character);
int main() {}
上面的函数没有具体的实现,也就不知道是执行常数次还是N次,也就是说,最好的情况是执行1次(常数次),最坏的情况是执行N次,平均的情况(最常见的情况)是执行次数在区间[2,N-1]之之间,那就不知道这个算法到底执行多少次,这时可以采用预期管理,按照最坏的情况考虑。
遇到不确定的执行次数时,均按照最坏情况考虑。
所以上面算法的时间复杂度是O(N)。
四、求算法的时间复杂度的方法:
1、最简单的情况,直接数循环次数即可,根据循环次数判断是在哪个量级。
2、大部分的算法都需要特殊考虑,到底执行次数是在哪个量级,比如下面的代码:
//判断算法的时间复杂度
#include<stdio.h>
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
int main()
{
size_t N = 0;
scanf("%d", &N);
Fib(N);
return 0;
}
这个算法看起来很简单,功能是求第N个斐波那契数,但是想要计算出时间复杂度还是比较复杂的。首先算法中含有函数递归,每一次函数递归都会开辟一个函数栈帧,这也算执行了一次,那问题就转换成这个算法到底递归了多少次即可。
函数递归过程如下:
Fib(N)函数递归过程:
Fib(N) 2º
Fib(N-1) Fib(N-2) 2¹
Fib(N-2) Fib(N-3) Fib(N-3) Fib(N-4) 2²
Fib(N-3) Fib(N-4) Fib(N-4) Fib(N-5) Fib(N-4) Fib(N-5) Fib(N-5) Fib(N-6) 2³
... ...
Fib(5)
Fib(4) Fib(3)
Fib(3) return = 1 return = 1 return = 1
return = 1 return = 1
从上面的递归过程发现,前期的调用大概是按照2ⁿ的形式调用的,后面当N<3时,就会返回1。所以如果是按照量级看来,大致计算执行次数应该是2º + 2¹ + 2² + 2³ + ... + 2^(n - 1) = 2ⁿ - 1。所以时间复杂度就是O(2ⁿ)。
错位相减计算法:
S(n) = 2º + 2¹ + 2² + 2³ + ... + 2^(n - 1)
2×S(n) = 2¹ + 2² + 2³ + ... + 2ⁿ
可知S(n) = 2ⁿ - 2º = 2ⁿ - 1。
还有一种求法,就是画出前几个递归图后,就大概判断出执行次数差不多是2ⁿ,少算的那部分根本不用管,这样就推测出时间复杂度是O(2ⁿ)了,这个也是求算法的时间复杂度中比较难的一个了。
以上就是我这次分享的内容啦!写的不好,请多担待!