一、分治与递归
分治的设计思想是:
将一个大问题,分割成一些规模比较小的相同问题,以便各个击破,分而治之。
分治法的流程:
- 分治法产生具有相同性质的较小规模子问题。
- 反复应用分治手段,可以使子问题规模不断缩小
- 最终使子问题缩小到很容易直接算出其解
- 将规模较小的问题的答案逐级向上合并,可得大问题答案。
分治由上到下分解大问题,由下到上合并子问题;这正是递归的过程,所以分治法通常使用递归算法完成。
递归算法的模板:
Type abc(参数){
if(满足边界条件){
边界条件的处理
}else{
非边界条件的处理:
(1)分:问题划分为子问题
(2)治:对各个子问题递归调用去解决
(3)合:合并子问题的解为问题的解
}
}
例一:阶乘函数
public int fact(int n){
if(n==0)return 1;//边界条件
else return n*fact(n-1)
//将问题分解成n-1规模的子问题
//子问题也是个递归调用
//使用子问题组成问题的解并返回
}
例二:Fibonacci数列
public int fibonacci(int n){
if(n<=1) return 1;
else return fibonacci(n-1)+fibonacci(n-2);
}
上述两个例子也可以使用非递归的方式描述,如下图:
- 当问题可以使用非递归的方式解决时,推荐使用非递归的方式去完成。
- 因为递归解决问题的方式包括一来一回,所以占用的内存空间和时间代价会比较大。
- 递归的有点在于结构清晰,可读性强,易用数学归纳法来整明算法的正确性。
例三:排列问题
设计一个递归算法生成n个元素
{
r
1
,
r
2
,
⋯
,
r
n
}
\{r_{1},r_{2},\cdots,r_{n}\}
{r1,r2,⋯,rn}的全排列。
- 设 R = { r 1 , r 2 , ⋯ , r n } R = \{r_{1},r_{2},\cdots,r_{n}\} R={r1,r2,⋯,rn}, R i = R − { r i } R_{i}=R-\{r_i\} Ri=R−{ri}
- 集合 X X X中的全排列记为 p e r m ( X ) perm(X) perm(X)
- ( r i ) p e r m ( X ) (r_i)perm(X) (ri)perm(X)表示在全排列perm(X)的每一个排列前加上前缀得到的排列。
该问题的递归定义:
- 当n==1时, p e r m ( R ) = ( r ) perm(R)=(r) perm(R)=(r),r为集合R中的唯一的元素【边界条件】
- 当n>1时, p e r m ( R ) perm(R) perm(R)由 ( r 1 ) p e r m ( R 1 ) , ( r 2 ) p e r m ( R 2 ) , ⋯ , ( r n ) p e r m ( R n ) (r_1)perm(R1),(r_2)perm(R2),\cdots,(r_n)perm(Rn) (r1)perm(R1),(r2)perm(R2),⋯,(rn)perm(Rn)构成【递归函数】
例四:Hanoi塔问题
递归定义如下:
- 当n==0,不需要移动
- 当n>0:
将A柱上的n-1个圆盘移动到C柱上;再将A最大的圆盘移动到B柱上。
接下来再将C柱上的n-1个圆盘,通过A柱移动到B柱上
假设hanoi(a,b,c,n)表示:
将a上的n个圆盘借助c柱,移动到b柱上。
根据上述的递归定义,可分解成:
- hanoi(a,c,b,n-1):将a上的n-1个圆盘借助b柱,移动到c柱上
- 将a柱圆盘移动到b柱
- hanoi(c,b,a,n-1):将c上的n-1个圆盘借助a柱,移动到b柱上
代码实现:
public class Main{
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
String A="A",B="B",C="C";
hanoi(A,B,C,3);
}
public static void hanoi(String A,String B,String C,int n) {
if(n>0) {
hanoi(A, C,B, n-1);
move(A,B);
hanoi(C,B,A, n-1);
}
}
public static void move(String A,String B) {
System.out.println(A+"-->"+B);
}
}
输出结果:
A-->B
A-->C
B-->C
A-->B
C-->A
C-->B
A-->B
二、分治法适应条件与时间复杂度
分治法所能解决的问题一般具有以下几个特征:
- 该问题的规模缩小到一定程度就可以容易地解决;
- 该问题可以分解成若干个规模较小的相同问题;(最优子结构性质)
- 利用该问题分解出的子问题的解可以合并为该问题的解;(最优子结构性质)
- 该问题所分解出的各个子问题是相互独立的。(无重复子问题)
分治法的基本步骤:
divide-and-conquer(P){
if(|P|<=n0)adhoc(P);//边界条件,解决小规模问题
divide P into smaller subinstances P1,P2,……,Pk//分解成若干规模较小的相同问题
for(i=1;i<=k;i++){
yi=divide-and-conquer(Pi);//递归求解各子问题
}
return merge(y1,y2……,yk);//各子问题的解合并为原问题的解
}
分治法中,将一个问题分成大致相等的k个子问题的处理方式是最有效的,即平衡子问题的思想。
分治法的时间复杂度度分析:
分治法将规模为n的问题分解成k个规模为n/m的子问题去解。
- 当遇到边界条件,即n==1时,规模为1的问题耗费1个时间单位。
- 将问题分解成k个子问题及用merge将k个子问题的解合并成原问题的解需要f(n)个时间单位。
- T(n)表示分治法解规模为|P|=n的问题所需要的计算时间
T ( n ) = { O ( 1 ) , n = 1 k T ( n / m ) + f ( n ) , n > 1 T(n)=\begin{cases} O(1), & \text{n\ = \ 1} \\ kT(n/m)+f(n), & \text{n\ >\ 1} \\ \end{cases} T(n)={O(1),kT(n/m)+f(n),n = 1n > 1
即当n>1时,时间复杂度为k个子问题的时间复杂度与合并k个子问题时间复杂度之和。
例一:二分搜索技术
给定按升序排好序的n个元素
a
[
0
:
n
−
1
]
a[0:n-1]
a[0:n−1],现要在这n个元素中找出一特定元素x。
分析:
- 该问题的规模缩小到1时,可以直接给出答案
- 该问题可以分解成若干个规模较小的子问题。(x要么在左半边,要么在右半边,可分解成在左/右半边寻找的子问题)
- 分解出的子问题的解可以合并为原问题的解;(子问题的解就是原问题的解)
- 分解出的各个子问题是相互独立的。
结论:可以使用分治法
思路:
设在a[l:r]中找x,m=(l+r)/2
(0) 如果x==a[m],则找到
(1)如果x<a[m],则在左半边a[l:m]中找x即可
(2)如果x>a[m],则在右半边a[m+1:r]中找x即可
子问题的答案就是大问题的答案
则将一个较大规模的问题分解成了一个较小规模的子问题
代码:
int BiSearch(int a[],int x,int l,int r){
if(r>=l){
int m = (l+r)/2;
if(x==a[m])return m;
else if(x<a[m]) return BiSearch(a,x,l,m-1);
else return BiSearch(a,x,m+1,r);
}else return -1;
}
时间复杂度:
T
(
n
)
=
{
O
(
1
)
,
n = 1
T
(
n
/
2
)
+
1
,
n > 1
T(n)=\begin{cases} O(1), & \text{n\ = \ 1} \\ T(n/2)+1, & \text{n\ >\ 1} \\ \end{cases}
T(n)={O(1),T(n/2)+1,n = 1n > 1
规模为n的问题分解成了原问题规模一半的解,所以分解及解决子问题的时间复杂度为T(n/2);子问题的解就是原问题的解,所以合并子问题的解时间复杂度为1。
三、快速幂算法
给定实数a和正整数n,用分治法设计求 a n a^{n} an的快速算法(递归算法)。
分析:
a n = { 0 , a = 0 1 , n = 0 ( a n 2 ) 2 , n > 0,n为偶数 ( a n 2 ) 2 ∗ a , n > 0,n为奇数 a^{n}=\begin{cases} 0, & \text{a\ = \ 0} \\ 1, & \text{n\ = \ 0} \\ (a^{\frac{n}{2}})^{2}, & \text{n\ > \ 0,n为偶数} \\ (a^{\frac{n}{2}})^{2}*a, & \text{n\ > \ 0,n为奇数} \\ \end{cases} an=⎩ ⎨ ⎧0,1,(a2n)2,(a2n)2∗a,a = 0n = 0n > 0,n为偶数n > 0,n为奇数
该问题满足分治的四个条件,估可使用分治法解决。
代码:
public double exp2(double a,int n){
if(a==0)return 0;
if(n<=0)return 1;
else{
double x = exp2(a,n/2);
if(n%2==1)return a*x*x;
else return x*x;
}
}
四、Strassen矩阵乘法
问题:
两个
n
×
n
n\times n
n×n矩阵A和B的乘积矩阵C,C中有
n
×
n
n\times n
n×n个元素,C中元素
C
[
i
,
j
]
C[i,j]
C[i,j]定义为:
C中的一个元素
C
[
i
]
[
j
]
C[i][j]
C[i][j]需要做n次乘法和n-1次加法,因此算出矩阵C的
n
×
n
n\times n
n×n个元素所需要的计算时间复杂性为
O
(
n
3
)
O(n^3)
O(n3).
尝试使用分治法优化该问题:
将矩阵A,B和C中每一矩阵都分块成4个大小相等的子矩阵。由此可将方程C=AB重写为: .
时间复杂度分析:
T
(
n
)
=
{
O
(
1
)
,
n = 2
8
T
(
n
/
2
)
+
O
(
n
2
)
,
n >2
T(n)=\begin{cases} O(1), & \text{n\ = \ 2} \\ \\ 8T(n/2)+O(n^2), & \text{n\ >2} \\ \end{cases}
T(n)=⎩
⎨
⎧O(1),8T(n/2)+O(n2),n = 2n >2
- T ( n / 2 ) T(n/2) T(n/2):
将原问题分解成了8个问题规模为 n / 2 n/2 n/2的子问题- O ( n 2 ) O(n^2) O(n2):
C 11 C_{11} C11问题的合并相加需要进行 ( n / 2 ) 2 (n/2)^2 (n/2)2次加法,总的问题合并规模则为 O ( n 2 ) O(n^2) O(n2)
该方法的复杂度依然是 O ( n 3 ) O(n^3) O(n3),没有得到改进。
为了降低复杂度,必须减少乘法的次数。进一步改进算法,如下图:
复杂度分析:
五、合并排序
基本思想:
将待排序元素分成大小大致相同的2个集合,分别对2个子集进行排序,最终将排好序的子集合并成为所要求的排好序的集合。
实例举例:
- 最坏时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
- 平均时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
- 辅助空间: O ( n ) O(n) O(n)
- 稳定的排序算法