1. 程式人生 > >演算法(4)歸併排序 java

演算法(4)歸併排序 java

在介紹歸併排序之前,先簡單的說一下O(NlogN)和O(N2)之間的比較,通過下面的圖片可以明顯的看出來,前者的優勢是很明顯的,並且隨著N的增大,優勢會越來越明顯,優化之後的程式碼可能意味著笨的演算法一輩子都算不出來結果,而優化之後的演算法,一瞬間就算出來了(細思極恐,這不就是現實生活嗎...)


歸併排序:歸併排序(MERGE-SORT)是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路

歸併

為什麼說歸併排序就是一個O(NlogN)的演算法呢?請看下圖


一個有N個元素的陣列,假設N=8,那麼採用分治法,通過上圖可以看出來,進行3次二分法就能將陣列拆分為單個元素,然後逐層進行歸併操作,3就是通過log28=3得出來的 通過這種拆分方法,把問題降低到通過O(N)的時間複雜度就能得到一個排好序的子序列,因此歸併排序是一個O(NlogN)的演算法,其在效能上要比O(N2)要好,並且是一個很穩定的演算法.

當然任何事情都是有兩面性,歸併演算法的缺陷是需要藉助於比較大的記憶體,但通過空間換取時間是比較值得的

可以使用遞迴思想或者迴圈來實現歸併排序,本次程式碼中使用遞迴思想來實現歸併排序

	// [l,r]
	public static void sort(int[] arr, int l, int r) {
		if (l >= r)
			return;
		int mid = (r + l) / 2;
		sort(arr, l, mid);
		sort(arr, mid + 1, r);
		merge(arr, l, r, mid);

	}

	// [l,mid] [mid+1,r]是已經排好序的 直接進行merge操作
	private static void merge(int[] arr, int l, int r, int mid) {
		int[] aux = new int[r - l + 1];// 大小從l 到r
		
		for (int i = l; i <= r; i++) {// 將arr中l到r的元素複製到aux中
			aux[i - l] = arr[i];
		}
		
		int i = l;// 在arr中的 左邊有序的下標
		int j = mid + 1;//在arr中的 右邊有序的下標 
		
		// 進行歸併操作
		for (int k = l; k <= r;) {
			
			if (i > mid) {// 說明左邊已經比完了 直接將右邊j下標對應的數字給arr
				arr[k++] = aux[j++ - l];
			} else if (j > r) {// 說明右邊已經比完了,直接將左邊i下標對應的數字給arr
				arr[k++] = aux[i++ - l];
			} else {// 否則比較值 直接用三目運算
				arr[k++] = aux[i - l] < aux[j - l] ? aux[i++ - l] : aux[j++ - l];
			}
			
		}

	}

然而,上面的程式碼在某些情況下,其效能是不如插入排序的,因此歸併排序是有一些優化的點的.來分析程式碼

public static void sort(int[] arr, int l, int r) {
		if (l >= r)
			return;
		int mid = (r + l) / 2;
		sort(arr, l, mid);
		sort(arr, mid + 1, r);
		merge(arr, l, r, mid);

	}
①在上面的程式碼中,將[l,mid]和[mid+1,r]兩個區間進行歸併排序,緊接著無論上面的兩個區間是否已經達成了有序不做判斷,直接進行merge操作,這樣在近乎有序的情況下,其效能會有影響.
②如果陣列近乎有序,那麼在元素個數小於一個常數的時候,利用插入排序效能反而更好,至於用哪個常數,在不同的應用場景,可以進行試驗.

經過兩次優化之後的程式碼:

	// [l,r]
	public static void sort(int[] arr, int l, int r) {
		if (r - l + 1 <= 7) {// 優化②當需要進行歸併排序的個數小於7的時候直接使用插入排序演算法
			// 插入排序演算法
			for (int i = l; i <= r; i++) {
				int t = arr[i];
				int j = i;
				for (; j > 0 && arr[j - 1] > t; j--) {
					arr[j] = arr[j - 1];
				}
				if (j != i) {
					arr[j] = t;
				}
			}
			return;
		}
		
		int mid = (r + l) / 2;
		sort(arr, l, mid);
		sort(arr, mid + 1, r);
		if (arr[mid] > arr[mid + 1]) {// 優化① 通過上面的步驟可以保證左區間和右區間都是有序的序列了,因此, 						//只有當左區間的最後一個元素大於右區間第一個元素時,才進行merge操作
			merge(arr, l, r, mid);
		}

	}

	// [l,mid] [mid+1,r]是已經排好序的 直接進行merge操作
	private static void merge(int[] arr, int l, int r, int mid) {
		int[] aux = new int[r - l + 1];// 大小從l 到r

		for (int i = l; i <= r; i++) {// 將arr中l到r的元素複製到aux中
			aux[i - l] = arr[i];
		}

		int i = l;// 在arr中的 左邊有序的下標
		int j = mid + 1;// 在arr中的 右邊有序的下標

		// 進行歸併操作
		for (int k = l; k <= r;) {

			if (i > mid) {// 說明左邊已經比完了 直接將右邊j下標對應的數字給arr
				arr[k++] = aux[j++ - l];
			} else if (j > r) {// 說明右邊已經比完了,直接將左邊i下標對應的數字給arr
				arr[k++] = aux[i++ - l];
			} else {// 否則比較值 直接用三目運算
				arr[k++] = aux[i - l] < aux[j - l] ? aux[i++ - l] : aux[j++ - l];
			}

		}

	}
通過上面的優化,這個歸併排序就已經算是達到了比較好的效能了.


-----------------------------------------------------------------分水嶺-----------------------------------

再附上一個不實用遞迴,使用迭代來完成的歸併排序,這個比較好理解

/**
	 * 通過自底向上的迭代來完成歸併排序
	 * @param arr
	 */
	public static void sortBU(int[] arr) {
		for(int size = 1 ; size <= arr.length - 1 ; size += size) {//控制每次進行merge操作的元素的個數 2倍					                                  關係 1->2->4 
			for(int i = 0 ; i + size <= arr.length - 1 ; i +=(size * 2)) {//控制每次merge操作的兩個區間[i,i+size - 1],[i + size , i+size * 2 - 1]
				merge(arr, i, Math.min(i + size * 2 - 1,arr.length - 1), i + size - 1);//為了避免i+size * 2 - 1 出現越界 使用Math.min 來取出 當前的right 和陣列邊界小的那一個
			}
		}
	}