1. 程式人生 > >高階排序演算法-歸併排序

高階排序演算法-歸併排序

輔助工具

歸併排序

  • O(nlogn)級別演算法
  • 需要額外的輔助空間
  • 穩定的排序演算法

對於排序演算法穩定性的定義, 這裡直接引用百度詞條:

假定在待排序的記錄序列中,存在多個具有相同的關鍵字的記錄,若經過排序,這些記錄的相對次序保持不變,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序後的序列中,r[i]仍在r[j]之前,則稱這種排序演算法是穩定的;否則稱為不穩定的。

排序步驟 :

  1. 首先將一個數組遞迴進行分解, 知道分解為每一部分都是一個元素, 這時每一部分都是有序的了

  2. 然後兩兩進行歸併, 遞迴進行, 直到再歸併成一個數組, 排序就完成了, 如下圖所示: 在這裡插入圖片描述

程式碼實現

package sort.merge;

import sort.Sort ;

/**
 * 歸併排序
 * @author 七夜雪
 *
 */
public class MergeSort implements Sort {
	
	@Override
	public void sort(int [] arr) {
		mergeSort(arr, 0, arr.length - 1);
	}
	
	/**
	 * 對陣列arr的[l, r]的區間進行排序
	 * @param arr
	 * @param l
	 * @param r
	 */
	private void mergeSort
(int[] arr, int l, int r){ // 遞迴終止條件 if (l >= r) { return; } int mid = l + (r - l) / 2; // 對[l, mid]部分進行歸併排序 mergeSort(arr, l, mid); // 對[mid + 1, r]進行歸併排序 mergeSort(arr, mid + 1, r); // 對[l, mid], [mid + 1, r]這兩部分進行歸併 merge(arr, l, mid, r); } /** * 對陣列[l, mid], [mid + 1, r]這兩部分進行歸併 * @param arr * @param l * @param mid * @param r */
private void merge(int[] arr, int l, int mid, int r){ // 用於歸併排序的輔助空間, 因為[l, r]是前閉後閉的區間, 所以aux的大小是r - l + 1 int[] aux = new int[r - l + 1]; for (int i = 0; i < aux.length; i++) { aux[i] = arr[i + l]; } // for (int i = l; i <= r; i++) { // aux[i - l] = arr[i]; // } // 用於記錄左半部分陣列下標 int leftIndex = l; // 用於記錄右半部分陣列下標 int rightIndex = mid + 1; // 迴圈進行歸併 for (int k = l; k <= r; k++) { // 表示左側部分已經完成歸併, 直接使用右側部分進行歸併 if (leftIndex > mid) { arr[k] = aux[rightIndex - l]; rightIndex++; // 表示右側部分已經完成歸併, 直接使用左側部分進行歸併 } else if (rightIndex > r) { arr[k] = aux[leftIndex - l]; leftIndex++; // 左側元素小於右側元素, 使用左邊元素進行歸併 } else if (aux[leftIndex - l] < aux[rightIndex - l]) { arr[k] = aux[leftIndex - l]; leftIndex++; } else { arr[k] = aux[rightIndex - l]; rightIndex++; } } } }

歸併排序優化

雖然歸併排序是nlogn級別的演算法, 但是在陣列資料量比較小的時候, 插入排序的效率仍然是高於歸併排序的, 所以可以在對陣列分解到足夠小之後, 使用插入排序, 然後再遞迴進行歸併排序, 具體程式碼如下 :

package sort.merge;

import sort.Sort ;
import utils.ArrayUtil ;

/**
 * 歸併排序
 * 優化 : 對於較小的陣列使用插入排序效能要優於歸併排序, 
 * 所以可以在分解到較小的部分時, 使用插入排序對陣列進行排序
 * @author 七夜雪
 *
 */
public class MergeSort2 implements Sort {
	
	@Override
	public void sort(int [] arr) {
		mergeSort(arr, 0, arr.length - 1);
	}
	
	/**
	 * 對陣列arr的[l, r]的區間進行排序
	 * @param arr
	 * @param l
	 * @param r
	 */
	private void mergeSort(int[] arr, int l, int r){
		// 遞迴終止條件, 小於等於16個元素時使用插入排序
		if (r - l < 16) {
            // 對arr陣列的[l,r]區間進行排序, 插入排序演算法
			ArrayUtil.insertSort(arr, l, r);
			return;
		}
		
		int mid = l + (r - l) / 2;
		// 對[l, mid]部分進行歸併排序
		mergeSort(arr, l, mid);
		// 對[mid + 1, r]進行歸併排序
		mergeSort(arr, mid + 1, r);
		// 對[l, mid], [mid + 1, r]這兩部分進行歸併
		merge(arr, l, mid, r);
	}
	
	
	/**
	 * 對陣列[l, mid], [mid + 1, r]這兩部分進行歸併
	 * @param arr
	 * @param l
	 * @param mid
	 * @param r
	 */
	private void merge(int[] arr, int l, int mid, int r){
		// 用於歸併排序的輔助空間, 因為[l, r]是前閉後閉的區間, 所以aux的大小是r - l + 1
		int[] aux = new int[r - l + 1];
		for (int i = 0; i < aux.length; i++) {
			aux[i] = arr[i + l];
		}
		
//		for (int i = l; i <= r; i++) {
//			aux[i - l] = arr[i];
//		}
		
		// 用於記錄左半部分陣列下標
		int leftIndex = l;
		// 用於記錄右半部分陣列下標
		int rightIndex = mid + 1;
		// 迴圈進行歸併
		for (int k = l; k <= r; k++) {
			// 表示左側部分已經完成歸併, 直接使用右側部分進行歸併
			if (leftIndex > mid) {
				arr[k] = aux[rightIndex - l];
				rightIndex++;
			// 表示右側部分已經完成歸併, 直接使用左側部分進行歸併
			} else if (rightIndex > r) {
				arr[k] = aux[leftIndex - l];
				leftIndex++;
			// 左側元素小於右側元素, 使用左邊元素進行歸併
			} else if (aux[leftIndex - l] < aux[rightIndex - l]) {
				arr[k] = aux[leftIndex - l];
				leftIndex++;
			} else {
				arr[k] = aux[rightIndex - l];
				rightIndex++;
			}
		}
	}
	
	
}

如果一個數組是近乎有序的, 或者說是完全有序的, 上述步驟會有很多無用的merge操作, 所以可以在進行merge前增加一個判斷, 效率也會有一定的提高, 程式碼如下 :

	/**
	 * 對陣列arr的[l, r]的區間進行排序
	 * @param arr
	 * @param l
	 * @param r
	 */
	private void mergeSort(int[] arr, int l, int r){
		// 遞迴終止條件, 小於等於16個元素時使用插入排序
		if (r - l < 16) {
			ArrayUtil.insertSort(arr, l, r);
			return;
		}
		
		int mid = l + (r - l) / 2;
		// 對[l, mid]部分進行歸併排序
		mergeSort(arr, l, mid);
		// 對[mid + 1, r]進行歸併排序
		mergeSort(arr, mid + 1, r);
        // 判斷是否需要進行merge, 因為[l, mid], [mid + 1, r]這兩部分都是排好序的陣列
		if (arr[mid] > arr[mid + 1]) {
			// 對[l, mid], [mid + 1, r]這兩部分進行歸併
			merge(arr, l, mid, r);
		}
	}

自底向上的歸併排序

上面的歸併排序是自頂向下的歸併排序, 對於歸併排序也可以自底向上進行歸併, 排序方法如下 :

  1. 從下往上分解成小塊, 然後一層層往上進行歸併, 最終歸併成一整個陣列, 如下圖所示 : 在這裡插入圖片描述

    上圖從下往上步驟如下:

    1. 每個元素作為一部分, 進行歸併, 歸併後結果如第三行
    2. 然後按照每兩個元素作為一部分, 進行歸併, 歸併後結果如第二行
    3. 最後對每四個元素作為一部分, 進行歸併, 歸併後結果如第一行, 整個陣列已經有序了

程式碼實現 :

	public void sort(int [] arr) {
		int length = arr.length;
		// 陣列元素個數小於等於16時, 直接使用插入排序
		if (length <= 16) {
			ArrayUtil.insertSort(arr, 0, length);
			return;
		}
		
		for (int sz = 8; sz < length; sz += sz) {
			// 每次遍歷2*sz個長度
			for (int i = 0; i + sz < length; i +=sz + sz) {
				if (sz == 8 ) {
					ArrayUtil.insertSort(arr, i, Math.min(i + sz + sz -1, length - 1));
				} else {
					if (arr[i + sz - 1] > arr[i + sz]) {
						// 對[i, i + sz -1]和[i + sz, 2*sz + i -1]區間進行merge操作
						merge(arr, i, i + sz - 1, Math.min(i + sz + sz -1, length - 1));
					}
				}
			}
		}
		
	}

測試

對插入排序, 進行了優化的各個版本的歸併排序進行效能測試, 這裡使用了Junit進行測試, 程式碼如下 :

	/**
	 * 測試插入排序和歸併排序效能
	 */
	@Test
	public void testSort(){
		// 生成一個隨機陣列
		int[] arr = ArrayUtil.generateArray(100000, 0, 100000);
		int[] arr1 = ArrayUtil.copyArray(arr);
		int[] arr2 = ArrayUtil.copyArray(arr);
		int[] arr3 = ArrayUtil.copyArray(arr);
		int[] arr4 = ArrayUtil.copyArray(arr);
		System.out.println("隨機陣列->插入排序 : " + ArrayUtil.testSort(arr, new InsertSort2()) + "s") ;
		System.out.println("隨機陣列->歸併排序1 : " + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
		System.out.println("隨機陣列->歸併排序2 : " + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
		System.out.println("隨機陣列->歸併排序3 : " + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
		System.out.println("隨機陣列->自底向上的歸併排序 : " + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;

		/*
		 * 生成一個近乎有序的陣列
		 * 100000 : 陣列元素個數
		 * 10 : 在一個完全有序的陣列上進行多少次元素交換
		 */
		arr = ArrayUtil.generateNearlyOrderedArray(100000, 10);
		arr1 = ArrayUtil.copyArray(arr);
		arr2 = ArrayUtil.copyArray(arr);
		arr3 = ArrayUtil.copyArray(arr);
		arr4 = ArrayUtil.copyArray(arr);
		System.out.println("近乎有序的陣列->插入排序:" + ArrayUtil.testSort(arr, new InsertSort2()) + "s") ;
		System.out.println("近乎有序的陣列->歸併排序1:" + ArrayUtil.testSort(arr1, new MergeSort()) + "s") ;
		System.out.println("近乎有序的陣列->歸併排序2:" + ArrayUtil.testSort(arr2, new MergeSort2()) + "s") ;
		System.out.println("近乎有序的陣列->歸併排序3:" + ArrayUtil.testSort(arr3, new MergeSort3()) + "s") ;
		System.out.println("近乎有序的陣列->自底向上的歸併排序:" + ArrayUtil.testSort(arr4, new MergeSortBU()) + "s") ;

	}

測試結果 :

隨機陣列->插入排序 : 2.353s 隨機陣列->歸併排序1 : 0.016s 隨機陣列->歸併排序2 : 0.017s 隨機陣列->歸併排序3 : 0.017s 隨機陣列->自底向上的歸併排序 : 0.015s 近乎有序的陣列->插入排序:0.002s 近乎有序的陣列->歸併排序1:0.007s 近乎有序的陣列->歸併排序2:0.005s 近乎有序的陣列->歸併排序3:0.002s 近乎有序的陣列->自底向上的歸併排序:0.003s

從上面的測試結果來看, 對於隨機陣列來說, 歸併排序的效率是遠遠高於插入排序的, 對於近乎有序的陣列來說, 歸併排序的效能也是可觀的, 測試結果說明:

  • 歸併排序1是最初始的歸併排序版本
  • 歸併排序2是底層使用了插入排序的版本
  • 歸併排序3是在2的基礎上加入了判斷是否需要進行歸併操作的版本