一 : 函数渐进的界
如果存在常数N和c,对于任意的N≤n, 都满足f(n)≤cg(n),则称g(n)是f(n)的上界g(n),记作f(n)=O(g(n))(采用大O表示法)
二: 算法的时间复杂度定义
上诉定义中:求得的g(n)就是算法的时间复杂度。
三: 算法的时间复杂度求解步骤
⑴ 找出算法中的基本语句;
算法中执行次数最多的那条语句就是基本语句,通常是最内层循环的循环体。
⑵ 计算基本语句的执行次数的数量级;
只需计算基本语句执行次数的数量级,这就意味着只要保证基本语句执行次数的函数中的最高次幂正确即可
⑶ 用大Ο记号表示算法的时间性能。
将基本语句执行次数的数量级放入大Ο记号中。
四:算法时间复杂度一些简单实例
1.常量阶
int Demo(void) {
printf("Hello, World!\n"); // 需要执行 1 次
return 0; // 需要执行 1 次
}
上述算法执行了2次。因而直接用常数阶表示 f(n)=O(1)
2.线性阶
int Demo(int n) {
for(int i = 0; i<n; i++) { // 需要执行(n + 1) 次
printf("Hello, World!\n"); // 需要执行 n 次
}
return 0; // 需要执行 1 次
}
上诉算法执行了f(n)=n+1+n+1=2n+2。因而直接用线性阶表示f(n)=O(n)
这里补充为什么2n+2次可以直接写成n阶?
由于函数上界的定义:如果存在常数N和c,使得N≤n,并且满足f(n)≤cg(n) ,就可以表示成f(n)=O(g(n))
2n+2的常数项2对2n+2的影响并不大,因为函数的增长速度主要受到最高阶影响,显然存在常数c满足上式条件。
所以g(n)=n,g(n)=n²,g(n)=n³ 等等都满足上诉条件,但我们选择最接近f(n)的一个函数。
3.平方阶
void Demo(int n) {
for(int i = 0; i < n; i++) { // 循环次数为 n
for(int j = 0; j < n; j++) { // 循环次数为 n
}
}
}
上诉算法执行f(n)=n²。因而f(n)=O(n²)
void Demo(int n) {
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
}
}
}
当 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。
4.对数阶
while( i < n )
{
i = i * 2;
}
假设有x个2相乘后大于或等于n,则会退出循环。由2^x = n得到x = log(2)n,因而f(n)=O(logn)
5.条件语句
void Demo(int n) {
if (n >= 0) {
// 路径时间复杂度为 O(n²)
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²), O(n)) , f(n) = O(n²)
常用的时间复杂度所耗费的时间从小到大依次是:
O(1) < O(logn) < (n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
五:算法的最好情况,最坏情况和平均情况
查找一个有n个随机数字数组中的某个数字。最好情况是第一个数字就是,那么算法的时间复杂度为O(1),但也有可能这个数 字就在最后一个位置,那么时间复杂度为O(n)。平均运行时间是期望的运行时间。最坏运行时间是一种保证。在应用中,这是一种最重要的需求,通常除非特别指定,否则我们提到的运行时间都是最坏情况的运行时间。
以上例题只是一些简单入门的时间复杂度的计算。解决这类问题关键要能够自己正确表示出算法运行的次数,读者可以自行找一些难度较高习题进行尝试计算. 比如顺序查找,二分查找,斐波那契数列等的时间复杂度计算。