我要學大資料之演算法——歸併排序
阿新 • • 發佈:2018-12-27
簡單粗暴的解釋:內部有序外部無序的兩個陣列的排序。
歸併排序以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)。
三、不使用遞迴如何實現歸併排序
???後續補上。。。