一、时间频度
时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多,一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)。
举例说明:计算1-100所有数字之和,两种算法如下:
public void sum(){
int total = 0;
int end = 100;
// 使用for循环计算
for(int i = 1;i <= end; i++){
total += i;
}
System.out.println(total)
}
public void sum(){
int total = 0;
int end = 100;
total = (1 + end) * end / 2;
System.out.println(total)
}
第一种方式的时间频度是
T
(
n
)
=
n
+
1
T(n) = n + 1
T(n)=n+1,也可写作
T
(
n
+
1
)
T(n + 1)
T(n+1)
第二种方式的时间频度是
T
(
n
)
=
1
T(n) = 1
T(n)=1,也可写作
T
(
1
)
T(1)
T(1)
二、计算方式
(1)常数项
结论:
- 2 n + 20 2n+20 2n+20 和 2 n 2n 2n 随着n 变大,执行曲线无限接近, 20可以忽略
- 3 n + 10 3n+10 3n+10 和 3 n 3n 3n 随着n 变大,执行曲线无限接近, 10可以忽略
(2)低次项
结论:
- 2 n 2 + 3 n + 10 2n^2+3n+10 2n2+3n+10 和 2 n 2 2n^2 2n2 随着n 变大, 执行曲线无限接近, 可以忽略 3 n + 10 3n+10 3n+10
- n 2 + 5 n + 20 n^2+5n+20 n2+5n+20 和 n 2 n^2 n2 随着n 变大,执行曲线无限接近, 可以忽略 5 n + 20 5n+20 5n+20
(3)系数
结论:
- 随着n值变大, 5 n 2 + 7 n 5n^2+7n 5n2+7n 和 3 n 2 + 2 n 3n^2 + 2n 3n2+2n行曲线重合, 说明 这种情况下, 5和3可以忽略
- 而 n 3 + 5 n n^3+5n n3+5n 和 6 n 3 + 4 n 6n^3+4n 6n3+4n曲线分离,说明多少次方是关键
三、时间复杂度
(1)一般情况下,算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数,用
T
(
n
)
T(n)
T(n)表示;
若有某个辅助函数
f
(
n
)
f(n)
f(n),使得当n趋近于无穷大时,
T
(
n
)
/
f
(
n
)
T(n) / f(n)
T(n)/f(n) 的极限值为不等于零的常数,则称
f
(
n
)
f(n)
f(n) 是
T
(
n
)
T(n)
T(n) 的同数量级函数,记作
T
(
n
)
=
O
(
f
(
n
)
)
T(n) = O(f(n))
T(n)=O(f(n)),称
O
(
f
(
n
)
)
O(f(n))
O(f(n)) 为时间复杂度。
(2)
T
(
n
)
T(n)
T(n) 不同,但时间复杂度可能相同。 如:
T
(
n
)
=
n
2
+
7
n
+
6
T(n)=n^2+7n+6
T(n)=n2+7n+6 与
T
(
n
)
=
3
n
2
+
2
n
+
2
T(n)=3n^2+2n+2
T(n)=3n2+2n+2它们的T(n) 不同,但时间复杂度相同,都为
O
(
n
2
)
O(n^2)
O(n2)。
四、时间复杂度类型
(1)常数阶 O ( 1 ) O(1) O(1)
public void sum(int i,int j){
++i;
j++;
int m = i + j;
System.out.println(m);
}
说明:上述代码在执行的时候,它消耗的时候并不随着某个变量的增长而增长,那么无论这类代码有多长,即使有几万几十万行,都可以用 O ( 1 ) O(1) O(1)来表示它的时间复杂度。
(2)对数阶 O ( l o g N ) O(logN) O(logN)
public void test(int n){
int i = 1;
while(i < n){
i = i * 2;
}
System.out.println(i);
}
- 在while循环里面,每次都将 i 乘以 2,乘完之后,i 距 n 就越来越近,假设循环 x 次之后,i 就大于 n 了,此时这个循环就退出了,也就是说 2 的 x 次方大于 n,那么也就是说当循环 l o g 2 n log_2n log2n 次以后,这个代码就结束了。因此这个代码的时间复杂度为: O ( l o g 2 n ) O(log_2n) O(log2n)
- O ( l o g 2 n ) O(log_2n) O(log2n)的这个2 时间上是根据代码变化的,如果循环中是i = i * 3 ,时间复杂度则是 O ( l o g 3 n ) O(log_3n) O(log3n)
- 注意:算法复杂度描述中为什么用
l
o
g
N
logN
logN,而不用
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)、
O
(
l
o
g
3
n
)
O(log_3n)
O(log3n)
由换底公式 l o g x y = l o g a y l o g a x log_xy=\displaystyle \frac{log_ay}{log_ax} logxy=logaxlogay可得:
O ( l o g 2 n ) = l o g 10 n l o g 10 2 O(log_2n)=\displaystyle \frac{log_{10}n}{log_{10}{2}} O(log2n)=log102log10n
O ( l o g 3 n ) = l o g 10 n l o g 10 3 O(log_3n)=\displaystyle \frac{log_{10}n}{log_{10}{3}} O(log3n)=log103log10n
时间复杂度中的运算规则是 O ( C × f ( N ) ) = O ( f ( N ) ) O(C×f(N))=O(f(N)) O(C×f(N))=O(f(N)),其中C是一个常数,可知对数阶的时间复杂度与底数没有关系,均可以用logN代替。
(3)线性阶 O ( n ) O(n) O(n)
public void sum(int end){
int total = 0;
// 使用for循环计算
for(int i = 1;i <= end; i++){
total += i;
}
System.out.println(total)
}
说明:在这段代码中,for循环里面的代码会执行n遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用 O ( n ) O(n) O(n)来表示它的时间复杂度。
(4)线性对数阶 O ( n l o g N ) O(nlogN) O(nlogN)
public void sum(int end){
for(int i = 1;i <= end; i++){
int j = 1;
while(j < end){
j = j * 2;
}
System.out.println(j);
}
}
说明:线性对数阶 O ( n l o g N ) O(nlogN) O(nlogN)其实非常容易理解,将时间复杂度为 O ( l o g N ) O(logN) O(logN)的代码循环n遍的话,那么它的时间复杂度就是 n ∗ O ( l o g N ) n*O(logN) n∗O(logN),也就是了 O ( n l o g N ) O(nlogN) O(nlogN)。
(5)平方阶 O ( n ² ) O(n²) O(n²)
public void multiple(int end){
int mult = 0;
for(int i = 1;i <= end; i++){
int j = 1;
for(int j = 1;j <= end; j++){{
mult = i * j;
System.out.print(mult);
}
System.out.println();
}
}
说明:把 O ( n ) O(n) O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O ( n ² ) O(n²) O(n²),如果将其中一层循环n次改成m次,那它的时间复杂度就变成了 O ( m n ) O(mn) O(mn)。
五、时间复杂度小结
(1)平均时间复杂度和最坏时间复杂度
- 平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间
- 最坏情况下的时间复杂度称最坏时间复杂度
- 一般讨论的时间复杂度均是最坏情况下的时间复杂度
(2)常见的算法时间复杂度由小到大依次为: O ( 1 ) Ο(1) O(1) < O ( l o g N ) Ο(logN) O(logN) < O ( n ) Ο(n) O(n) < O ( n l o g N ) Ο(nlogN) O(nlogN) < O ( n 2 ) Ο(n^2) O(n2) < O ( n 3 ) Ο(n^3) O(n3) < O ( n k ) Ο(n^k) O(nk) < O ( 2 n ) Ο(2^n) O(2n) ,随着问题规模的不断增大,上述时间复杂度不断增大,算法的执行效率越低。