一.复杂度的定义.
复杂度:复杂度是衡量一个代码质量的重要指标,主要分为时间复杂度与空间复杂度,其中又以时间复杂度更为重要.
复杂度记号:一个算法
X
X
X的复杂度上界
O
(
X
)
O(X)
O(X)定义为
X
X
X的运行总量多项式只保留最高次项并去掉常数后的值,例如:
O
(
3
n
2
+
5
n
+
7
)
=
O
(
n
2
)
O
(
7
8
n
+
10
log
3.4
n
)
=
O
(
n
)
O
(
5
n
2
log
3.5
3
(
100
n
)
)
=
O
(
n
2
log
3
n
)
O
(
2
n
+
2
+
8
n
7
)
=
O
(
2
n
)
O(3n^{2}+5n+7)=O(n^{2})\\ O(7\sqrt{8n}+10\log_{3.4} n)=O(\sqrt{n})\\ O(5n^{2}\log_{3.5}^{3}(100n))=O(n^{2}\log^{3} n)\\ O(2^{n+2}+8n^{7})=O(2^{n})
O(3n2+5n+7)=O(n2)O(78n+10log3.4n)=O(n)O(5n2log3.53(100n))=O(n2log3n)O(2n+2+8n7)=O(2n)
当然有些时候常数对代码的效率影响也很大,但在复杂度计算中不予考虑.
其实记号 O O O表示的只是一个上界,也就是说一个算法的复杂度为 O ( n 2 ) O(n^2) O(n2),那么它的复杂度也可以被写为 O ( n 3 ) O(n^3) O(n3).
再来一些实例解释一下吧,比如说下面这串代码:
int ans=0;
for (int i=1;i<=n;i+=5)
for (int j=n;j>=10;--j)
for (int k=1;k<=n;k+=2) ++ans;
这串代码的时间复杂度就是 O ( n 3 ) O(n^3) O(n3).
在比如说下面这串代码:
int ans=0;
for (int i=1;i<=n;i<<=1)
for (int j=2;j<1<<i;++j)
for (int k=4;k*k<=n;++k) ++ans;
这串代码的时间复杂度就是 O ( 2 n n log n ) O(2^{n}\sqrt{n}\log n) O(2nnlogn).
除此之外,复杂度还有记号例如
Ω
(
n
)
\Omega(n)
Ω(n)和
Θ
(
n
)
\Theta(n)
Θ(n),其中
Ω
\Omega
Ω表示下界,
Θ
\Theta
Θ表示上下都有界.
二.积分估计计算时间复杂度.
若对于某一个和式:
∑
i
=
1
n
f
(
i
)
\sum_{i=1}^{n}f(i)
i=1∑nf(i)
其中 f ( x ) f(x) f(x)是一个多项式,但这个东西并不好估计它的具体值,我们该如何计算呢?
有一个非常简单的方法,直接将这个和式看成积分的离散表示,然后直接用积分算复杂度.
由于复杂度计算的只是大概值,所以这样算是没有关系的.
也就是说:
O
(
∑
i
=
1
n
f
(
i
)
)
=
O
(
∫
1
n
f
(
i
)
d
i
)
O\left(\sum_{i=1}^{n}f(i)\right)=O\left(\int_{1}^{n}f(i)\mathrm{d}i\right)
O(i=1∑nf(i))=O(∫1nf(i)di)
一个经典的例子是计算如下和式在复杂度表示中的值:
O
(
n
∑
i
=
1
n
1
i
)
=
O
(
n
∫
i
=
1
n
1
i
d
i
)
=
O
(
n
log
n
)
O\left(n\sum_{i=1}^{n}\frac{1}{i}\right)=O\left(n\int_{i=1}^{n}\frac{1}{i}\mathrm{d}i\right)=O(n\log n)
O(ni=1∑ni1)=O(n∫i=1ni1di)=O(nlogn)
三.主定理.
对于一个递归函数:
T
(
n
)
=
a
T
(
n
b
)
+
f
(
n
)
T(n)=aT(\frac{n}{b})+f(n)
T(n)=aT(bn)+f(n)
如何计算 O ( T ( n ) ) O(T(n)) O(T(n))呢?
主定理:对于
T
(
n
)
=
a
T
(
n
b
)
+
f
(
n
)
T(n)=aT(\frac{n}{b})+f(n)
T(n)=aT(bn)+f(n),则有:
O
(
T
(
n
)
)
=
{
O
(
n
log
b
a
)
∃
k
>
0
⇒
f
(
n
)
=
O
(
n
log
b
a
−
k
)
O
(
n
log
b
a
log
k
+
1
n
)
∃
k
>
0
⇒
f
(
n
)
=
O
(
n
log
b
a
log
k
n
)
O
(
f
(
n
)
)
∃
k
>
0
⇒
f
(
n
)
=
Ω
(
n
log
b
a
+
k
)
O(T(n))= \left\{\begin{matrix} O(n^{\log_{b}a})&\exists k>0\Rightarrow f(n)=O(n^{\log_{b}a-k})\\ O(n^{\log_{b}a}\log^{k+1}n)&\exists k>0\Rightarrow f(n)=O(n^{\log_{b}a}\log^{k}n)\\ O(f(n))&\exists k>0\Rightarrow f(n)=\Omega(n^{\log_{b}a+k}) \end{matrix}\right.
O(T(n))=⎩⎨⎧O(nlogba)O(nlogbalogk+1n)O(f(n))∃k>0⇒f(n)=O(nlogba−k)∃k>0⇒f(n)=O(nlogbalogkn)∃k>0⇒f(n)=Ω(nlogba+k)
有了主定理我们就可以算很多分治算法的复杂度了,例如:
T
(
n
)
=
2
T
(
n
2
)
+
O
(
1
)
⇒
O
(
T
(
n
)
)
=
O
(
n
)
T
(
n
)
=
T
(
n
2
)
+
O
(
1
)
⇒
O
(
T
(
n
)
)
=
O
(
log
n
)
T
(
n
)
=
2
T
(
n
2
)
+
O
(
n
log
n
)
⇒
O
(
T
(
n
)
)
=
O
(
n
log
2
n
)
T
(
n
)
=
2
T
(
n
2
)
+
O
(
n
2
)
⇒
O
(
T
(
n
)
)
=
O
(
n
2
)
T(n)=2T(\frac{n}{2})+O(1)\Rightarrow O(T(n))=O(n)\\ T(n)=T(\frac{n}{2})+O(1)\Rightarrow O(T(n))=O(\log n)\\ T(n)=2T(\frac{n}{2})+O(n\log n)\Rightarrow O(T(n))=O(n\log^2 n)\\ T(n)=2T(\frac{n}{2})+O(n^2)\Rightarrow O(T(n))=O(n^2)
T(n)=2T(2n)+O(1)⇒O(T(n))=O(n)T(n)=T(2n)+O(1)⇒O(T(n))=O(logn)T(n)=2T(2n)+O(nlogn)⇒O(T(n))=O(nlog2n)T(n)=2T(2n)+O(n2)⇒O(T(n))=O(n2)
四.均摊分析.
有些时候,一个算法有 n n n步,处理每一步的时候复杂度可能达到 O ( n ) O(n) O(n),但其总复杂度却可以保证 O ( n ) O(n) O(n),是不是非常神奇?
是的,确实有这种算法,而且还有不少,例如KMP就是其中最经典的一种.
那么它的复杂度是怎么保证的呢?有一个叫均摊复杂度方法可以证明.
例如KMP的时间复杂度分析过程:我们注意到KMP的每执行一次 j = n e x t [ j ] j=next[j] j=next[j]都会使得 j j j至少 − 1 -1 −1,而总共 n n n步中 j j j每步最多只能 + 1 +1 +1,且 j j j无论何时都是 > 0 >0 >0的,所以它的复杂度是 O ( n ) O(n) O(n)的.
这就是一种均摊时间复杂度的方法.