此次咱們介紹另外一種時間複雜度爲O(nlogn)
的排序算法叫作歸併排序。歸併排序在數據量大且數據遞增或遞減連續性好的狀況下,效率比較高,且是O(nlogn)
複雜度下惟一一個穩定的排序。java
歸併排序是創建在歸併操做上的一種有效的排序算法,該算法是採用分治法的一個很是典型的應用。將已有序的子序列合併,獲得徹底有序的序列;即先使每一個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱爲二路歸併。歸併排序是一種穩定的排序方法。
實現歸併的一種直截了當的辦法是將兩個不一樣的有序數組歸併到第三個數組中。實現的方法很簡單,建立一個適當大小的數組而後將兩個輸入數組中的元素一個個從小到大放入這個數組中。可是,當用歸併將一個大數組排序時,咱們須要進行不少次歸併,這樣每次歸併時都建立一個新數組來存儲排序結果就會浪費空間,所以咱們可使用原地歸併。算法
原地歸併的思路是:一樣須要建立一個新數組做爲輔助空間,可是這個數組不是用於存放歸併後的結果,而是存放歸併前的結果,而後將歸併後的結果一個個從小到大放入原來的數組中。數組
原地歸併的步驟以下:函數
k
指向原來數組的第一個位置,i
指向新數組左半部分的第一個元素,j
指向右半部分的一個元素。i
指向的元素ei
小於j
指向的元素ej
,則將ei
放入k
指向的位置,而後i++
指向下一個元素,k++
指向下一個須要存放的位置。不然若是ei>ej
,則將ej
放入k
指向的位置,而後j++
指向下一個元素,k++
指向下一個須要存放的位置。i
指向的位置已經超過中間位置,而此時右半部分j
還未移動到末尾,那麼將j
指向位置後面的全部元素都移動到k
指向位置的後面,反之相似。下圖展現了對數組[8, 7, 6, 5, 4, 3, 2, 1]
進行從小到大歸併排序的過程:性能
歸併排序的代碼:優化
public static void sort(Comparable[] arr) { int n = arr.length; sort(arr, 0, n - 1); } // 遞歸使用歸併排序,對arr[l...r]的範圍進行排序 private static void sort(Comparable[] arr, int l, int r) { if (l >= r) { return; } // 這種寫法防止溢出 int mid = l + (r - l) / 2; sort(arr, l, mid); sort(arr, mid + 1, r); merge(arr, l, mid, r); } // 將arr[l...mid]和arr[mid+1...r]兩部分進行歸併 private static void merge(Comparable[] arr, int l, int mid, int r) { Comparable[] aux = Arrays.copyOfRange(arr, l, r + 1); // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1 int i = l, j = mid + 1; for (int k = l; k <= r; k++) { if (i > mid) { // 若是左半部分元素已經所有處理完畢 arr[k] = aux[j - l]; j++; } else if (j > r) { // 若是右半部分元素已經所有處理完畢 arr[k] = aux[i - l]; i++; } else if (aux[i - l].compareTo(aux[j - l]) < 0) { // 左半部分所指元素 < 右半部分所指元素 arr[k] = aux[i - l]; i++; } else { // 左半部分所指元素 >= 右半部分所指元素 arr[k] = aux[j - l]; j++; } } }
和快速排序同樣,對於小規模數組,咱們可使用直接插入排序。其次,對於近乎有序的數組,咱們能夠減小歸併的次數。spa
優化的歸併排序代碼:code
public static void sort(Comparable[] arr) { int n = arr.length; sort(arr, 0, n - 1); } private static void sort(Comparable[] arr, int l, int r) { // 對於小規模數組, 使用插入排序 if (r - l <= 15) { InsertionSort.sort(arr, l, r); return; } int mid = (l + r) / 2; sort(arr, l, mid); sort(arr, mid + 1, r); // 對於arr[mid] <= arr[mid+1]的狀況,不進行merge // 對於近乎有序的數組很是有效,可是對於通常狀況,有必定的性能損失 if (arr[mid].compareTo(arr[mid + 1]) > 0) { merge(arr, l, mid, r); } } private static void merge(Comparable[] arr, int l, int mid, int r) { Comparable[] aux = Arrays.copyOfRange(arr, l, r + 1); // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1 int i = l, j = mid + 1; for (int k = l; k <= r; k++) { if (i > mid) { // 若是左半部分元素已經所有處理完畢 arr[k] = aux[j - l]; j++; } else if (j > r) { // 若是右半部分元素已經所有處理完畢 arr[k] = aux[i - l]; i++; } else if (aux[i - l].compareTo(aux[j - l]) < 0) { // 左半部分所指元素 < 右半部分所指元素 arr[k] = aux[i - l]; i++; } else { // 左半部分所指元素 >= 右半部分所指元素 arr[k] = aux[j - l]; j++; } } }
咱們對空間進行優化,上述歸併排序因爲每次調用merge
方法都會申請新的輔助空間,遞歸深度過大,就會形成 OOM。blog
然而咱們能夠經過參數的方式傳遞給子函數,這樣只須要在開始的時候申請一次輔助空間。排序
優化代碼:
public static void sort(Comparable[] arr) { int n = arr.length; Comparable[] aux = new Comparable[n]; sort(arr, aux, 0, n - 1); } private static void sort(Comparable[] arr, Comparable[] aux, int l, int r) { // 對於小規模數組, 使用插入排序 if (r - l <= 15) { InsertionSort.sort(arr, l, r); return; } int mid = (l + r) / 2; sort(arr, aux, l, mid); sort(arr, aux, mid + 1, r); // 對於arr[mid] <= arr[mid+1]的狀況,不進行merge // 對於近乎有序的數組很是有效,可是對於通常狀況,有必定的性能損失 if (arr[mid].compareTo(arr[mid + 1]) > 0) { merge(arr, aux, l, mid, r); } } private static void merge(Comparable[] arr, Comparable[] aux, int l, int mid, int r) { System.arraycopy(arr, l, aux, l, r - l + 1); // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1 int i = l, j = mid + 1; for (int k = l; k <= r; k++) { if (i > mid) { // 若是左半部分元素已經所有處理完畢 arr[k] = aux[j]; j++; } else if (j > r) { // 若是右半部分元素已經所有處理完畢 arr[k] = aux[i]; i++; } else if (aux[i].compareTo(aux[j]) < 0) { // 左半部分所指元素 < 右半部分所指元素 arr[k] = aux[i]; i++; } else { // 左半部分所指元素 >= 右半部分所指元素 arr[k] = aux[j]; j++; } } }
自底向上的歸併排序是先歸併小數組,而後成對歸併獲得的子數組,即先進行兩兩歸併(把每一個元素想象成大小爲 1 的數組),而後是四四歸併(把兩個大小爲 2 的數組歸併成一個有 4 個元素的數組),而後是八八歸併,一直下去。在每一輪歸併中,最後一次歸併的第二個可能比第一個子數組要小,不然全部的歸併中兩個數組的大小都應該同樣,而在下一輪中子數組的大小會翻倍。
過程以下圖,利用迭代實現:
自底向上的歸併排序代碼:
public static void sort(Comparable[] arr) { int n = arr.length; // 外循環控制歸併數組的大小 for (int len = 1; len < n; len += len) { // 內循環根據外循環分配的大小進行兩兩歸併 for (int i = 0; i < n - len; i += len + len) { // 對 arr[i...i+len-1] 和 arr[i+len...i+2*len-1] 進行歸併 // 須要知足 i+len < n 且 i+2*len-1 < n merge(arr, i, i + len - 1, Math.min(i + len + len - 1, n - 1)); } } } private static void merge(Comparable[] arr, int l, int mid, int r) { Comparable[] aux = Arrays.copyOfRange(arr, l, r + 1); // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1 int i = l, j = mid + 1; for (int k = l; k <= r; k++) { if (i > mid) { // 若是左半部分元素已經所有處理完畢 arr[k] = aux[j - l]; j++; } else if (j > r) { // 若是右半部分元素已經所有處理完畢 arr[k] = aux[i - l]; i++; } else if (aux[i - l].compareTo(aux[j - l]) < 0) { // 左半部分所指元素 < 右半部分所指元素 arr[k] = aux[i - l]; i++; } else { // 左半部分所指元素 >= 右半部分所指元素 arr[k] = aux[j - l]; j++; } } }
優化思路同上:
優化的代碼:
public static void sort(Comparable[] arr) { int n = arr.length; Comparable[] aux = new Comparable[n]; // 對於小數組, 使用插入排序優化 for (int i = 0; i < n; i += 16) { InsertionSort.sort(arr, i, Math.min(i + 15, n - 1)); } for (int len = 16; len < n; len += len) { for (int i = 0; i < n - len; i += len + len) { // 對於arr[mid] <= arr[mid+1]的狀況,不進行merge if (arr[i + len - 1].compareTo(arr[i + len]) > 0) { merge(arr, aux, i, i + len - 1, Math.min(i + len + len - 1, n - 1)); } } } } private static void merge(Comparable[] arr, Comparable[] aux, int l, int mid, int r) { System.arraycopy(arr, l, aux, l, r - l + 1); // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1 int i = l, j = mid + 1; for (int k = l; k <= r; k++) { if (i > mid) { // 若是左半部分元素已經所有處理完畢 arr[k] = aux[j - l]; j++; } else if (j > r) { // 若是右半部分元素已經所有處理完畢 arr[k] = aux[i - l]; i++; } else if (aux[i - l].compareTo(aux[j - l]) < 0) { // 左半部分所指元素 < 右半部分所指元素 arr[k] = aux[i - l]; i++; } else { // 左半部分所指元素 >= 右半部分所指元素 arr[k] = aux[j - l]; j++; } } }
對比歸併排序與快速排序:
O(nlogn)
,而快速排序最壞O(n^2)
,最好O(n)
。