歸併排序詳解,Java版描述。
為了簡單起見,使用int型別陣列講述歸併演算法,後面擴充套件到其他型別的排序。
目錄
1.1 用具體例子說明
十人排序問題。
- 將十人均分為兩隊
- 五人分為三人,二人兩隊
- 對於三人的隊伍,再次分成兩人和一人的隊伍
- 對於兩人的隊伍分成一人一隊
- 迴圈上面幾步,直到有十隊時,結束。
- 那麼此時所有隊伍都是一人一隊
- 找到相鄰的兩隊進行有序合併。
- 迴圈上一步直到只有一個隊伍時截止。
- 此時就已經完成了排序
1.2 排序思想
分而治之:將一個大陣列均分為兩個大小相等的陣列(奇數容量下有 1 的差值),然後進行排序的話,時間就從對T = O(n^2)
操作變成了2*O((n/2)^2) =2*O((n^2)/4) = O(n^2) / 2 = T / 2,時間縮小了一倍,我們將這個過程迴圈下去,直到所有陣列都被分成一個元素為止,由歸納可得時間為O(n*lgn),之後進行迴圈有序合併,那麼明顯時間為O(n),捨去,所以歸併排序將基礎排序中的O(n^2)改進為了O(n*lgn);有效的改進了時間複雜度。
1.3 見名知意
歸併排序就是將原來的陣列分為兩個小的陣列就行排序之後進行合併,因為常常使用遞迴實現(由先拆分後合併的性質決定的),所以我們稱其為歸併排序。
1.4 抽象過程
- 對於一個大小為n的待排序陣列,將其劃分為n個小陣列,即每個數劃分為一個數組,對於只有一個元素的陣列,那麼該陣列有序
。- 有序合併相鄰相同大小的陣列,即合併兩個有序陣列為一個有序陣列。
- 迴圈上一步直到所有小數組合併到一個數組中為止。
- 此時排序就已經在不斷的有序中完成,陣列有序。
1.5 例項操作
1.6 程式碼實現(JAVA版本)
這個排序屬於高階排序,比較抽象,所以我劃分為多個方法進行實現,力求每個方法都不難理解。
而且因為歸併排序是java語言底層的Arrays.sort()的實現,所以力求學會歸併,並且可以看懂sort原始碼。
1.6.1呼叫排序部分
// 兩次入口以便他人呼叫(Java底層的sort就是這麼做的) public static void mergeSort(int[] a, int lo, int hi) { // 歸併兩個陣列需要額外的陣列空間,我們直接在此地new一個,防止遞迴時迴圈申請空間 int[] temp = new int[a.length]; // 接下來進入遞迴體中。專門提取出來便於理解遞迴。 mergeSort(a, temp, lo, hi); } public static void mergeSort(int[] a) {//為了方便使用,直接轉發至另一個排序實體中。 mergeSort(a, 0, a.length - 1); }
我們提供給外界兩個呼叫歸併的介面,預設排序呼叫條件排序。條件排序中申請了一箇中間陣列,這是因為歸併兩個陣列要有額外陣列。(你可以試一試不用額外陣列實現自身歸併,即使你可以實現,你也會發現時間損耗將失去歸併的O(n*lgn)).
1.6.2遞迴實體
/**
* Description: 歸併演算法實現
*
* @Title: mergeSort
* @param a
* 原資料位置
* @param temp
* 用來存放陣列
* @param lo
* 邏輯陣列中的下界,遞迴中頻繁使用。
* @param hi
* 邏輯陣列中的上界,遞迴中頻繁使用。
*void
*/
private static void mergeSort(int[] a, int[] temp, int lo, int hi) {
int mid = (lo + hi) / 2; // 求切分點
if (hi - lo > 1) { // 子陣列長度大於1時,進行拆分
// 拆分兩個陣列
mergeSort(a, temp, lo, mid); //遞迴
mergeSort(a, temp, mid + 1, hi); // 遞迴,注意mid已經屬於前一個數組,所以記得+1
}
//合併兩個陣列a[lo..mid]和 a[mid..hi],兩個陣列都是有序陣列,合併出來一個有序陣列
merge(a, temp, lo, mid, hi);
}
接下來進入重點,兩個有序的邏輯陣列的合併。
我們首先將兩個有序數組合併到一箇中間陣列中,然後遍歷放回到原來的陣列中。(無法實現不借用外空間的合併)
注:此時便可以看懂我們為什麼要將 temp 陣列從外面的呼叫中傳入進來,以防止在遞迴中反反覆覆的申請,初始化。
1.6.3合併兩個陣列
private static void mergeArr(int[] a, int[] temp, int lo, int mid, int hi) {
// 初始化arr1的上界與下界
int arr1Begin = lo;
int arr1End = mid;
// 初始化arr2的上界與下界
int arr2Begin = mid + 1;
int arr2End = hi;
// 初始化上面的目的:1.可讀性強 2.在下面的邏輯中我們需要改變兩個Begin的值,儲存lo,mid,hi 的值。
// 初始化中間陣列的起始位置。
int k = lo;
// 所有使用到的變數進行初始化完畢,開始合併arr1和arr2
while (arr1Begin <= arr1End || arr2Begin <= arr2End) { // 當兩個陣列都沒有結束時,進入迴圈
if (arr1Begin > arr1End) { // 說明1號陣列中已經無值,只需將二號陣列放入結果resultArr陣列中即可
temp[k++] = a[arr2Begin++];
} else if (arr2Begin > arr2End) { // 同上。
temp[k++] = a[arr1Begin++];
} else {// 兩個陣列都沒有結束,比大小新增。
if (a[arr1Begin] < a[arr2Begin]) {
temp[k++] = a[arr1Begin++];
} else {
temp[k++] = a[arr2Begin++];
}
}
}
// 將結果陣列中的值取出放到排序陣列a[]中,完成a中陣列排序。
for (int i = lo; i <= hi; i++) {
a[i] = temp[i];
}
}
1.7 程式碼實現(C語言版)
TODO,改天寫
1.8 演算法分析
歸併排序是所有排序中快且穩定的,時間為O(n*lgn),但是明顯的,他需要一個輔助陣列,在大陣列中,一個相同大小的輔助陣列也是很恐怖的。
快速排序則是另一種實現了O(n*lgN)排序,而且不需要額外空間,但是代價是失去了穩定性。我們總是在權衡中進行前進。
還有一個非常大的改進空間就可以當做java1.6之前的底層的sort使用了(系統的sort裡快排,插排,歸併,計數排序都用到了,根據陣列長度和型別sort會選擇不同的排序方法)
因為歸併排序會將一個大陣列分割為若干個小陣列,我們加一個判斷,當小陣列的長度小於某個值(一般選取9~15)時,轉為插入排序(插入排序也是穩定的),所以既可以保證穩定性又能明顯改善歸併排序實際使用時間。
java底層的sort一般開始都有一個if語句,就是為了在不同的遞迴下使用,畢竟小陣列的話,誰都一樣,大陣列的話,可以明顯改善。
1.9全部原始碼
package sort;
/**
* Description: 歸併排序
*
* @ClassName: MergeSort
* @author 過道
* @date 2018年8月11日 上午10:42:38
*/
public class MergeSort {
public static void mergeSort(int[] a, int lo, int hi) {
// 歸併兩個陣列需要額外的陣列空間,我們直接在此地new一個,放置遞迴時迴圈申請空間
int[] temp = new int[a.length];
mergeSort(a, temp, lo, hi);
}
public static void mergeSort(int[] a) {
mergeSort(a, 0, a.length - 1);
}
/**
* Description: 歸併演算法實現
*
* @Title: mergeSort
* @param a
* 原資料位置
* @param temp
* 用來存放陣列
* @param lo
* @param hi
* void
*/
private static void mergeSort(int[] a, int[] temp, int lo, int hi) {
int mid = (lo + hi) / 2;
if (hi - lo > 1) { // 子陣列長度大於1時,進行拆分
mergeSort(a, temp, lo, mid);
mergeSort(a, temp, mid + 1, hi);
}
merge(a, temp, lo, mid, hi);
// 合併a[lo..mid]和 a[mid..hi] //此時兩個陣列都是有序陣列
}
/**
* Description:
*
* @Title: merge
* @param a
* @param temp
* @param lo
* @param mid
* @param hi
* void
*/
private static void merge(int[] a, int[] temp, int lo, int mid, int hi) {
int arr1Begin = lo;
int arr1End = mid;
int arr2Begin = mid + 1;
int arr2End = hi;
int k = lo;
while (arr1Begin <= arr1End || arr2Begin <= arr2End) {
if (arr1Begin > arr1End) { // 說明1號陣列中已經無值
temp[k++] = a[arr2Begin++];
} else if (arr2Begin > arr2End) { // 說明二號陣列中已經無值
temp[k++] = a[arr1Begin++];
} else {// 兩個陣列都沒有結束,比大小新增。
if (a[arr1Begin] < a[arr2Begin]) { // 比大小新增
temp[k++] = a[arr1Begin++];
} else {
temp[k++] = a[arr2Begin++];
}
}
}
for (int i = lo; i <= hi; i++) {
a[i] = temp[i];
}
}
public static void main(String[] args) {
int a[] = { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
mergeSort(a);
for (int i = 0; i < a.length; i++) {
System.out.print(a[i] + " ");
}
}
}