1. 程式人生 > >分治法的基本思想與例子解析

分治法的基本思想與例子解析

    分治法的設計思想:將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。

    凡治眾如治寡,分數是也。——孫子兵法

1.基本思想

(1) 將求解的較大規模的問題分割成k個更小規模的子問題。


(2) 對這k個子問題分別求解。如果子問題的規模仍然不夠小,則再劃分為k個子問題,如此遞迴的進行下去,直到問題規模足夠小,很容易求出其解為止。


(3) 將求出的小規模的問題的解合併為一個更大規模的問題的解,自底向上逐步求出原來問題的解。

 

2.適用條件

分治法所能解決的問題一般具有以下幾個特徵:

I. 該問題的規模縮小到一定的程度就可以容易地解決; II. 該問題可以分解為若干個規模較小的相同問題,即該問題具有最優子結構性質
III. 利用該問題分解出的子問題的解可以合併為該問題的解; IV. 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子問題。 

注意:

       如果各子問題是不獨立的,則分治法要做許多不必要的工作,重複地解公共的子問題,此時雖然也可用分治法,但一般用動態規劃較好。

3. 分治法的應用例子

(a) 快速排序

I. 分解(divide):以a[p]為基準元素將a[p:r]劃分為3段a[p:q-1],a[q]和a[q+1,r],使得a[p:q-1]中任何元素小於等於a[q],a[q+1,r]中任何元素大於等於a[q]。下標q在劃分過程中確定。

II. 遞迴求解(conquer):通過遞迴呼叫快速排序演算法,分別對a[p:q-1]和a[q+1,r]進行排序。

III. 合併(merge):由於對a[p:q-1]和a[q+1,r]的排序時就地進行的,所以在a[p:q-1]和a[q+1,r]都已排好的序後不需要執行任何計算,a[p:r]就已排好序。

package Sort;
/**
 * @author LIn
 * 演算法名稱:快速排序
 * 演算法描述:
 * 1.通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小
 * 2.重複步驟1對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。
 *
 * 複雜度分析:
 * 1.平均時間複雜度:O(nlogn)
 * 2.空間複雜度:O(logn)(最值元素儲存空間)
 */
public class QuickSort {
	
	public static void quickSort(int[] a){
		quickSort(a, 0, a.length - 1);
	}

	private static void quickSort(int[] a, int left, int right){
		int pivotpos;  //劃分後基準的位置
		if(left < right){
			pivotpos = Partition(a, left ,right);
			quickSort(a, left, pivotpos-1);
			quickSort(a, pivotpos+1, right);
		}
	}
	
	/**
	 * 普通選擇基準
	 */
	private static int Partition(int[] a, int p, int r){
		//呼叫Partition(a,left,right)時,對a[left...right]做劃分
		//並返回基準記錄的位置
		int i = p, j = r + 1;
		int pivot = a[p];  //用區間的第一個記錄作為基準
		
		while(true){
			while(a[++i] < pivot){}
			while(a[--j] > pivot){}
			if(i < j){
				swap(a, i, j);
			}
			else{
				break;
			}
		}
		
		swap(a, j, p);
		return j;
	}
	
	private static void swap(int[] a, int x, int y){
		int temp = a[x];
		a[x] = a[y];
		a[y] = temp;
	}

}

(b) 二分查詢

       合併排序演算法是用分治策略實現對n個元素進行排序的演算法。其基本思想是:將待排序元素分成大小大致相同的2個子集合,分別對2個子集合進行排序,最終將排好序的子集合合併為所要求的排好序的集合。

package Sort;
/**
 * @author LIn
 * 演算法名稱:歸併排序
 * 演算法描述:
 * 1.將陣列分為n等份(演算法中為2),對各子陣列遞迴呼叫歸併排序
 * 2.等分為2份時為2路歸併,最後子陣列排序結束後,將元素合併起來,複製回原陣列
 * 
 * 複雜度分析:
 * 1.平均時間複雜度:O(nlogn)
 * 2.空間複雜度:O(n)(臨時資料儲存空間)
 */
public class MergeSort {
	
	/*public型的mergeSort是private型遞迴方法mergeSort的驅動程式*/
	public static void mergeSort(int[] a){
		int[] tempArray = new int[a.length];   //若陣列元素為物件型別,需建立Comparable類的陣列,再強轉為該物件型別
		
		mergeSort(a, tempArray, 0, a.length - 1);
	}
	
	/**
	 * 遞迴呼叫歸併排序
	 */
	private static void mergeSort(int[] a, int[] tempArray, int left, int right){
		if(left < right){
			int center = (left + right) / 2;
			mergeSort(a, tempArray, left, center);
			mergeSort(a, tempArray, center + 1, right);
			merge(a, tempArray, left, center + 1, right);   //子陣列排序結束後,將子數組合並
		}
	}
	
	/**
	 * 合併左右的半分子陣列
	 * @param a          需排序陣列
	 * @param tempArray  臨時儲存陣列
	 * @param leftPos    左半子陣列開始的下標
	 * @param rightPos   右半子陣列開始的下標
	 * @param rightEnd   右半子陣列結束的下標
	 */
	private static void merge(int[] a, int[] tempArray, int leftPos, int rightPos, int rightEnd) {
		int leftEnd = rightPos - 1;
		int tempPos = leftPos;
		int num = rightEnd - leftPos + 1;
		
		//主迴圈
		while(leftPos <= leftEnd && rightPos <= rightEnd){
			if(a[leftPos] <= a[rightPos]){
				tempArray[tempPos++] = a[leftPos++];
			}else{
				tempArray[tempPos++] = a[rightPos++];
			}
		}
		/*比較結束後,只會有一個子陣列元素未完全被合併*/
		while(leftPos <= leftEnd){        //複製左半子陣列剩餘的元素
			tempArray[tempPos++]  = a[leftPos++];
		}
		while(rightPos <= rightEnd){      //複製右半子陣列剩餘的元素
			tempArray[tempPos++]  = a[rightPos++];
		}
		
		//將元素從臨時陣列賦值回原陣列
		for(int i = 0; i < num; i++, rightEnd--){
			a[rightEnd] = tempArray[rightEnd];
		}
		
	}

}

參考資料:

1. 《演算法設計與分析》

2. 《演算法》