1. 程式人生 > >我要學大資料之演算法——歸併排序

我要學大資料之演算法——歸併排序

簡單粗暴的解釋:內部有序外部無序的兩個陣列的排序。

歸併排序以O(NlogN)最壞情形時間執行,而所使用的比較次數幾乎是最優的。它是遞迴演算法一個好的例項。

典型應用場景:MapReduce。 

遞迴:一個方法呼叫自己本身。其關鍵點是要找到結束方法遞迴呼叫的條件出口。

歸併排序的合併演算法說明,內容直接擷取自《資料結構與演算法分析·Java語言描述·第3版》。

一、實現Java語言描述的歸併排序功能

1、定義歸併排序類,使用泛型,僅對實現了Comparable介面的類元素進行排序。

2、首先從整體來看,一個無序的陣列,要對其使用歸併排序,必須先把它從中間分成兩個子陣列,然後將他們分別排序,最後對它們進行合併。

3、每個子陣列的排序,又可以分別對其使用歸併排序,這就涉及到了遞迴。最終切分成小陣列的遞迴方法呼叫,終會因為陣列只有一個元素而達到出口條件然後結束進入合併環節。

4、在遞迴前定義一個臨時陣列變數,然後傳遞到遞迴方法裡面使用,而不是每次歸併時建立一個臨時陣列,這樣可以避免頻繁建立物件從而消耗時間和記憶體。

/**
 * 歸併排序
 * 執行時間:
 * T(N)=2T(N/2)+N=O(NlogN)
 * @author z_hh
 * @time 2018年11月18日
 */
public class MergeSort<AnyType extends Comparable<? super AnyType>> {

	/**
	 * 歸併排序
	 * @param a 實現了Comparable介面的物件陣列
	 */
	public void mergeSort(AnyType[] a) {
		/*
		 * 如果對merge的每個遞迴呼叫均區域性宣告一個臨時陣列,那麼在任一時刻就可能有logN個臨時陣列處在活動期。
		 * 因此,我們由始至終使用一個臨時陣列,可以避免陣列拷貝帶來的效能消耗。
		 */
		AnyType[] tmpArray = (AnyType[]) new Comparable[a.length];
		mergeSort(a, tmpArray, 0, a.length - 1);
	}

	/**
	 * 遞迴排序
	 * @param a 實現了Comparable介面的物件陣列
	 * @param tmpArray 臨時陣列
	 * @param left 子陣列的最左元素索引
	 * @param right 子陣列的最右元素索引
	 */
	private void mergeSort(AnyType[] a, AnyType[] tmpArray, int left, int right) {
		if (left < right) {
			int center = (left + right) / 2;// 中間位置
			mergeSort(a, tmpArray, left, center);// 左邊遞迴排序
			mergeSort(a, tmpArray, center + 1, right);// 右邊遞迴排序
			merge(a, tmpArray, left, center + 1, right);// 將兩邊歸併
		}
	}

	/**
	 * 合併兩個已排序的子陣列
	 * @param a 實現了Comparable介面的物件陣列
	 * @param tmpArray 臨時陣列
	 * @param leftPos 左邊陣列的開始元素索引
	 * @param rightPos 右邊陣列的開始元素索引
	 * @param rightEnd 右邊陣列的結束元素索引
	 */
	private void merge(AnyType[] a, AnyType[] tmpArray, int leftPos, int rightPos, int rightEnd) {
		int leftEnd = rightPos - 1;
		int tmpPos = leftPos;
		int numElements = rightEnd - leftPos + 1;
		// 左右兩邊從初始位置開始,分別拿出當前位置的元素進行比較,小的放到臨時陣列的指定位置,然後位置向後移一位
		while (leftPos <= leftEnd && rightPos <= rightEnd) {
			if (a[leftPos].compareTo(a[rightPos]) <= 0) {
				tmpArray[tmpPos++] = a[leftPos++];
			}
			else {
				tmpArray[tmpPos++] = a[rightPos++];
			}
		}
		// 右邊元素放完了,將左邊的全部元素放到臨時陣列
		while (leftPos <= leftEnd) {
			tmpArray[tmpPos++] = a[leftPos++];
		}
		// 左邊元素放完了,將右邊的全部元素放到臨時陣列
		while (rightPos <= rightEnd) {
			tmpArray[tmpPos++] = a[rightPos++];
		}
		// 將臨時陣列的元素拷回原陣列
		for (int i = 0; i < numElements; i++, rightEnd--) {
			a[rightEnd] = tmpArray[rightEnd];
		}
	}
	
	// 測試
	public static void main(String[] args) {
		// 產生指定數量的隨機排序的陣列(比那種隨機產生一個數放進集合前判斷是否存在效率高多了)
		int size = 100;
		List<Integer> numbers = new ArrayList<>(size);
		for (int i = 0; i < size; i++) {
			numbers.add(i + 1);
		}
		Random random = new Random();
		int sourceSize = numbers.size();
		Integer[] array = new Integer[sourceSize];
		for (int i = 0; i < sourceSize; i++) {
			int index = random.nextInt(numbers.size());
			array[i] = numbers.remove(index);
		}
		// 排序前
		System.out.println("排序前");
		AtomicInteger no = new AtomicInteger(1);
		Arrays.stream(array)
		.forEach(i -> {
			System.out.print(i + " ");
			if (no.getAndIncrement() % 20 == 0) {
				System.out.println();
			}
		});
		// 排序
		MergeSort<Integer> mergeSort = new MergeSort<>();
		mergeSort.mergeSort(array);
		// 排序後
		System.out.println("排序後");
		Arrays.stream(array)
			.forEach(i -> {
				System.out.print(i + " ");
				if (i % 20 == 0) {
					System.out.println();
				}
			});
	}

}

 

二、歸併排序的時間複雜度

當只有一個排序的元素時,T(1)=1。

設定N個元素使用歸併排序的時間複雜度為T(N)。根據其演算法,先把陣列分為兩個子陣列也使用歸併排序,其時間複雜度分別為為T(N/2),加起來是T(N/2)+T(N/2)=2T(N/2),然後,兩個排好序的子陣列進行合併的時候,最多需要比較N次,其時間複雜度為N,最後總的時間複雜度為2T(N/2)+N。經過各種化解,T(N)=2T(N/2)+N=O(NlogN)。

三、不使用遞迴如何實現歸併排序

???後續補上。。。