歸並排序(MergeSort)和快速排序(QuickSort)都是用了分治算法思想。
所謂分治算法,顧名思義,就是分而治之,就是將原問題分割成同等結構的子問題,之后將子問題逐一解決后,原問題也就得到了解決。
同時,歸並排序(MergeSort)和快速排序(QuickSort)也代表了兩類分治算法的思想。
對於歸並排序,我們對於待處理序列怎么分的問題上並沒有太多關注,僅僅是簡單地一刀切,將整個序列分成近乎均勻的兩份,然后將子序列進行同樣處理。但是,我們更多的關注點在於怎么把分開的部分合起來,也就是merge的過程。
對於快速排序來說,我們則是花了很大的功夫放在了怎么分這個問題上,我們設定了樞軸(標定點),然后通過partition的過程將這個樞軸放在合適的位置,這樣我們就不用特別關心合起來的過程,只需要一步一步地遞歸下去即可。
下面說兩個從歸並排序和快速排序所衍生出來的問題。
1)關於求一個數組中逆序對數量的問題
在一個數組中,
![]()
隨機取出兩個元素,例如取出的是2和3,根據它們原來的位置順序看,它們是有序的,那么這個數字對就稱為順序對。
當取出的是2和1時,根據它們原來的位置順序看,2排在1的前面,而2卻比1要大,這樣的數字對稱為逆序對。
一個數組中逆序對的數量,可以用來衡量一個數組的有序程度。
那么怎么求一個數組中逆序對的數量呢?
一個最簡單的方法就是暴力解法:考察每一個數字對(使用雙重循環),算法復雜度為O(n2)
我們還可以使用歸並算法進行考察逆序對的個數,使得我們的算法復雜度到達O(nlog2n)級別的。
使用歸並算法,最關鍵的是歸並的過程。
舉個例子:

在這個序列的歸並排序中,歸並過程我們首先比較紫色部分的2和1的大小,由於1比2小,所以我們把1放到最前端的位置。

由於每個子序列在歸並之前都是有序的,既然1比2小,那么1也一定比第一個子序列中2后面的所有元素小,換句話說,這個1比前面子序列中2以及2之后的所有元素都構成了逆序對。所以在將1放到整個序列最前端的過程中,我們就可以給逆序對的計數器加上4。
接下來比較2和4:

這里2比4要小,那么就把2放到1的后面。

2比4小,還意味着2比4后面的所有元素都要小。那么此時就沒有構成任何逆序對。計數器不動,繼續歸並。
以此類推。
這樣我們就把暴力解法中的一對一對的比較變成了一組一組的比較。
代碼:
package com.mergeSort; import java.util.*; public class InversionCount{ // 我們的算法類不允許產生任何實例 private InversionCount(){} // merge函數求出在arr[l...mid]和arr[mid+1...r]有序的基礎上, arr[l...r]的逆序數對個數 private static long merge(int[] arr, int l, int mid, int r) { int[] aux = Arrays.copyOfRange(arr, l, r+1);//注意復制后的數組元素包括前索引位置的元素,不包括后索引位置的元素 // 初始化逆序數對個數 res = 0 long res = 0L; // 初始化,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];//注意有l個偏移量 j ++; } else if( j > r ){ // 如果右半部分元素已經全部處理完畢 arr[k] = aux[i-l]; i ++; } else if( aux[i-l]<=aux[j-l] ){ // 左半部分所指元素 <= 右半部分所指元素 arr[k] = aux[i-l]; i ++; } else{ // 右半部分所指元素 < 左半部分所指元素 arr[k] = aux[j-l]; j ++; // 此時, 因為右半部分k所指的元素小 // 這個元素和左半部分的所有未處理的元素都構成了逆序數對 // 左半部分此時未處理的元素個數為 mid - i + 1 res += (long)(mid - i + 1); } } return res; } // 求arr[l..r]范圍的逆序數對個數 // 思考: 歸並排序的優化可否用於求逆序數對的算法? :) private static long solve(int[] arr, int l, int r) { if (l >= r) return 0L; int mid = l + (r-l)/2; // 求出 arr[l...mid] 范圍的逆序數 long res1 = solve(arr, l, mid); // 求出 arr[mid+1...r] 范圍的逆序數 long res2 = solve(arr, mid + 1, r);
//只有每一次merge才會返回逆序數,而最底層的res(即solve()方法的返回值)都為0
//所以這一句最后加的總和其實就是每次merge的返回值
return res1 + res2 + merge(arr, l, mid, r); } public static long solve(int[] arr){ int n = arr.length; return solve(arr, 0, n-1); } // 測試 InversionCount public static void main(String[] args) { int[] arr=new int[]{1,2,3,5,4,4}; long l=solve(arr); System.out.println(l); return; } }
2)取出數組(無序)中第n個大的元素
最簡單的實例就是求數組中的最大值和最小值。這需要我們從頭到尾遍歷掃描一下即可。時間復雜度為O(n)。
那么怎么求數組中第n個大的元素呢?
容易想到的一個就是給整個數組排一下序,時間復雜度為:O(nlog2n)。
其實我們可以使用快速排序算法的思想來求解這個問題,來使得時間復雜度達到O(n)級別。
在快速排序中,每一次我們都需要找到一個標定點,然后將這個標定點放到合適的位置,這個合適的位置就是這個元素在排好序后最終應該在的位置。那么從它的索引就能看出它是第幾大的元素。
例如,

