1. 程式人生 > >《演算法(第四版)》排序-----歸併排序

《演算法(第四版)》排序-----歸併排序

參考文章的地址:http://www.cnblogs.com/skywang12345/p/3602369.html

歸併排序基本原理是將兩個有序的陣列,在合併的過程中進行排序成一個更大的有序陣列。

歸併排序的時間複雜度為NlogN,缺點是需要額外的記憶體空間

歸併排序的實現方法是:建立一個適當大小的陣列然後將兩個輸入陣列中的元素一個個從小到大的放入這個陣列中。

將兩個陣列抽象化的歸併方法如下:

方法名為merge(a,lo,mid,hi),它會將子陣列a[lo...mid]  a[mid+1...hi]歸併成一個有序的陣列並將結果存放在a[lo...hi]中,

public static void merge(Comparable[] a, int lo, int mid, int hi){
//將a[lo...mid]  和a[mid+1...hi]歸併
    int i = lo;   // 第一個有序區域的索引 
    int j = mid +1;  //第二個有序區域的索引
    
    for(int k = lo; k <= hi; k++){//將a[lo...hi]複製到aux[lo...hi]中,這樣在操作a[]是資料不會被覆蓋而丟
        aux[k] = a[k];            //aux[]   是在類中建立的一個輔助陣列,它的大小和a[]的大小一樣大
    }
    
    for(int k = lo; k <=hi; k++){  //歸併到a[lo...hi]中
        if(i > mid){            //左半邊用盡,也就是當第一個有序陣列的所有資料都放入到歸併的陣列中了,由於上一次結果會i++,
                                //所以會進入到這個條件中,那就直接把右邊的往歸併數組裡扔就可以了
            a[k] = aux[j++];
        }else if(j > hi){       //同上,當右半邊資料(第二個有序區域)都放入到歸併的陣列中了,那就直接吧剩下的左邊資料(第一個有序陣列)放入歸併陣列
            a[k] = aux[i++];
        }else if(less(aux[j], aux[i])){//這句話才是歸併在比較大小的那句,誰小誰放入陣列中
            a[k] = aux[j++];
        }else{
            a[k] = aux[i++];
        }
    }
}

歸併排序一共有兩種方法,一種是自上而下的歸併排序,一種是自下而上的歸併排序

(1)自下而上的歸併排序

所謂的自下而上排序分為三個步驟

1.分解:將待排序的數列分成若干個長度為1的子數列,然後將這些數列兩兩合併;

2.合併:得到若干個長度為2的有序數列,再將這些數列兩兩合併;

3.遞迴:得到若干個長度為4的有序數列,再將它們兩兩合併;直接合併成一個數列為止。這樣就得到了我們想要的排序結果。

結合上圖自下而上的排序如下

通過"從下往上的歸併排序"來對陣列{80,30,60,40,20,10,50,70}進行排序時:
1. 將陣列{80,30,60,40,20,10,50,70}看作由8個有序的子陣列{80},{30},{60},{40},{20},{10},{50}和{70}組成。


2. 將這8個有序的子數列兩兩合併。得到4個有序的子樹列{30,80},{40,60},{10,20}和{50,70}。
3. 將這4個有序的子數列兩兩合併。得到2個有序的子樹列{30,40,60,80}和{10,20,50,70}。
4. 將這2個有序的子數列兩兩合併。得到1個有序的子樹列{10,20,30,40,50,60,70,80}。

具體程式碼實現如下:

public class Down2UpMerge {
	private static Comparable[] aux;  //歸併所需的輔助陣列
	public static void sort(Comparable[] a){
		//進行lgN次來兩兩歸併
		int N = a.length;
		aux = new Comparable[N];
		for (int sz = 1; sz < N; sz = sz +sz) {  //sz:子陣列的大小, sz成2的冪次方增長
			for(int lo = 0; lo < N-sz; lo += sz+sz){//lo:子陣列的索引,  sz=1時,子陣列有2個數,sz=2時,子陣列有4個數,sz=4時,子陣列有8個數
				merge(a, lo, lo+sz-1, Math.min(lo+sz+sz-1, N-1));
			}
		}
	}
	
	public static void merge(Comparable[] a, int lo, int mid, int hi){
		// 將a[lo...mid] 和a[mid+1...hi]歸併
		int i = lo; // 第一個有序區域的索引
		int j = mid + 1; // 第二個有序區域的索引

		for (int k = lo; k <= hi; k++) {// 將a[lo...hi]複製到aux[lo...hi]中,這樣在操作a[]是資料不會被覆蓋而丟
			// aux[] 是在類中建立的一個輔助陣列,它的大小和a[]的大小一樣大
			aux[k] = a[k]; 
		}

		for (int k = lo; k <= hi; k++) { // 歸併到a[lo...hi]中
			if (i > mid) { // 左半邊用盡,也就是當第一個有序陣列的所有資料都放入到歸併的陣列中了,由於上一次結果會i++,
							// 所以會進入到這個條件中,那就直接把右邊的往歸併數組裡扔就可以了
				a[k] = aux[j++];
			} else if (j > hi) { // 同上,當右半邊資料(第二個有序區域)都放入到歸併的陣列中了,那就直接吧剩下的左邊資料(第一個有序陣列)放入歸併陣列
				a[k] = aux[i++];
			} else if (less(aux[j], aux[i])) {// 這句話才是歸併在比較大小的那句,誰小誰放入陣列中
				a[k] = aux[j++];
			} else {
				a[k] = aux[i++];
			}
		}
	}

}
假設陣列有16個數

sz=1時

merge(a,0,0,1)

merge(a,2,2,3)

merge(a,4,4,5)

merge(a,6,6,7)

merge(a,8,8,9)

merge(a,10,10,11)

merge (a,12,12,13)

merge(a,14,14,15)

sz=2

merge(a,0,1,3)

merge(a,4,5,7)

merge (a,8,9,11)

merge(a,12,13,15)

sz = 4

merge (a,0,3,7)

merge(a,8,11,15)

sz = 8

merge(a,0,7,15)

(2)自上而下歸併排序

所謂的自上而下歸併排序,它與"從下往上"在排序上是反方向的。它基本包括3步:
① 分解 -- 將當前區間一分為二,即求分裂點 mid = (low + high)/2; 
② 求解 -- 遞迴地對兩個子區間a[low...mid] 和 a[mid+1...high]進行歸併排序。遞迴的終結條件是子區間長度為1。
③ 合併 -- 將已排序的兩個子區間a[low...mid]和 a[mid+1...high]歸併為一個有序的區間a[low...high]。

結合上圖:

通過"從上往下的歸併排序"來對陣列{80,30,60,40,20,10,50,70}進行排序時:
1. 將陣列{80,30,60,40,20,10,50,70}看作由兩個有序的子陣列{80,30,60,40}和{20,10,50,70}組成。對兩個有序子樹組進行排序即可。
2. 將子陣列{80,30,60,40}看作由兩個有序的子陣列{80,30}和{60,40}組成。
    將子陣列{20,10,50,70}看作由兩個有序的子陣列{20,10}和{50,70}組成。
3. 將子陣列{80,30}看作由兩個有序的子陣列{80}和{30}組成。
    將子陣列{60,40}看作由兩個有序的子陣列{60}和{40}組成。
    將子陣列{20,10}看作由兩個有序的子陣列{20}和{10}組成。
    將子陣列{50,70}看作由兩個有序的子陣列{50}和{70}組成。

具體程式碼實現如下:

public static void sort(Comparable[] a){
		aux = new Comparable[a.length];//一次性分配空間,aux是輔助陣列
		sort(a, 0, a.length-1);
}
private static void up2DownSort(Comparable[] a, int lo, int hi) {
	// 將陣列a[lo...hi]排序
	if(hi <= lo)
	    return;
	int mid = lo + (hi - lo)/2;
	sort(a, lo, mid);    //將左半邊排序
	sort(a, mid+1, hi);  //將右半邊排序
	merge(a, lo, mid, hi);//歸併結果(
}
public static void merge(Comparable[] a, int lo, int mid, int hi){
	// 將a[lo...mid] 和a[mid+1...hi]歸併
	int i = lo; // 第一個有序區域的索引
	int j = mid + 1; // 第二個有序區域的索引

	for (int k = lo; k <= hi; k++) {// 將a[lo...hi]複製到aux[lo...hi]中,這樣在操作a[]是資料不會被覆蓋而丟
		// aux[] 是在類中建立的一個輔助陣列,它的大小和a[]的大小一樣大
		aux[k] = a[k]; 
	}

	for (int k = lo; k <= hi; k++) { // 歸併到a[lo...hi]中
		if (i > mid) { // 左半邊用盡,也就是當第一個有序陣列的所有資料都放入到歸併的陣列中了,由於上一次結果會i++,
			// 所以會進入到這個條件中,那就直接把右邊的往歸併數組裡扔就可以了
			a[k] = aux[j++];
		} else if (j > hi) { // 同上,當右半邊資料(第二個有序區域)都放入到歸併的陣列中了,那就直接吧剩下的左邊資料(第一個有序陣列)放入歸併陣列
			a[k] = aux[i++];
		} else if (less(aux[j], aux[i])) {// 這句話才是歸併在比較大小的那句,誰小誰放入陣列中,此處的less()函式時之前文章提到的
			a[k] = aux[j++];
		} else {
			a[k] = aux[i++];
		}
	}
}

通過比較發現,自下而上的歸併排序比自上而下的方法程式碼量少一些

自下而上的歸併排序會多次遍歷整個陣列,根據子陣列大小進行兩兩歸併。子陣列的大小sz的初始值為1,每次加倍,最後一個子陣列的大小隻有在陣列大小是sz的偶數倍的時候才會等於sz(否則它會比sz小)

自下而上的歸併排序比較適合用連結串列組織的資料

當陣列長度為2的冪時,自上而下和自下而上的歸併排序所用的比較次數和陣列訪問次數正好相同

歸併排序時間複雜度
歸併排序的時間複雜度是O(N*lgN)。
假設被排序的數列中有N個數。遍歷一趟的時間複雜度是O(N),需要遍歷多少次呢?
歸併排序的形式就是一棵二叉樹,它需要遍歷的次數就是二叉樹的深度,而根據完全二叉樹的可以得出它的時間複雜度是O(N*lgN)。

歸併排序穩定性
歸併排序是穩定的演算法,它滿足穩定演算法的定義。
演算法穩定性 -- 假設在數列中存在a[i]=a[j],若在排序之前,a[i]在a[j]前面;並且排序之後,a[i]仍然在a[j]前面。則這個排序演算法是穩定的!