1.为什么要衡量时间复杂度?
用于衡量算法的优劣
2.衡量时间复杂度的方法
事后统计:依赖硬件环境、受测试数据规模影响大
事前分析:直观、快捷
3.事后统计
斐波那契数列:这个数列是从第3项开始,每一项都等于前两项之和
0 1 1 2 3 5 8 1 3
求出第n个数字?
-
递归实现
public static long fun1(long n){` if(n<=1) return n; return fun1(n-1)+fun1(n-2); }
-
循环实现
public static long fun2(long n){ if(n<=1) return n; else { int first=0; int second=1; for(int i=0;i<n-1;i++) //通过观察发现要求第n个数字,前面需要加n-1次,循环n-1次 int third = first+second; first = second; second = third; } } }
-
输出
小笔记:
- 递归会耗时比较大
- 【算法】确定重复的步骤(第1位+第2位 = 第3位 ——> 第1、2位向右移动1位),确定循环次数,循环就可以写出了
4.时间复杂度表达式
T ( n ) = O ( f ( n ) ) T(n)=O(f(n)) T(n)=O(f(n))
- T(n)代表代码执行的时间
- n表示规模的大小
- f(n)表示每行代码执行的次数的总和
- O 表示代码的执行时间T(n)与f(n) 表达式成正比
- 大O时间复杂度 实际上并不具体代表代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势,所以也叫做渐进时间复杂度(asymptotic time complexity),简称时间复杂度
- 分类:最好、最坏、平均,一般计算的都是最坏时间复杂度
5.计算方法
(实际直接看循环就好啦)
1.分号结束只一次(赋值、判断、返回语句等)
2.循环注意判次数(循环次数:对于i<g(n), 假设循环x次跳出循, 列出等式 f(i) = g(n) , 求出x)
多层循环分内外(外层循环走多次,内层循环走一次)
循环计算乘后加( 内层循环总执行次数 * 外层循环次数 + 外层循环执行次数)
3.忽略低阶常系数(关注影响最大的项)
例子1—单层循环:
//时间复杂度 3+3n ------- f(n) = O(n)
public int cal(int n){
int sum = 0;//执行1次
//这个循环循环n次,所以总共是 1+(1+1)* n + 1* n = 3n+1
for(int i = 1; i <= n; i++){
// 执行1次;执行1*n次;执行1*n次
sum = sum + i;//执行1*n次
}
return sum;//执行1次
}
//注意:循环内的判断条件其实会多判断一次
第一步:
分号结束只一次(如代码中标注)
第二步:
循环注意判次数(i=1; i<=n ;i++ 循环n次)
多层循环分内外(这里只有一个循环)
循环计算乘后加( 内层循环总执行次数 = 1*n 次)
第三步:
忽略低阶常系数(3+3n ------> f(n) = O(n))
例子2—循环次数:
//时间复杂度 1+log2(n) ------- f(n) = O(logn)
public void cal02 (int n){
int i = 1;//执行1次
while(i < n){
i = i * 2;//假设循环x次,等比数列,当 2^x>=n 跳出循环,x=log2(n)
//执行log2(n)*1次
}
}
小笔记:对于步长为1的循环,为1有等(号),循环n次,无等-1;为0无等,循环n次,有等+1
例子3—嵌套循环:
//时间复杂度 1+63n ------- f(n) = O(n)
public void cal(int n){
//嵌套循环共执行 内层循环总执行次数 * 外层循环次数 + 外层循环执行次数
// = (1+20+20+20) * n + 2n+1 = 1+63n
for(int i = 0; i < n; i++){
// 执行1次;执行n次;执行n次 外层循环共执行 2n+1 次
for(int j = 0; j < 20; j++){
//执行1次;执行1*20次;执行1*20次
System.out.print("Hello");//执行1*20次
}
}
}
第一步:
分号结束只一次(如代码中标注)
第二步:
循环注意判次数(外循环n次 内循环20次)
多层循环分内外
循环计算乘后加( 内层循环总执行次数 * 外层循环次数 + 外层循环执行次数 = 61*n + 2n+1 )
第三步:
忽略低阶常系数(1+63n ------- f(n) = O(n))
例子4—递归:
public static long fun1(long n){
if(n<=1) return n;
return fun1(n-1)+fun1(n-2);
}
6.常见的时间复杂度量级
常数阶O(1)
首先介绍顺序结构的时间复杂度
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是O(1),如:
int i = 1;
int j = 2;
++i;
j++;
int m = i + j;
上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用O(1)来表示它的时间复杂度。
注意:不管这个常数是多少,都记作O(1),而不能记作O(3),O(12)等其他任何数字。
对于分支结构无论判断条件是真还是假,执行的次数都是恒定的,不会随着n的变大而发生变化,所以单纯的分支结构(不包含在循环结构中),其时间复杂度都是O(1)。
线性阶O(n)
线性阶的循环结构会复杂很多,要确定某个算法的阶次,需要确定某个特定语句运行的次数,因此分析算法的复杂度,关键就是要分析循环结构的运行情况。
「一个循环」,算法需要执行的运算次数用输入大小n的函数表示,即 T(n) 。
for(int i=1;i<=n;i++){
System.out.println(i);
}
「一个循环」,算法需要执行的运算次数用输入大小n的函数表示,即 T(n) 。
for(int i=1;i<=n;i++){
System.out.println(i);
}
for(int i=1;i<=n;i++){
System.out.println(i);
}
如果是for循环并列关系那么n会执行2n次,忽略常数也是O(n)
对数阶O(logn)
一般省略底数
int i = 1;
while(i<n){
i = i * 2;
}
从上面代码可以看到,在while循环里面,每次都将 i 乘以 2,乘完之后,i 距离 n 就越来越近了。我们试着求解一下,假设循环x次之后,i 就大于 n 了,此时这个循环就退出了,也就是说 2 的 x 次方等于 n,那么 x = l o g 2 n log_{2}n log2n
也就是说当循环 l o g 2 n log_{2}n log2n以后,这个代码就结束了。一般忽略省略底数,因此这个代码的时间复杂度为:O(logn).
平方阶O(n^2)
举例:
for(i=1; i<=n; i++){
for(j=1; j<=n; j++){
j = i;
j++;
}
}
这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²)
如果将其中一层循环的n改成m,即:
for(i=1; i<=m; i++){
for(j=1; j<=n; j++){
j = i;
j++;
}
}
那它的时间复杂度就变成了 O(m*n),所以总结循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。下面这个循环嵌套它的时间复杂度又是多少呢?
for(i=0; i<n; i++){
for(j=i; j<n; i++){
Sytem.out.println("111");
}
}
由于i=0时,内循环执行了n次,当i=1时,执行了n-1次…当i=n-1时,执行了1次,所以总共执行了:n+(n-1)+(n-2)+…+1=n(n+1)/2=n^2/2+n/2
使用推导大O阶的方法:最终为n^2
线性对数阶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;
}
}
立方阶O(n³)、K次方阶O(n^k)
参考上面的O(n²) 去理解就好了,O(n³)相当于三层n循环,其它的类似。
但是O(n³)过大的n都会使得结果变得不现实,同样O(n²) 和O(n!)等除非是很小的n值,否则哪怕n只是100都是噩梦般的运行时间,所以这种不切实际的算法时间复杂度一般我们不讨论。
7.不同时间复杂度效率问题
O( n )、O( log(n) ) 最好
8.练习题
【ACM】设某算法的计算时间表示为递推关系式T(n)=T(n-1)+n (n为正整数) 以及 T(0) =1, 则该算法的时间复杂度为()
答案:O(n^2)
在动手的过程中,发现不知道替换出后面实数那部分,是先写出T(1) + 2 + 3 … + (n-2) + (n-1) + n 再根据上面的规律写出T(2) + 3 … + (n-2) + (n-1) + n