时间复杂度的概念
将算法中所有语句被重复执行的次数之和记为T(n),它是问题规模n的函数,时间复杂度主要分析T(n)的数量级。
例如我们观察以下代码:
public static void main(String[] args) {
int i = 0;
int n = 10;
while(i < n){
System.out.printf("我被执行了%d次!\n", i);
i++;
}
}
/*
OUTPUT:
我被执行了0次!
我被执行了1次!
我被执行了2次!
我被执行了3次!
我被执行了4次!
我被执行了5次!
我被执行了6次!
我被执行了7次!
我被执行了8次!
我被执行了9次!
Process finished with exit code 0
*/
上述代码经过简单的分析,不难发现
int i = 0;
执行了1次
int n = 10;
执行了1次
System.out.printf("我被执行了%d次!\n", i);
执行了10次,因为n为变量(可修改为任何值),记为n次
i++;
执行了n次
共计1 + 1 + n + n = 2n+2次,假如每句代码执行时间为1ms,则总执行时间为2n + 2 * 1(ms) = 2n + 2(ms)
时间复杂度的计算
概念
我们现在已经知道时间复杂度是和代码语句的执行次数成正比的关系了,我们经常在各种地方看到O(n),O(n2)等时间复杂度的表达方式,这与我们所说的2n + 2截然不同,这些时间复杂度是如何表示的呢。
其实,时间复杂度仅仅需要分析问题规模函数T(n)的数量级即可,并非需要复杂精细的计算,我们将算法中基本运算(即基础语句)执行的次数的公式记为f(n),则按照上述例子,f(n) = 2n + 2,分析其数量级并记为T(n),T(n)与函数f(n)的对应关系为:
T
(
n
)
=
O
(
f
(
n
)
)
<
=
=
>
lim
n
→
∞
T
(
n
)
f
(
n
)
=
k
(
k
为常数
)
T(n)=O(f(n)) <==> \displaystyle\lim_{n\to\infty}\frac {T(n)}{f(n)} = k (k为常数)
T(n)=O(f(n))<==>n→∞limf(n)T(n)=k(k为常数)
这里可能对微积分不了解的朋友看起来会比较晦涩,我们可以这样表示,例如2n + 1
,我们可以取公式中最大的n作为其数量级,而
lim
n
→
∞
\displaystyle\lim_{n\to\infty}
n→∞lim即为n无限接近无穷大(无穷大可以浅显理解为没有任何一个数比它大),以上公式即可表示为:
lim
n
→
∞
n
2
n
+
1
=
k
(
k
为常数
)
\displaystyle\lim_{n\to\infty}\frac {n}{2n+1} = k (k为常数)
n→∞lim2n+1n=k(k为常数)
至于计算过程,我们可以将该公式上下同除n,即可化为:
lim
n
→
∞
1
2
+
1
n
\displaystyle\lim_{n\to\infty}\frac {1}{2+\frac{1}{n}}
n→∞lim2+n11,而
lim
n
→
∞
1
n
\displaystyle\lim_{n\to\infty}\frac{1}{n}
n→∞limn1可以理解为无限接近于0(因为是1除以一个无穷大的东西,任何常数除以无穷大都近似等0),也就可以相当于0,直接可以得出结果为
1
2
\frac{1}{2}
21,结果为一个常数。
假如不能理解以上计算,也可以直接通过“取大头”的方式计算简单时间复杂度的数量级,如2n2+n+1,即可直接表示为O(n2)。
计算
例如我们观察以下代码:
void cal(int n){
for(int i = 0; i < n; i++){
System.out.println("outer");
System.out.println("outer");
for(int j = 0; j < n; j++){
System.out.println("inner");
}
}
}
不难发现,其中System.out.println("outer");
代码执行了2n次,而System.out.println("inner");
执行了System.out.println("看稀罕的");
次,System.out.println("看稀罕的");
仅执行了一次,我们可以将其总和记为n2+2n+1,通过"取大头"的方法,我们可以直接得出,该代码的时间复杂度为O(n2),其计算过程为:
lim
n
→
∞
n
2
n
2
+
2
n
+
1
=
lim
n
→
∞
1
1
+
2
n
+
1
n
2
=
1
\displaystyle\lim_{n\to\infty}\frac {n^2}{n^2+2n+1} = \displaystyle\lim_{n\to\infty}\frac {1}{1+\frac{2}{n}+\frac{1}{n^2}}=1
n→∞limn2+2n+1n2=n→∞lim1+n2+n211=1
灰常的简单。
但是,时间复杂度的计算并非如此简单,我们还可能遇到一些复杂的计算方式,例如:
void cal(int n){
int i = 0;
while(i <= n){
i = i * 2;
}
}
我们发现,i并非按照加法进行计算,而是按照2, 4, 8, 16...
的速度在增加,我们该如何统计它的执行次数呢?
其实,我们可以总结出,它在while
中的公式可以写为2x <= n,然后对两边同取对数
log
2
\log_2
log2,即可得到
x
<
=
log
2
n
x<=\log_2n
x<=log2n,所以,我们即可得出它的时间复杂度为
log
2
n
\log_2n
log2n。
void cal(int n){
for(int i = 0; i < n; i++){
System.out.println("outer");
for(int j = 0; j < n; j = j * 2){
System.out.println("inner");
}
}
}
该代码的时间复杂度是多少呢?可以算一算,很简单*。
计算法则
在计算一个程序的时间复杂度时,我们也可以遵循以下公式:
加法法则: T ( n ) = T 1 ( n ) + T 2 ( n ) = O ( f ( n ) ) + O ( g ( n ) ) = O ( m a x ( f ( n ) , g ( n ) ) ) T(n)=T_1(n)+T_2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n))) T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n)))
乘法法则: T ( n ) = T 1 ( n ) ∗ T 2 ( n ) = O ( f ( n ) ) ∗ O ( g ( n ) ) = O ( f ( n ) ∗ g ( n ) ) T(n)=T_1(n)*T_2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)) T(n)=T1(n)∗T2(n)=O(f(n))∗O(g(n))=O(f(n)∗g(n))
计算max时,我们可以遵循以下比例:
O ( 1 ) < O ( l o g 2 n ) < O ( n ) < O ( n l o g 2 n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(log_2n)<O(n)<O(nlog_2n)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)
复杂度分类
最坏时间复杂度是指在最坏情况下,算法的时间的时间复杂度。
平均时间复杂度是指所有可能得输入实例等概率出现的情况下,算法的期望运行时间。
最好时间复杂度是指在最好情况下,算法的时间的时间复杂度。
*上述答案:O( n l o g 2 n nlog_2n nlog2n)