歸并排序
歸并排序的思想是分治法+回溯,將一個無序的數組先按照原來的一半進行拆分,一直拆分到最后一個元素,然后開始回溯,排序開始的過程是再回溯時開始排序的。
思想總結:
- 將源數組進行拆分,每次拆分一半,由圖可以分析出,當arr.length=n,需要拆分log2^8=3次。
- 當拆分到不能再拆分,也就是分組到每個組只有1個元素,停止拆分
- 開始排序并回溯排序,每次排序的時間復雜度為O(n)
- 總的時間復雜度為n x log2^n,時間復雜度不考慮系數和底數,所以n x log2^n等價于 O(nlogn)
==其實歸并總結就是2部分 1. 先拆分 2 回溯排序==
代碼分析
從分析我們知道,想要實現回溯,那么通常是使用遞歸的。那么回溯的問題解決了,我們要如何實現O(n)的時間復雜度呢?。以下圖i=2對應回溯為例子:
如果我們分別對數組arr1={2,3,6,8},和arr2={1,4,5,7},如果使用選擇,插入,冒泡等排序都是O(n^2), 顯然最后算法變成n^2*logn。我們可以換個思路:
說下回溯的過程:
- 當我們回溯排序時,源數組不變,我們將拆分后的數組歷史保存到另外的數組中,如臨時數組arr1={2,3,6,8},臨時數組arr2={1,4,5,7},臨時數組總空間O(n),(源arr的順序是上次回溯后排序得到的)。
- 然后我們定義幾個指針:
left->arr1 起始位置
mid->arr1結束的位置
right->arr2->結束的位置
i指向左邊待排序的元素
j指向右邊待排序的位置
k指向源數組中排好序的元素的下一個位置(指向新排序元素將要放置的位置)
- left,mid,right作為判斷結束的條件,i,j不斷移動指向左右數組要排序的元素,k用來作為存放元素的位置
//對數組[l...r] 全閉空間排序
public static void mergeSort(int[] arr) {
mergeSort(arr, 0, arr.length - 1);
}
public static void mergeSort(int[] arr, int l, int r) {
if (l == r) {//只有一個元素了,那么它就是有序的
return;
} else {
//找到中間邊界mid 拆分2個數組[l...mid]和[mid+1...r]
int mid = (r - l) / 2 + l;
//左邊繼續拆分
mergeSort(arr, l, mid);
//右邊邊繼續拆分
mergeSort(arr, mid + 1, r);
//一直拆分到l==r 說明只有一個元素 retuen
//然后開始回溯合并排序
merge(arr, l, mid, r);
}
}
public static void merge(int[] arr, int l, int mid, int r) {
//1. l不一定是從0開始的 2.因為是 對數組[l...r] 全閉空間排序 所以要+1
int[] temp = new int[r - l + 1];
//賦值臨時數組 也就是拆分左右數組,將arr拆分成arr1和arr2
for (int i = l; i <= r; i++) {
//上面說過l不是從0開始的 但是我們的temp是0開始的,所以要進行l的偏移
temp[i - l] = arr[i];
}
int i = l, j = mid + 1;
for (int k = l; k <= r; k++) {
//當arr2排序完成 但是arr1還有元素 那么直接賦值
if (j > r && i <= mid) {
arr[k] = temp[i - l];
i++;
//當arr1排序完成 但是arr2還有元素 那么直接賦值
} else if (i > mid && j <= r) {
arr[k] = temp[j - l];
j++;
} else if (temp[i - l] < temp[j - l]) {
arr[k] = temp[i - l];
i++;
} else {
arr[k] = temp[j - l];
j++;
}
}
}
首先根據代碼理解思想。要是還是有點似懂非懂的話。下面這張圖應該能幫到你。下面描述的情況是2-2歸并。最后左面4個元素有序,右邊4個元素有序(對應上面分析圖的上一步)
歸并排序--迭代法
歸并排序,還有一種迭代法。遞歸法歸并排序使用的是先自上而下拆分(分治),再自底向上歸并,那么如果我們直接通過遞歸,將數組按照size=1,2,4,8...n 去拆分,那么合并的數組為arr1-arr2: 1-1.2-2,4-4,8-8...左右兩邊分別代表arr1和arr2的長度。代碼:
/**
* 自定向上排序
*
* @param arr 待排序數組
* @param n 數組的長度
*/
public static void mergeSort(int[] arr, int n) {
/*
* 數組分2層循環 第一層是確定分組后每個組的長度,按照上面圖的圖示所知
* size分別為1,2,4,8...
* 當size=1 那么arr1.length=1 arr2.length=1 所以 1-1 排序 最終 “每” 2個元素有序
* 當size=2 那么arr1.length=2 arr2.length=2 所以 2-2 排序 最終 “每” 4個元素有序
* 當size=4 那么arr1.length=4 arr2.length=4 所以 4-4 排序 最終 “每” 8個元素有序
* 所以這層循環 就是為了幫助我們創建符合要求變化的size大小
* 最開始數組長度=1 也就是1個元素 他就是有序的 那么size = 2 * size這個算式就幫助我們迭代創建size分別為1,2,4,8...
*
*/
for (int size = 1; size <= n; size = 2 * size) {
/*
*i表示的是每個要合并分作的起始坐標也就是left 我們知道 left-right是通過mid分成arr1和arr2的
* 也就是[l...mid]-[mid+1...r] 等價于[l...size-1]和[size...r]
* 所以i的取值變化為i = i + 2 * size
* 同時i不能越界 所以i<n
*/
for (int i = 0; i + size < n; i = i + 2 * size) {
/*
* 上面分析[l...mid]-[mid+1...r] 等價于[l...size-1]和[size...r]
* 所以 i等價l i + size - 1等價mid i + size + size - 1等價r
* 雖然i<n合法 但是i + size可能越界,
* 同時當i+size>=n說明 只有arr1 arr2為null,那么也就不歸并了 他就是有序的
* 因為從上面得知,歸并的前提是arr1和arr2是有序的
*
*
* i + size + size - 1相當于r 他可能越界 所以Math.min(i + size + size - 1, n - 1)
*
*/
merge(arr, i, i + size - 1, Math.min(i + size + size - 1, n - 1));
}
}
}
歸并排序優化
現在直接上代碼
public static void mergeSort(int[] arr, int l, int r) {
if (r-l<=15) {
insertSort(arr,l,r);// 1 .
return;
} else {
//找到中間邊界mid 拆分2個數組[l...mid]和[mid+1...r]
int mid = (r - l) / 2 + l;
//左邊繼續拆分
mergeSort(arr, l, mid);
//右邊邊繼續拆分
mergeSort(arr, mid + 1, r);
//一直拆分到l==r 說明只有一個元素 retuen
//然后開始回溯合并排序
if (arr[mid] > arr[mid + 1])//2.
merge(arr, l, mid, r);
}
}
- 當拆分到足夠小的時候選擇使用插入排序,原始是插入排序對相對有序的數組效率比較高,所以當數組越小的時候有序的幾率就越大,所以使用插入排序
- (arr[mid] <= arr[mid + 1]) 也就是所arr1中的最大的元素已經比arr2最小的元素還要小,那么arr1所有元素就小于等于arr2的所有元素,因為再歸并中arr1和arr2都是有序的,那么此時[l...r]就是有序的,所以當(arr[mid] > arr[mid + 1])時我們才需要排序
思考
逆序對 ?
什么是逆序對.逆序對是判斷一個數組有序程度的一個標示。一個完全有序的數組,逆序對個數=0
1 2 3 4 5 6 8 7 一個逆序對
利用歸并思想,當向上歸并:
2 3 6 8 | 1 4 5 7
2與1比較 1<2 那么1比左邊2 3 6 8 都要小那么此時逆序對4
繼續歸并到
1 2 3 當4<6時 4比左邊6 8 小 逆序對未2
最終將計數相加
感謝:
https://juejin.im/post/5a96d6b15188255efc5f8bbd
https://juejin.im/post/5ab4c7566fb9a028cb2d9126
https://blog.csdn.net/dugudaibo/article/details/79508198
算法與數據結構-綜合提升 C++版