首先介绍3种表示复杂度的符号:
- Tight 上下界,f = tight(g)表示当输入大于某个整数时,存在常数c1和c2,使得 c1*g < f < c2*g ;
- Lower 下界, f = Lower(g)表示当n足够大的时候,存在c,使得 f<g *c;
- Upper上界, f = upper(g) 表示你足够大的时候,存在c使得 f> g * c;
算法复杂度分析就是求g的过程。通常来讲,算法的上界复杂度才有意义,即第二种,它表现了算法的优劣。
递归算法的复杂度求法不同于普通的算法,以归并排序为例,它的复杂度可以表示如下:
T(n) = 2 * T(n/2) +Lower(n)
它表示规模为n的算法所需时间是规模为n/2的两倍加上对整个数组进行扫描的时间。
最简单的求解算法是替换法,它由两个步骤组成:
- 猜测复杂度函数;
- 用猜测的函数替换递归表达式中的复杂度函数,并推导等式是否成立;
以上面的递归表达式为例,我们猜测复杂度为g=nlg(n),这也意味着当n最够大的时候f< n*lg(n)*c
现在用这个表达式替换 T(n/2),并推导出T(n)也符合这个猜测:
T(n) = 2 * T(n/2) +Lower(n)
<= 2 * n/2 *lg(n/2)*c+c2 * n
= n *(lg(n) -1) *c + c2*n
= n * lg(n)* c + (c2-c)* n
< c * n * lg(n)
最后一步假设c2<c。由于这个两个常数都是自己选定的,所以可以做到这一点。
在使用替换方法时,最难的是猜测出表达式的形式 g。同时要注意到g中的c是常数,不能随着n而变化。有时可以通过变量替换的方法,将近似的表达式变换成熟悉的表达式。
使用递归树的方法可以较容易的猜测出表达式的形式。递归树的思想是,输入为n的复杂度是几个较小输入的复杂度的总和,这样一层一层的划分下去,会形成一个树。在树的每一层会增加复杂度,最后树的叶子节点是简单的操作,我们可以认为他的复杂度为常数c。这样我们只需要求的叶子节点的数量,以及树的层数,就可以计算出整个算法的复杂度。
以上面的归并排序算法为例,假设有n个节点,那么每个父亲两个孩子,所以有lg(n)层,每层复杂度为n,最后叶节点是n个。所以复杂度是n * lg(n) * c + n, 由于n的次数较低,所以略去。
利用递归树可以得到一个递归复杂度的通解,如下所示:
0=========(x / V)----+++++(x)+++++-------(x*V)^^^^^^^^^
首先给所有的递归复杂度一个统一的定义:
T(n)=aT(n/b)+f(n), a>1,b>1
那么x表示n的lg(a)/lg(b)次方。
V是n的一个未知的正数次方,任何正数都可以,0.1,0.0001,1,100。
上面那一排表示f(n)可能的复杂度,可以看到从左到右,逐渐增大。随着f(n)的复杂度不同,算法的复杂度也不一样。
- 在====区域,算法复杂度由x/v确定,T(n)=tight(x)
- 在++++区域,复杂度为 T(n)=Tight(X*lg(n))
- 在^^^^区域,复杂度由f(n)确定,T(n)=Tight(f(n))
最后一个有一个额外的限制,不过我还不明白为什么需要这个限制,这个限制是:a* f(n/b)<=c*f(n)其中c是小于1的常数。
推理很简单,设f(n)=n的y次方。
那么进入第三种情况说明,f(n)>x,也就是说y大于lg(a)/lg(b),也就说明a<b的y次方。
再回到a*f(n/b)<c f(n)上来,如果要满足题意,只需要则a* (n/b)的y次方<c*n的次方,那么只需要a<c*b的y次方。
由于a,b,y都是常数,必然可以求得一个c使得上面的等式成立。
最后是一个思考题,a[1...n]存放0到n共n+1个整数,只能进行一个操作"读取第i个元素的第j个bit",如何在Lower(n)的复杂度内找到丢失那个数。
我想到的方法是我们可以知道在0到31个bit上,所有整数共有多少个1是确定的。那么对n个数扫描32遍,肯定能发现在某几个位置上1丢失了,把这些1组合起来,就是我们丢失的数组。
不知道更好的解法是什么。