分治法作爲一種常見的算法思想,其概念爲:把一個複雜的問題分成兩個或更多的相同或相似的子問題,再把子問題分成更小的子問題,直到最後子問題可以簡單的直接求解,原問題的解即子問題的解的合併。
分治法的策略是:對於一個問題,若該問題可以容易地解決(比如說規模較小)則直接解決,否則將其分解爲若干個規模較小的子問題(這些子問題互相獨立且與原問題形式相同),遞歸地求解這些子問題,然後將各子問題的解合併得到原問題的解。
可以用分治法解決的問題一般有如下特徵:
1>問題的規模縮小到一定的程度就可以容易地解決。此特徵是大多數問題所具備的,當問題規模增大時,解決問題的複雜度不可避免地會增加。
2>問題可以分解爲若干個規模較小的相同問題,即該問題具有最優子結構性質。此特徵也較爲常見,是應用分治法的前提。
3>拆分出來的子問題的解,可以合併爲該問題的解。這個特徵在是否採用分治法的問題上往往具有決定性作用,比如棋盤覆蓋、漢諾塔等,需要將子問題的解彙總,纔是最終問題的解。
4>拆分出來的各個子問題是相互獨立的,即子問題之間不包含公共的子問題。該特徵涉及到分治法的效率,如果各子問題是不獨立的,則需要重複地解公共的子問題,此時用動態規劃法更好。
使用分治法的基本步驟:
1>分解,將原問題分解爲若干個規模較小、相互獨立、與原問題形式相同的子問題。
2>解決,若子問題規模較小而容易被解決則直接解,否則遞歸地解各個子問題。
3>合併,將各個子問題的解合併爲原問題的解。
二分查找是一種常見的高效查找方式,也是常見的分治法的例子。同時,二分查找有着較爲嚴格的要求,即二分查找要求線性表必須採用順序存儲結構,而且表中元素按關鍵字有序排列。
二分查找的大致查找過程爲:假設線性表中元素是按升序排列,將表中間位置記錄的關鍵字與查找關鍵字比較,如果兩者相等,則查找成功。否則利用中間位置記錄將表分成前、後兩個子表,如果中間位置記錄的關鍵字大於查找關鍵字,則進一步查找前一子表,否則進一步查找後一子表。重複以上過程,直到找到滿足條件的記錄,查找成功;或直到子表不存在爲止,此時查找不成功(查找關鍵字不在線性表中)。
二分查找用Java代碼表示爲:
int[ ] arr = {2,3,5,8,10};
int result = binarySearch(arr,0,arr.length-1,5);
System.out.println("result:"+result);
private static int binarySearch(int[ ] arr,int start,int end,int target){
if(end < start){
return -1;
}
if(start <= end)
{
int mid = (start + end)/2;
if(target == arr[mid])
{
return mid;
}else if(arr[mid] > target)
{
return binarySearch(arr, start, mid-1,target);
}
else
{
return binarySearch(arr, mid+1,end,target);
}
}
return -1;
}
漢諾塔也是典型的使用分治法的例子之一,該問題源於印度一個古老傳說的益智玩具。大梵天創造世界的時候做了三根柱子,在一根柱子上從下往上按照大小順序摞着64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序重新擺放在另一根柱子上。並且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。
該問題的分析過程,可以用這樣一句話簡單概括,將1-n個盤子,藉助B塔,實現從A塔到C塔的搬運。用分析的思想來解釋的話,就是如下三步:
1>分:將第1到n-1個盤子從A塔搬到B塔;
2>治:將第n個盤子從A塔搬到C塔;
3>循環地分治:將第n-1個盤子從B塔搬到C塔。
Java代碼如下:
public static void main(String[] args) {
hanoi(3, "A", "B", "C");
}
public static void hanoi(int n, String source, String temp, String target) {
if (n == 1) {
//如果只有1個盤子,直接從source移動到target
move(n, source, target);
} else {
//將第1個盤子到第n-1個盤子由source經過target移動到temp,繼續遞歸
hanoi(n - 1, source, target, temp);
//移動盤子n由source移動到target,實現移動
move(n, source, target);
//把之前移動到tempTower的第1個盤子到第n-1個盤子n-1,由temp經過source移動到target,繼續遞歸
hanoi(n - 1, temp, source, target);
}
}
//第n個盤子的從source移動到target
private static void move(int n, String source, String target) {
System.out.println("第" + n + "號盤子,從:" + source + " 移動到 " + target);
}
歸併排序是另一個常見的使用分治思想的例子,針對這個問題,網上有個經典的圖解,如下:
從這個圖能看出來,歸併排序就是個不斷拆分、最後再組合的過程,用分治的說法描述是:
1>分:不斷拆解、直到每個子數組數量爲1的左右子數組。
2>治:左右子數組不斷合併。
分拆的過程比較好理解,就是不斷通過遞歸來實現數組“由大到小”的變化過程。合治的過程需要分情況討論:
1>兩個子數組恰好一組一個進行合併;
2>左邊子數組已經合併完了,此時只需要將右邊子數組插入到有序數組中;
3>右邊子數組已經合併完了,此時只需要將左邊子數組插入到有序數組中。
Java代碼如下:
//歸併排序
public static void main(String[] args) {
int[ ] arr = {5,3,1,6,2};
int[ ] temp = new int[ 5 ];
mergeSort(arr,0,arr.length-1,temp);
for(int i=0; i < arr.length; i++)
System.out.print(arr[ i ]+" ");
}
//將兩個有序數組合並排序
private static void mergeArray(int a[ ],int first,int mid,int last,int temp[ ])
{
int i=first,j=mid+1;
int m=mid,n=last;
int k=0;
//兩個子數組同時遍歷
while(i <= m && j <= n)
{
if(a[ i ] < a[ j ])
temp[ k++ ]=a[ i++ ];
else
temp[ k++ ]=a[ j++ ];
}
//遍歷前半個子數組
while(i <= m)
temp[ k++ ]=a[ i++ ];
//遍歷後半個子數組
while(j <= n)
temp[ k++ ]=a[ j++ ];
//賦值給原數組
for(i=0; i < k; i++)
a[ first+i ]=temp[ i ];
}
//將兩個任意數組合並排序
private static void mergeSort(int[ ] arr,int first,int last,int[ ] temp)
{
if(first < last)
{
int mid=(first+last)/2;
mergeSort(arr,first,mid,temp); //遞歸處理左邊子數組
mergeSort(arr,mid+1,last,temp); //遞歸處理右邊子數組
mergeArray(arr,first,mid,last,temp); //將由原始數組分隔而成的兩個有序子數組合並
}
}