在排完第一個元素時,我們發現一個元素4恰好是第4名,也就是說4這個元素在這個序列中是第4大的。那么現在問題是:請問這個序列第6大的元素是誰?
此時我們就不用去管元素4前面的位置了,只需要遞歸地去求解元素4后面的元素即可。
同樣的,如果問題是:請問這個序列第2大的元素是誰?
那么我們只需要遞歸地去求解元素4前面的元素即可。
在不太規范的統計下,時間復雜度為:

代碼:
package com.quickSort; import java.util.*; public class QuickSortWhoBig2 { // 我們的算法類不允許產生任何實例 private QuickSortWhoBig2(){} // 對arr[l...r]部分進行partition操作 // 返回p, 使得arr[l...p-1] < arr[p] ; arr[p+1...r] > arr[p] // partition 過程, 和快排的partition一樣 private static int partition(int[] arr, int l, int r){ int v = arr[l]; int j = l; // arr[l+1...j] < v ; arr[j+1...i) > v for( int i = l + 1 ; i <= r ; i ++ ) if( arr[i] < v ){ j ++; int tem=arr[i]; arr[i]=arr[j]; arr[j]=tem; } int tem=arr[l]; arr[l]=arr[j]; arr[j]=tem; return j; } // 求出nums[l...r]范圍里第k小的數 private static int solve(int[] nums, int l, int r, int k){ if( l == r ) return nums[l]; // partition之后, nums[p]的正確位置就在索引p上 int p = partition(nums, l, r); if( k == p ) // 如果 k == p, 直接返回nums[p] return nums[p]; else if( k < p ) // 如果 k < p, 只需要在nums[l...p-1]中找第k小元素即可 return solve( nums, l, p-1, k); else // 如果 k > p, 則需要在nums[p+1...r]中找第k-p-1小元素 // 注意: 由於我們傳入QuickSortWhoBig2的依然是nums, 而不是nums[p+1...r], // 所以傳入的最后一個參數依然是k, 而不是k-p-1 return solve( nums, p+1, r, k ); } // 尋找nums數組中第k小的元素 // 注意: 在我們的算法中, k是從0開始索引的, 即最小的元素是第0小元素, 以此類推 // 如果希望我們的算法中k的語意是從1開始的, 只需要在整個邏輯開始進行k--即可, 可以參考solve2 public static int solve(int nums[], int k) { return solve(nums, 0, nums.length - 1, k); } // 尋找nums數組中第k小的元素, k從1開始索引, 即最小元素是第1小元素, 以此類推 public static int solve2(int nums[], int k) { return QuickSortWhoBig2.solve(nums, k - 1); } // 測試 QuickSortWhoBig2 public static void main(String[] args) { int[] arr=new int[]{10,9,8,7,6,5,4,3,2,1}; int n=1; for(int i=0;i<10;i++){ System.out.println("第"+n+"大的元素為:"+solve2(arr,n)); n++; } } }
輸出結果:

