演算法系列(四)歸併排序及其改進(java實現)

ChuanjieZhu發表於2017-09-04

前言:演算法第四版2.2節 歸併排序學習總結

歸併排序:將兩個有序的陣列歸併成一個更大的有序陣列。採用分治(divide and conquer)策略,利用遞迴每次將陣列分成兩半,直到子陣列個數為1(1個元素的陣列自然就有序的),將結果歸併再返回。

效能:所需時間與NlogN成正比(N為陣列大小);所需額外空間與N成正比。當所有元素都相同時,歸併排序的執行時間是線性的。

歸併兩個有序陣列的merge()方法:

        private static void merge(Comparable[] a, int lo, int mid, int hi){
		int i = lo, j = mid+1;
		for(int k = lo; k <= hi; k++){
			aux[k] = a[k];
		}
		for(int k = lo; k<= hi; k++){
			if(i>mid)
				a[k] = aux[j++];
			else if(j>hi)
				a[k] = aux[i++];
			else if(aux[j] < aux[i]))  
				a[k] = aux[j++];
			else 
				a[k] = aux[i++];
		}		
	}
      先將元素複製到輔助陣列aux[ ]中,四個判斷:左半邊耗盡時,將右版邊剩餘的直接複製到a中;右半邊耗盡時,將左半邊剩餘的直接複製到a中;右半邊元素小於左半邊時,複製右半邊的元素;右半邊的元素大於等於左半邊的元素時,複製左半邊的元素(保證了穩定性)。示例如下:


分治所需時間複雜度的證明:

D (N) = 2 D (N / 2) + N,其中 D( 1 ) = 0,證明D (N) = N lg N

1.圖解法


2.逐步擴充套件法


3.歸納法


自頂向下的歸併排序(標準遞迴):

利用遞迴:一分為二,再一分為二,直到分到1,再return

public class MergeSort {

	private static Comparable[] aux;  //輔助陣列
	
	public static void sort(Comparable[] a){
		aux = new Comparable[a.length];
		sort(a,0,a.length-1);
	}
	
	private static void sort(Comparable[] a,int lo, int hi){
		if(lo>=hi) 
			return;
		int mid = lo + (hi -lo) / 2;
		sort(a,lo,mid);     //左半排序
		sort(a,mid+1,hi);   //右半排序
		merge(a,lo,mid,hi); //歸併
	}
	
	private static void merge(Comparable[] a, int lo, int mid, int hi){
		int i = lo, j = mid+1;
		for(int k = lo; k <= hi; k++){
			aux[k] = a[k];
		}
		for(int k = lo; k<= hi; k++){
			if(i>mid)
				a[k] = aux[j++];
			else if(j>hi)
				a[k] = aux[i++];
			else if(less(aux[j],aux[i]))  //將aux的元素歸併到a中
				a[k] = aux[j++];
			else 
				a[k] = aux[i++];
		}		
	}

	private static boolean less(Comparable v, Comparable w) {		
		return v.compareTo(w) < 0;
	}
	private static void exch(Comparable[] a, int i, int j) {
		Comparable t = a[i];
		a[i] = a[j];
		a[j] = t;
	}
	public static void show(Comparable[] a){
		for (int i = 0; i < a.length; i++) {
			System.out.print(a[i] + " ");
		}
		System.out.println();
	}
	public static boolean isSorted(Comparable[] a){
		for (int i = 1; i < a.length; i++) {
			if(less(a[i], a[i-1]))
				return false;			
		}
		return true;
	}
}
自底向上的歸併排序:

先歸併小陣列,再成對歸併得到的子陣列,依次往上。size=1,2,4,8,16...,其程式碼量比標準遞迴小。

	public static void sort(Comparable[] a){
		int N = a.length;
		aux = new Comparable[a.length];
		for (int size = 1; size < N; size = size+size) {
			for(int lo = 0; lo < N-size; lo += size+size){
					merge(a, lo, lo+size-1, Math.min(lo+size+size-1, N-1));
			}
		}
	}
不使用靜態陣列來輔助:

由於靜態變數是物件共享的,如果有多個程式同時在用這個類,就會出錯,所以將輔助陣列作為引數傳遞。

	public static void sort(Comparable[] a){
		Comparable[] aux = new Comparable[a.length];
		sort(a,aux,0,a.length-1);
	}
	
	private static void sort(Comparable[] a, Comparable[] aux,int lo, int hi){
		if(lo>=hi) 
			return;
		int mid = lo + (hi -lo) / 2;
		sort(a,aux,lo,mid); 
		sort(a,aux,mid+1,hi); 
		merge(a,aux,lo,mid,hi);
	}
	
	private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi){
		int i = lo, j = mid+1;
		for(int k = lo; k <= hi; k++){
			aux[k] = a[k];
		}
		for(int k = lo; k<= hi; k++){
			if(i>mid)
				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. 如果子陣列較小(7左右),則採用速度更快的插入排序;

2. 檢測待歸併的兩個子陣列是否已經有序,如果已經有序則直接複製進a[ ];

3. 通過再遞迴中交換引數來避免每次歸併時都要複製陣列到輔助陣列。

        private static final int CUTOFF = 7;
	
	public static void sort(Comparable[] a){
//		Comparable[] aux = new Comparable[a.length];
		Comparable[] aux = a.clone();  //初始化aux,為了後面把它當原始陣列a[]用
		sort(aux,a,0,a.length-1);
	}
	
	private static void sort(Comparable[] a, Comparable[] aux,int lo, int hi){
		
		int mid = lo + (hi -lo) / 2;
//		if(hi <= lo) 
//			return;		
		if(hi <= lo + CUTOFF - 1){  //小陣列使用插入排序
			insertionSort(a, lo, hi);
			return;
		}		
		sort(aux,a,lo,mid);     
		sort(aux,a,mid+1,hi);   
		if(!less(a[mid+1],a[mid])){ //如果陣列已經有序則直接複製,不再merge
			System.arraycopy(aux, lo, a, lo, hi-lo+1);
			return;
		}
		merge(a,aux,lo,mid,hi); 
	}
	
	private static void insertionSort(Comparable[] a, int lo, int hi) {
		for (int i = lo; i <= hi; i++) {
			for(int j = i; j > lo && less(a[j],a[j-1]); j--)
				exch(a,j,j-1);
		}
	}

	private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi){
		int i = lo, j = mid+1;
		/*for(int k = lo; k <= hi; k++){
			aux[k] = a[k];
		}
		for(int k = lo; k<= hi; k++){
			if(i>mid)
				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++];
		}		*/
		
		//消除陣列複製
		for(int k = lo; k<= hi; k++){
			if     (i > mid)           aux[k] = a[j++];
			else if(j > hi)            aux[k] = a[i++];
			else if(less(a[j],a[i]))   aux[k] = a[j++];
			else 		               aux[k] = a[i++];
		}

	}

快速歸併:

這裡修改merge()方法,目的是省掉內迴圈中總是判斷某半邊是否耗盡,做法是按索引值降序將a[ ]的後半部分複製到aux[ ]中,再去歸併。

	private static void merge(Comparable[] a,Comparable[] aux, int lo, int mid, int hi){		
		for(int k = lo; k <= mid; k++)
			aux[k] = a[k];
		for(int k = mid+1; k <= hi; k++)
			aux[k] = a[hi+mid+1-k];
		
		int i = lo, j = hi;  //由兩邊向中間比較
		for(int k=lo;k<=hi; k++ ){
			if(less(aux[j],aux[i]))
				a[k] = aux[j--];
			else 
				a[k] = aux[i++]; //保證穩定性
		}			
	}


總結:可以證明基於比較的排序演算法排序長度為N的陣列至少需要lg( N! ) ~ NlgN次比較(這裡是以2為底的),所以歸併排序的時間複雜度是最優,但是空間複雜度不是,它需要O(N)的儲存空間。另外,歸併排序是穩定的。




相關文章