复杂度分析基础
算法时间复杂度分析
-
事后统计分析方法:
编写算法对应程序,统计其执行时间。但实际统计出来的时间会受到诸多因素的影响,例如程序设计语言(汇编语言写出来的程序绝对无敌),执行程序的环境(机器处理性能)等等 -
事前估算分析方法:
避开上面因素的干扰,认为算法的执行时间是问题规模n的函数
分析步骤
- 求出算法所有原操作的执行次数(也称为频度),它是问题规模n( 一般是正整数 )的函数,用T(n)表示
- 算法执行时间大致 = 原操作所需的时间 × T(n),即T(n)与算法的执行时间成正比,可以用T(n)表示算法的执行时间
- 比较不同算法的T(n)大小得出算法执行时间的好坏
T(n)和O(n)
T(n)是问题规模n的某个函数f(n),记作:
T
(
n
)
=
O
(
f
(
n
)
)
T(n) = O( f(n) )
T(n)=O(f(n))
O表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同
O
的
定
义
:
T
(
n
)
=
O
(
f
(
n
)
)
表
示
∃
M
>
0
,
使
得
当
n
≥
n
0
(
n
足
够
大
的
时
候
)
时
都
满
足
:
\begin{aligned} &O的定义:\\ &T(n) = O( f(n) )表示\exist M>0,使得当n \ge n~0~(n足够大的时候)时都满足:\\ \end{aligned}
O的定义:T(n)=O(f(n))表示∃M>0,使得当n≥n 0 (n足够大的时候)时都满足:
∣
T
(
n
)
∣
≤
M
∣
f
(
n
)
∣
|T(n) | \le M| f(n) |
∣T(n)∣≤M∣f(n)∣
即f(n)是T(n)的上界,通常取最接近的上界
lim
n
→
∞
T
(
n
)
f
(
n
)
=
M
\lim\limits_{n\rightarrow \infin} \frac {T(n)} {f(n)} = M
n→∞limf(n)T(n)=M
当问题规模n最够大时,可以忽略低阶项和常系数,我们只需求出T(n)的最高阶,比较T(n)本质上在比较最高数量级
T
(
n
)
=
2
n
2
+
2
n
+
1
=
O
(
n
2
)
T(n) = 2n^2+2n+1 = O(n^2)
T(n)=2n2+2n+1=O(n2)
时间复杂度的比较关系
O ( 1 ) < O ( log 2 N ) < O ( n ) < O ( n log 2 N ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) O(1) < O(\log_2N) < O(n) < O(n\log_2N) < O(n^2) < O(n^3) < O(2^n) < O(n!) O(1)<O(log2N)<O(n)<O(nlog2N)<O(n2)<O(n3)<O(2n)<O(n!)
实际上这个log的底数并不一定是2,底数会根据具体的情况而变化,在后面的例题中我们就会遇见这种情况
O(1) | 没有循环的算法。其执行时间与问题规模n无关 |
O(n) | 只有一重循环的算法。其执行时间与问题规模n的增长呈线性关系 |
上下文和嵌套的关系
设算法A的复杂度T1(n)=O(f1(n)),算法B的复杂度为T2(n)=O(f2(n))
A
B
互
为
上
下
文
关
系
时
:
T
1
(
n
)
+
T
2
(
n
)
=
max
(
O
(
f
1
(
n
)
)
,
O
(
f
2
(
n
)
)
)
A
B
互
为
嵌
套
关
系
时
:
T
1
(
n
)
+
T
2
(
n
)
=
O
(
f
1
(
n
)
×
f
2
(
n
)
)
\begin{aligned} AB互为上下文关系时:T_1(n) + T_2(n) &= \max( O( f_1(n)), O(f_2(n)))\\ AB互为嵌套关系时:T_1(n) +T_2(n) &= O( f_1(n) \times f_2(n)) \end{aligned}
AB互为上下文关系时:T1(n)+T2(n)AB互为嵌套关系时:T1(n)+T2(n)=max(O(f1(n)),O(f2(n)))=O(f1(n)×f2(n))
下面举具体的例子来分析
/*
**算法A
*/
for( i = 0; i < n; i++ ){
printf("%d", i);
}
/*
**算法B
*/
for( j = 0; j < n; j++ ){
printf("%d", j);
}
不难看出
T
A
(
n
)
=
T
B
(
n
)
=
O
(
n
)
T_A(n) = T_B(n)=O(n)
TA(n)=TB(n)=O(n)
for( i = 0; i < n; i++ ){
printf("%d", i);
for( j = 0; j < n; j++ ){
printf("%d", j);
}
}
AB互为嵌套关系时
T
总
(
n
)
=
T
A
(
n
)
×
T
B
(
n
)
=
O
(
n
2
)
T_总(n) = T_A(n) \times T_B(n)=O(n^2)
T总(n)=TA(n)×TB(n)=O(n2)
简化的算法时间复杂度分析
算法中的基本操作一般是最深层循环内的原操作。计算T(n)时仅仅考虑基本操作的运算次数。
例题1
求这段代码的时间复杂度
int cnt = 0;
for( k = 1; k <= n; k *= 2 ){
for( j = 1; j <= n; j++ ){
cnt++;
}
解:
由题易得,内层循环n次
设外层循环x次,由题设:2x = n 解得x = log2n
同时两者互为嵌套关系,所以总时间复杂度为两者相乘,T(n) = O(nlog2 n)
例题2
求这段代码的时间复杂度
int i = 0, s = 0;
while( s < n ){
i++;
s += i;
}
解:
变量i从0开始递增1,s可视为首项为0,公差为1的等差数列的和。设循环次数为x
s
=
x
(
x
+
1
)
2
s = \frac {x(x+1)} 2
s=2x(x+1)
循环结束时:
s
=
x
(
x
+
1
)
2
≥
n
s = \frac {x(x+1)} 2 \ge n
s=2x(x+1)≥n
加上一个修正常量k,把不等式变成等式
x
(
x
+
1
)
2
+
k
=
n
\frac {x(x+1)} 2 + k = n
2x(x+1)+k=n
解得
x
=
−
1
+
8
n
−
8
k
+
1
2
x = \frac {-1+ \sqrt {8n - 8k + 1}} 2
x=2−1+8n−8k+1
只考虑最高数量级,即
T
(
n
)
=
x
=
O
(
n
)
T(n) = x = O(\sqrt n)
T(n)=x=O(n)
算法的空间复杂度分析
空间复杂度用于量度一个算法在运行过程中临时占用的存储空间的大小。一般也作为问题规模n的函数,采用数量级形式描述,记作
S
(
n
)
=
O
(
g
(
n
)
)
S(n) = O(g(n))
S(n)=O(g(n))
如果一个算法的空间复杂度表示为O(1),即空间复杂度与问题规模n无关,我们称这个算法为原地工作或就地工作算法。
平均时间复杂度
设一个算法的输入规模为n,Dn是所有输入的集合,任一输入I∈Dn,P(I) 是I出现的概率
∑
I
∈
D
n
P
(
I
)
=
1
\displaystyle\sum_{I\in D_n}P(I) = 1
I∈Dn∑P(I)=1
T(I) 是算法在输入I下的执行时间,则算法平均时间复杂度为
A
(
n
)
=
∑
I
∈
D
n
P
(
I
)
×
T
(
I
)
A(n) = \displaystyle\sum_{I\in D_n}P(I) \times T(I)
A(n)=I∈Dn∑P(I)×T(I)
算法的最坏时间复杂度W(n)
W
(
n
)
=
max
I
∈
D
n
{
T
(
I
)
}
W(n)=\max_{I\in D_n}\{ T(I)\}
W(n)=I∈Dnmax{T(I)}
算法的最好时间复杂度B(n)
B
(
n
)
=
min
I
∈
D
n
{
T
(
I
)
}
B(n)=\min_{I\in D_n}\{ T(I)\}
B(n)=I∈Dnmin{T(I)}
最好和最坏通常是指一种或几种特殊情况
例题
分析算法的平均时间复杂度。其中 1 <= i <= n
int
fun( int a[], int n, int i )
{
int j, max = a[0];
for( j = 1; j <= i - 1; j++ ){
if( a[j] > max ){
max = a[j];
}
}
return max;
}
对于求前i个元素的最大值时,需要元素比较i-1次(最大值初始化为a[0],从a[1]开始比较)
在每种情况出现的概率相等,概率均为1/n的情况下
平均时间复杂度:
A
(
n
)
=
∑
i
=
0
n
1
n
×
(
i
−
1
)
=
1
n
∑
i
=
0
n
i
−
1
=
n
−
1
2
A(n) = \displaystyle\sum^n_{i=0} \frac 1 n \times ( i - 1)= \frac 1 n\displaystyle\sum^n_{i=0} i - 1 = \frac {n-1} 2
A(n)=i=0∑nn1×(i−1)=n1i=0∑ni−1=2n−1
平均时间算法复杂度为O(n)
当i = n时 ,有最坏时间复杂度O(n)
当i = 1时,有最好时间复杂度O(1)
递归算法的时空复杂度分析
递归的算法是指算法中出现调用自己的成分。
递归算法也称为变长时空分析,非递归算法分析也称为定长时空分析。两者的分析方法大有不同。
求解递归算法时空复杂度的一般步骤
- 确定问题规模n
- 确定终止情况
- 确定递推情况
- 由递推关系求出T(n)
- 用复杂度表示T(n)
例题
分析时间复杂度和空间复杂度
void
fun( int a[], int n, int k )
{
int i;
if( k == n - 1 ){
for( i = 0; i < n; i++ ){ //执行n次
printf("%d\n", a[i]);
}
}else{
for( i = k; i < n; i++ ){ //执行n-k次
a[i] += i * i;
}
fun(a,n,k+1);
}
}
时间复杂度
设fun( a, n, k )的执行时间为T(n),fun( a, n, k1 )的执行时间为T1( n, k1 )
T
(
n
)
=
T
1
(
n
,
0
)
T(n) = T_1( n, 0 )
T(n)=T1(n,0)
递推关系
当
k
=
n
−
1
时
,
T
1
(
n
,
k
)
=
n
其
他
情
况
,
T
1
(
n
,
k
)
=
(
n
−
k
)
+
T
1
(
n
,
k
+
1
)
\begin{aligned} 当k=n-1时,T_1(n,k)&=n\\ 其他情况,T_1(n,k)&=(n-k)+T_1(n,k+1) \end{aligned}
当k=n−1时,T1(n,k)其他情况,T1(n,k)=n=(n−k)+T1(n,k+1)
则
T
(
n
)
=
T
1
(
n
,
0
)
=
n
+
T
1
(
n
,
1
)
=
n
+
(
n
−
1
)
+
T
1
(
n
,
2
)
=
n
+
(
n
−
1
)
+
.
.
.
+
2
+
T
1
(
n
,
n
−
1
)
=
n
+
(
n
−
1
)
+
.
.
.
+
2
+
n
=
(
n
−
1
)
(
n
+
2
)
2
+
n
=
O
(
n
2
)
\begin{aligned} T(n)&=T_1(n,0)\\ &=n+T_1(n,1)\\ &=n+(n-1)+T_1(n,2)\\ &=n+(n-1)+...+2+T_1(n,n-1)\\ &=n+(n-1)+...+2+n\\ &=\frac {(n-1)(n+2)} 2 + n\\ &= O(n^2) \end{aligned}
T(n)=T1(n,0)=n+T1(n,1)=n+(n−1)+T1(n,2)=n+(n−1)+...+2+T1(n,n−1)=n+(n−1)+...+2+n=2(n−1)(n+2)+n=O(n2)
空间复杂度
设fun( a, n, k )的空间为S(n),fun( a, n, k1 )的空间为S1( n, k1 )
S
(
n
)
=
S
1
(
n
,
0
)
S(n) = S_1( n, 0 )
S(n)=S1(n,0)
递推关系
当
k
=
n
−
1
时
,
S
1
(
n
,
k
)
=
n
其
他
情
况
,
S
1
(
n
,
k
)
=
1
+
S
1
(
n
,
k
+
1
)
\begin{aligned} 当k=n-1时,S_1(n,k)&=n\\ 其他情况,S_1(n,k)&=1+S_1(n,k+1)\\ \end{aligned}
当k=n−1时,S1(n,k)其他情况,S1(n,k)=n=1+S1(n,k+1)
则
S
(
n
)
=
S
1
(
n
,
0
)
=
1
+
S
1
(
n
,
1
)
=
1
+
1
+
S
1
(
n
,
2
)
=
1
+
1
+
.
.
.
+
1
=
O
(
n
)
\begin{aligned} S(n)&=S_1(n,0)\\ &=1+S_1(n,1)\\ &=1+1+S_1(n,2)\\ &=1+1+...+1\\ &=O(n)\\ \end{aligned}
S(n)=S1(n,0)=1+S1(n,1)=1+1+S1(n,2)=1+1+...+1=O(n)