一、 總體是遞歸的,左邊排好序右邊排好序,最後merge讓總體有序,merge過程須要申請和被排序數組等長度的輔助空間面試
二、 讓其總體有序的過程裏用了排外序的方法算法
三、 利用master公式來求解歸併的時間複雜度數組
四、 歸併排序可改成非遞歸實現less
主函數但願一個數組的0~3位置排序f(arr, 0, 3)dom
第一層遞歸但願f(arr, 0, 1)和f(arr, 2, 3)分別有序。函數
第二層遞歸:f(arr, 0, 1)但願f(arr, 0, 0)和f(arr, 1, 1)有序, f(arr, 2, 3)但願f(arr, 2, 2)和f(arr, 3, 3)分別有序。ui
f(arr, 0, 0)和f(arr, 1, 1)已經有序,回到第一層遞歸f(arr, 0, 1)中去merge0位置的數和1位置的數後刷回元素組的0到1位置,0到1位置變爲有序; f(arr, 2, 2)和f(arr, 3, 3)已經有序,回到f(arr, 2, 3)中去merge2位置的數和3位置的數後刷回原數組的2到3位置,2到3位置變爲有序。設計
f(arr, 0, 3)須要merge f(arr, 0, 1)和f(arr, 2, 3)此時f(arr, 0, 1)和f(arr, 2, 3)已經有序merge後copy到原數組的0到3位置。因而f(arr, 0, 3)總體有序指針
對於一個給定長度爲n的數組arr,咱們但願arr有序code
初始分組爲a=2,咱們讓每兩個有序,不夠一組的當成一組
分組變爲a=2*2=4,因爲上一步已經保證了兩兩有序,那麼咱們能夠當前分組的四個數的前兩個和後兩個數merge使得每四個數有序
分組變爲a=2*4=8,...直至a>=n,總體有序
package class03; public class Code01_MergeSort { // 遞歸方法實現 public static void mergeSort1(int[] arr) { if (arr == null || arr.length < 2) { return; } // 傳入被排序數組,以及左右邊界 process(arr, 0, arr.length - 1); } // arr[L...R]範圍上,變成有序的 // L...R N T(N) = 2*T(N/2) + O(N) -> public static void process(int[] arr, int L, int R) { if (L == R) { // base case return; } // >> 有符號右移1位,至關於除以2 int mid = L + ((R - L) >> 1); process(arr, L, mid); process(arr, mid + 1, R); // 當前棧頂左右已經排好序,準備左右merge,注意這裏的merge遞歸的每一層都會調用 merge(arr, L, mid, R); } public static void merge(int[] arr, int L, int M, int R) { // merge過程申請輔助數組,準備copy int[] help = new int[R - L + 1]; // 用來標識help的下標 int i = 0; // 左邊有序數組的指針 int p1 = L; // 右邊有序數組的指針 int p2 = M + 1; // p1和p2都沒越界的狀況下,誰小copy誰 while (p1 <= M && p2 <= R) { help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++]; } // 要麼p1越界了,要麼p2越界了,誰沒越界把誰剩下的元素copy到help中 while (p1 <= M) { help[i++] = arr[p1++]; } while (p2 <= R) { help[i++] = arr[p2++]; } // 把輔助數組中總體merge後的有序數組,copy回原數組中去 for (i = 0; i < help.length; i++) { arr[L + i] = help[i]; } } // 非遞歸方法實現 public static void mergeSort2(int[] arr) { if (arr == null || arr.length < 2) { return; } int N = arr.length; // 當前有序的,左組長度,那麼實質分組大小是從2開始的 int mergeSize = 1; while (mergeSize < N) { // log N // L表示當前分組的左組的位置,初始爲第一個分組的左組位置爲0 int L = 0; // 0.... while (L < N) { // L...M 當前左組(mergeSize) int M = L + mergeSize - 1; // 當前左組包含當前分組的全部元素,即沒有右組了,無需merge已經有序 if (M >= N) { break; } // L...M爲左組 M+1...R(mergeSize)爲右組。右組夠mergeSize個的時候,右座標爲M + mergeSize,右組不夠的狀況下右組邊界座標爲整個數組右邊界N - 1 int R = Math.min(M + mergeSize, N - 1); // 把當前組進行merge merge(arr, L, M, R); // 下一個分組的左組起始位置 L = R + 1; } // 若是mergeSize乘2一定大於N,直接break。防止mergeSize溢出,有可能N很大,下面乘2有可能範圍溢出(整形數大於21億) if (mergeSize > N / 2) { break; } // 無符號左移,至關於乘以2 mergeSize <<= 1; } } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // for test public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // for test public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static void main(String[] args) { int testTime = 500000; int maxSize = 100; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); mergeSort1(arr1); mergeSort2(arr2); if (!isEqual(arr1, arr2)) { succeed = false; printArray(arr1); printArray(arr2); break; } } System.out.println(succeed ? "Nice!" : "Oops!"); } }
遞歸複雜度計算,用master公式帶入,子問題規模N/2,調用2次,除了遞歸以外的時間複雜度爲merge的時間複雜度,爲O(N)。a=2,b=2,d=1知足master第一條logb^a == d規則
T(N) = 2T(N/2) + O(N) => O(N*logN)
非遞歸複雜度計算,mergeSize2等於分組從2->4->8->...,每一個分組下執行merge操做O(N)。因此非遞歸和遞歸的時間複雜度相同,也爲O(N)O(logN) = O(NlogN)
遞歸和非遞歸的歸併排序時間複雜度都爲O(NlogN)
Tips: 爲何選擇,冒泡,插入排序的時間複雜度爲O(N^2)而歸併排序時間複雜度爲O(NlogN),由於選擇,冒泡,插入排序的每一個元素浪費了大量的比較行爲N次。而歸併無浪費比較行爲,每次比較的結果有序後都會保存下來,最終merge
一、在一個數組中,一個數左邊比它小的數的總和,叫作小和,全部數的小和累加起來,叫作數組的小和。求數組的小和。例如[1, 3, 4, 2, 5]
1左邊比1小的數:沒有 3左邊比3小的數:1 4左邊比4小的數:一、3 2左邊比2小的數爲:1 5左邊比5小的數爲:一、三、四、2 因此該數組的小和爲:1+1+3+1+1+3+4+2 = 16
暴力解法,每一個數找以前比本身小的數,累加起來,時間複雜度爲O(N^2),面試沒分。可是暴力方法能夠用來作對數器
歸併排序解法思路:O(NlogN)。在遞歸merge的過程當中,產生小和。規則是左組比右組數小的時候產生小和,除此以外不產生;當左組和右組數相等的時候,拷貝右組的數,不產生小和;當左組的數大於右組的時候,拷貝右組的數,不產生小和。實質是把找左邊比自己小的數的問題,轉化爲找這個數右側有多少個數比本身大,在每次merge的過程當中,一個數若是處在左組中,那麼只會去找右組中有多少個數比本身大
package class03; public class Code02_SmallSum { public static int smallSum(int[] arr) { if (arr == null || arr.length < 2) { return 0; } return process(arr, 0, arr.length - 1); } // arr[L..R]既要排好序,也要求小和返回 // 全部merge時,產生的小和,累加 // 左 排序 merge // 右 排序 merge // arr 總體 merge public static int process(int[] arr, int l, int r) { // 只有一個數,不存在右組,小和爲0 if (l == r) { return 0; } // l < r int mid = l + ((r - l) >> 1); // 左側merge的小和+右側merge的小和+總體左右兩側的小和 return process(arr, l, mid) + process(arr, mid + 1, r) + merge(arr, l, mid, r); } public static int merge(int[] arr, int L, int m, int r) { // 在歸併排序的基礎上改進,增長小和res = 0 int[] help = new int[r - L + 1]; int i = 0; int p1 = L; int p2 = m + 1; int res = 0; while (p1 <= m && p2 <= r) { // 當前的數是比右組小的,產生右組當前位置到右組右邊界數量個小和,累加到res。不然res加0 res += arr[p1] < arr[p2] ? (r - p2 + 1) * arr[p1] : 0; // 只有左組當前數小於右組copy左邊的,不然copy右邊的 help[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++]; } while (p1 <= m) { help[i++] = arr[p1++]; } while (p2 <= r) { help[i++] = arr[p2++]; } for (i = 0; i < help.length; i++) { arr[L + i] = help[i]; } return res; } // for test public static int comparator(int[] arr) { if (arr == null || arr.length < 2) { return 0; } int res = 0; for (int i = 1; i < arr.length; i++) { for (int j = 0; j < i; j++) { res += arr[j] < arr[i] ? arr[j] : 0; } } return res; } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // for test public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // for test public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static void main(String[] args) { int testTime = 500000; int maxSize = 100; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); if (smallSum(arr1) != comparator(arr2)) { succeed = false; printArray(arr1); printArray(arr2); break; } } System.out.println(succeed ? "Nice!" : "Fucking fucked!"); } }
相似題目:求一個數組中的全部降序對,例如[3,1,7,0,2]降序對爲:(3,1), (3,0), (3,2), (1,0), (70), (7,2)。也能夠藉助歸併排序來解決。實質就是要求一個數右邊有多少個數比自身小
什麼樣的題目之後能夠藉助歸併排序:糾結每一個數右邊(左邊)有多少個數比自身大,比自身小等。求這種數的數量等等
給定一個數組arr,和一個整數num。請把小於等於num的數放在數組的左邊,大於num的數放在數組的右邊(不要求有序)。要求額外空間複雜度爲O(1),時間複雜度爲O(N)。例如[5,3,7,2,3,4,1],num=3,把小於等於3的放在左邊,大於3的放在右邊
思路:設計一個小於等於區域,下標爲-1。
一、 開始遍歷該數組,若是arr[i]<=num,當前數和區域下一個數交換,區域向右擴1,i++
二、 arr[i] > num, 不作操做,i++
給定一個數組,和一個整數num。請把小於num的數放在數組的左邊,等於num的放中間,大於num的放右邊。要求額外空間複雜度爲O(1),時間複雜度爲O(N)。[3,5,4,0,4,6,7,2],num=4。實質是經典荷蘭國旗問題
思路:設計一個小於區域,下標爲-1。設計一個大於區域,下表爲arr.length,越界位置。
一、 若是arr[i]當前位置的數==num, i++直接跳下一個
二、 若是arr[i]當前位置的數< num,當前位置的數arr[i]和小於區域的右一個交換,小於區域右擴一個位置,當前位置i++
三、 若是arr[i]當前位置的數> num,當前位置的數arr[i]與大於區域的左邊一個交換,大於區域左移一個位置,i停在原地不作處理,這裏不作處理是由於當前位置的數是剛從大於區域交換過來的數,還沒作比較
四、i和大於區域的邊界相遇,中止操做
思路:在給定數組上作partion,選定數組最右側的位置上的數做爲num,小於num的放在該數組的左邊,大於num的放在該數組的右邊。完成以後,把該數組最右側的數組num,交換到大於num區域的第一個位置,確保了交換後的num是小於等於區域的最後一個數(該數直至最後能夠保持當前位置不變,屬於已經排好序的數),把該num左側和右側的數分別進行一樣的partion操做(遞歸)。至關於每次partion搞定一個數的位置,代碼實現quickSort1
思路:藉助荷蘭國旗問題的思路,把arr進行partion,把小於num的數放左邊,等於放中間,大於放右邊。遞歸時把小於num的區域和大於num的區域作遞歸,等於num的區域不作處理。至關於每次partion搞定一批數,與標記爲相等的數。代碼實現quickSort2
初版和第二版的快排時間複雜度相同O(N^2):用最差狀況來評估,自己有序,每次partion只搞定了一個數是自身,進行了N次partion
隨機選一個位置i,讓arr[i]和arr[R]交換,再用=arr[R]做爲標記位。剩下的全部過程跟快排2.0同樣。即爲最經典的快排,時間複雜度爲O(NlogN)
爲何隨機選擇標記爲時間複雜度就由O(N^2)變爲O(NlogN)?若是咱們隨機選擇位置那麼就趨向於標記位的左右兩側的遞歸規模趨向於N/2。那麼根據master公式,能夠計算出算法複雜度爲O(NlogN)。實質上,在咱們選擇隨機的num時,最差狀況,最好狀況,其餘各類狀況的出現機率爲1/N。對於這N種狀況,數學上算出的時間複雜度最終指望是O(NlogN),這個數學上能夠進行證實,比較複雜
例如咱們的num隨機到數組左側三分之一的位置,那麼master公式爲
T(N) = T((1/3)N) + T((2/3)N) + O(N)
對於這個遞歸表達式,master公式是解不了的,master公式只能解決子問題規模同樣的遞歸。對於這個遞歸,算法導論上給出了計算方法,大體思路爲假設一個複雜度,看這個公式是否收斂於這個複雜度的方式,比較麻煩
時間複雜度參考上文每種的複雜度
空間複雜度:O(logN)。空間複雜度產生於每次遞歸partion以後,咱們須要申請額外的空間變量保存相等區域的左右兩側的位置。那麼每次partion須要申請兩個變量,多少次partion?實質是該遞歸樹被分了多少層,樹的高度,有好有壞,最好logN,最差N。隨機選擇num以後,指望仍然是機率累加,收斂於O(logN)。
package class03; public class Code03_PartitionAndQuickSort { public static void swap(int[] arr, int i, int j) { int tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; } // partion問題 public static int partition(int[] arr, int L, int R) { if (L > R) { return -1; } if (L == R) { return L; } int lessEqual = L - 1; int index = L; while (index < R) { if (arr[index] <= arr[R]) { swap(arr, index, ++lessEqual); } index++; } swap(arr, ++lessEqual, R); return lessEqual; } // arr[L...R] 玩荷蘭國旗問題的劃分,以arr[R]作劃分值 // 小於arr[R]放左側 等於arr[R]放中間 大於arr[R]放右邊 // 返回中間區域的左右邊界 public static int[] netherlandsFlag(int[] arr, int L, int R) { // 不存在荷蘭國旗問題 if (L > R) { return new int[] { -1, -1 }; } // 已經都是等於區域,因爲用R作劃分返回R位置 if (L == R) { return new int[] { L, R }; } int less = L - 1; // < 區 右邊界 int more = R; // > 區 左邊界 int index = L; while (index < more) { // 當前值等於右邊界,不作處理,index++ if (arr[index] == arr[R]) { index++; // 小於交換當前值和左邊界的值 } else if (arr[index] < arr[R]) { swap(arr, index++, ++less); // 大於右邊界的值 } else { swap(arr, index, --more); } } // 比較完以後,把R位置的數,調整到等於區域的右邊,至此大於區域纔是真正意義上的大於區域 swap(arr, more, R); return new int[] { less + 1, more }; } public static void quickSort1(int[] arr) { if (arr == null || arr.length < 2) { return; } process1(arr, 0, arr.length - 1); } public static void process1(int[] arr, int L, int R) { if (L >= R) { return; } // L..R上partition 標記位爲arr[R] 數組被分紅 [ <=arr[R] arr[R] >arr[R] ],M爲partion以後標記位處在的位置 int M = partition(arr, L, R); process1(arr, L, M - 1); process1(arr, M + 1, R); } public static void quickSort2(int[] arr) { if (arr == null || arr.length < 2) { return; } process2(arr, 0, arr.length - 1); } public static void process2(int[] arr, int L, int R) { if (L >= R) { return; } // 每次partion返回等於區域的範圍 int[] equalArea = netherlandsFlag(arr, L, R); // 對等於區域左邊的小於區域遞歸,partion process2(arr, L, equalArea[0] - 1); // 對等於區域右邊的大於區域遞歸,partion process2(arr, equalArea[1] + 1, R); } public static void quickSort3(int[] arr) { if (arr == null || arr.length < 2) { return; } process3(arr, 0, arr.length - 1); } public static void process3(int[] arr, int L, int R) { if (L >= R) { return; } // 隨機選擇位置,與arr[R]上的數作交換 swap(arr, L + (int) (Math.random() * (R - L + 1)), R); int[] equalArea = netherlandsFlag(arr, L, R); process3(arr, L, equalArea[0] - 1); process3(arr, equalArea[1] + 1, R); } // for test public static int[] generateRandomArray(int maxSize, int maxValue) { int[] arr = new int[(int) ((maxSize + 1) * Math.random())]; for (int i = 0; i < arr.length; i++) { arr[i] = (int) ((maxValue + 1) * Math.random()) - (int) (maxValue * Math.random()); } return arr; } // for test public static int[] copyArray(int[] arr) { if (arr == null) { return null; } int[] res = new int[arr.length]; for (int i = 0; i < arr.length; i++) { res[i] = arr[i]; } return res; } // for test public static boolean isEqual(int[] arr1, int[] arr2) { if ((arr1 == null && arr2 != null) || (arr1 != null && arr2 == null)) { return false; } if (arr1 == null && arr2 == null) { return true; } if (arr1.length != arr2.length) { return false; } for (int i = 0; i < arr1.length; i++) { if (arr1[i] != arr2[i]) { return false; } } return true; } // for test public static void printArray(int[] arr) { if (arr == null) { return; } for (int i = 0; i < arr.length; i++) { System.out.print(arr[i] + " "); } System.out.println(); } // for test public static void main(String[] args) { int testTime = 500000; int maxSize = 100; int maxValue = 100; boolean succeed = true; for (int i = 0; i < testTime; i++) { int[] arr1 = generateRandomArray(maxSize, maxValue); int[] arr2 = copyArray(arr1); int[] arr3 = copyArray(arr1); quickSort1(arr1); quickSort2(arr2); quickSort3(arr3); if (!isEqual(arr1, arr2) || !isEqual(arr2, arr3)) { succeed = false; break; } } System.out.println(succeed ? "Nice!" : "Oops!"); } }