知识
分治模式
- 分解:原问题分为若干子问题,这些子问题都是原问题的规模较小的实例。
- 解决:递归求解各子问题。当子问题规模足够小则直接求解。
- 合并:合并这些子问题的解成原问题的解。
归并排序
- 分解:分解带排序的n个元素的序列为两个n/2的子序列。
- 解决:使用归并排序递归地排序两个子序列。
- 合并:合并两个已排序的子序列产生已排序的答案。
当排序序列长度为1时,递归开始回升。
下面是用RUBY的实现,没有使用哨兵牌。
def merge(arr,p,q,r) arr1 = arr[p..q] arr2 = arr[q+1..r] k = p while !(arr1.empty?&&arr2.empty?) do if arr1.empty? arr[k]=arr2.shift elsif arr2.empty? arr[k]=arr1.shift elsif arr1[0]<=arr2[0] arr[k]=arr1.shift else arr[k]=arr2.shift end k=k+1 end end def merge_sort(arr, p, r) if p < r q = (p+r)/2 merge_sort(arr,p,q) merge_sort(arr,q+1,r) merge(arr,p,q,r) end end
对于合并(merge)过程满足循环不变式:
- 初始化:当循环开始前,k=p,所以数组arr[p..k-1]为空,满足不变式。
- 保持:循环中,每一次循环都是把arr1和arr2从队列前端取较小放入arr中,而arr1和arr2是排序好的,所以保证了arr[p..k-1]也是排序好的。
- 终止:终止时k=r+1, a[p..r] 全部排序完成。
分析分治算法
递归式:根据在较小输入规模上的运行时间来描述在规模为n的问题上的总运行时间。
假设这个较小输入规模就是 n<=c, 此时 T(n) = Θ(1).
其他情况:T(n)=aT(n/b)+D(n)+C(n), 其中:
- a是被分成的子问题的个数
- n/b是子问题的输入规模(a和b不一定相同)
- D(n)是分解问题的时间
- C(n)是合并问题的时间
在归并排序算法中:
- 分解:简单的计算与规模无关,所以D(n)=Θ(1)
- 解决:分为2个规模为n/2的子问题,贡献2T(n/2)的运行时间
- 合并:merge方法可以看出,C(n)=Θ(n)
D(n)相当于常数项,可以并进C(n)中,因为C(n)相当于一个线性函数。
所以:
T(n)=Θ(1) n=1
T(n)=2T(n/2)+Θ(n) n>1
使用c代替Θ(1)(为啥Θ(n)变成了cn呢,这是一个假设。假设c为T(1)与C(n)+D(n)中较大的那个,得出的就是运行时间的上界;假设为较小的得到的就是下界,这两个界不影响时间的阶,所以可以规避这个问题。):
T(n)= c n=1
T(n)=2T(n/2)+cn n>1
下面来做一个递归树!
T(n) => cn =再展开==> cn ...
/ \ / \
T(n/2) T(n/2) cn/2 cn/2
/ \ / \
T(n/4)T(n/4) T(n/4) T(n/4)
最终:
结果出来啦,T(n) = cn*(lgn+1) = cnlgn + cn
所以,时间复杂度是Θ(nlgn)
习题
2.3-1 使用图2-4作为模型,说明归并排序在数组A=<2,41,52,26,38,57,9,49>上的操作
答:
2,9,26,38,41,49,52,57
2,26,41,52
9,38,49,57
2,41
26,52
38,57
9,49
2
41
52
26
38
57
9
49
2.3-2 重写merge,使之不用哨兵。
答:这种写法的效率经过测试慢于上面的实现方法。
def merge(arr,p,q,r) arr1 = arr[p..q] arr2 = arr[q+1..r] k = p while !(arr1.empty?&&arr2.empty?) do if arr1.empty? arr[k..r] = arr2.shift(arr2.size) elsif arr2.empty? arr[k..r] = arr1.shift(arr1.size) elsif arr1[0]<=arr2[0] arr[k]=arr1.shift else arr[k]=arr2.shift end k=k+1 end end</span>
2.3-3 使用数学归纳法证明,当n刚好是2的幂时,以下递归式的解是T(n) = nlgn
T(n) = 2 n=2
T(n) = 2T(n/2)+n n=2^k,k>1
答:
第一数学归纳法 一般地,证明一个与自然数n有关的命题P(n),有如下步骤: (1)证明当n取第一个值n0时命题成立。n0对于一般数列取值为0或1,但也有特殊情况; (2)假设当n=k(k≥n0,k为自然数)时命题成立,证明当n=k+1时命题也成立。 综合(1)(2),对一切自然数n(≥n0),命题P(n)都成立。
按照数学归纳法,
首先 n = 2时,T(n) = 2*lg2 = 2,成立。
假设,当n = 2^k时,有T(2^k) = 2^k*lg(2^k) = k*2^k。
只需要证明当n=2^(k+1)的时候,T(2^(k+1))=(k+1)*2^(k+1)成立即可。
T(2^(k+1)) = 2T(2^(k+1)/2)+2^(k+1) = 2T(2^k)+2^(k+1) = 2k*2^k + 2^(k+1) = k*2^(k+1)+2^(k+1) = (k+1)2^(k+1)成立。
所以题干得证。
2.3-4 我们可以把插入排序表示为如下的一个递归过程。为了排序A[1..n],我们递归排序A[1..n-1],然后把A[n]插入已排序的数组A[1..n-1]。
为插入排序的这个递归版本的罪化情况运行时间写一个递归式。
答:
- 分解:A[1..n]的数组,分解成A[1..n-1]和A[n]两个部分,A[1..n-1]成为新的待排序数组
- 解决:将A[1..n-1]的部分继续递归地进行排序
- 合并:将A[n]插入已排序的A[1..n-1]中
也就是说知道遇到n=2时,递归开始回升。
合并的过程就是插入排序的一次循环的内容。
T(n) = Θ(1) n<=c
T(n) = aT(n/b)+D(n)+C(n) Other
根据本题,C(n)的复杂度是Θ(n)。D(n)的复杂度是Θ(1). 由于每次都只有一个字数组进入迭代,所以a = 1, 而下一个迭代规模是 n-1,所以:
T(n) = T(n-1)+Θ(n) Other
用c代入得到递归式:
T(n) = c n=1
T(n) = T(n-1)+ cn Other
2.3-5回顾查找问题(参见练习2.1-3),注意到,如果序列A已排好序,就可以将序列的中点与v比较。根据比较结果,原序列中就有一半不用再做考虑了。
二分查找算法重复这个过程,每次都将序列的剩余部分规模减半。为二分查找写出迭代或递归的伪代码。证明:二分查找的最坏情况运行时间为Θ(lgn)
答:
递归版本:
- 分解:把A[1..n]从中间分为两部分,并取其一作为子问题的输入
- 解决:通过比较中间值取其一作为子问题的输入,进入递归继续查找
- 合并:不需要合并结果集,用尾递归直接返回值
伪代码:
迭代版本:search(A,l,r,v) if l < r m = (l+r)/2 if A[m] < v return search(A,m+1,r,v) else if A[m] > v return search(A,l,m-1,v) else return m else if A[l] == v return l else return nil
search (A,v) start = 1 end = A.size result = nil while start <= end m = (start +end)/2 if A[m]>v end = m-1 else if A[m] < v start = m+1 else result = m break
递归式:
T(n) = c n = 1
T(n) = T(n/2)+c n >1
按照上面做成递归树,共lgn+1层,所以是Θ(lgn)
2.3-6 注意到2.1节中的过程INSERTION-SORT的第5-7行的while循环采用一种线性查找来(反向)扫描已排好序的数组A[1..j-1]。我们可以使用二分查找来把插入排序的最坏情况总运行时间改进到Θ(nlgn)嘛?
答:
不可以,因为不仅仅是查找就可以,还需要把元素移动的过程,这是无法避免的。
2.3-7 描述一个运行时间为Θ(nlgn)的算法,给定n个整数的集合S和另一个整数x,该算法能确定S中是否存在两个其和刚好为x的元素。
答:
这个算法第一步是作差,第二步是在之后的集合中使用二分查找找到这个差。
外层循环需要进行(n-1)次,第二步复杂度Θ(lgn),所以(n-1)lgn取高次项为nlgn,所以是Θ(nlgn).