歸併排序、隨機快排
歸併排序
1、 整體是遞迴的,左邊排好序右邊排好序,最後merge讓整體有序,merge過程需要申請和被排序陣列等長度的輔助空間
2、 讓其整體有序的過程裡用了排外序的方法
3、 利用master公式來求解歸併的時間複雜度
4、 歸併排序可改為非遞迴實現
遞迴思路:
主函式希望一個陣列的0~3位置排序f(arr, 0, 3)
第一層遞迴希望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)分別有序。
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有序
初始分組為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、在一個陣列中,一個數左邊比它小的數的總和,叫做小和,所有數的小和累加起來,叫做陣列的小和。求陣列的小和。例如[1, 3, 4, 2, 5]
1左邊比1小的數:沒有
3左邊比3小的數:1
4左邊比4小的數:1、3
2左邊比2小的數為:1
5左邊比5小的數為:1、3、4、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)。也可以藉助歸併排序來解決。實質就是要求一個數右邊有多少個數比自身小
什麼樣的題目以後可以藉助歸併排序:糾結每個數右邊(左邊)有多少個數比自身大,比自身小等。求這種數的數量等等
快排
Partion過程
給定一個陣列arr,和一個整數num。請把小於等於num的數放在陣列的左邊,大於num的數放在陣列的右邊(不要求有序)。要求額外空間複雜度為O(1),時間複雜度為O(N)。例如[5,3,7,2,3,4,1],num=3,把小於等於3的放在左邊,大於3的放在右邊
思路:設計一個小於等於區域,下標為-1。
1、 開始遍歷該陣列,如果arr[i]<=num,當前數和區域下一個數交換,區域向右擴1,i++
2、 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,越界位置。
1、 如果arr[i]當前位置的數==num, i++直接跳下一個
2、 如果arr[i]當前位置的數< num,當前位置的數arr[i]和小於區域的右一個交換,小於區域右擴一個位置,當前位置i++
3、 如果arr[i]當前位置的數> num,當前位置的數arr[i]與大於區域的左邊一個交換,大於區域左移一個位置,i停在原地不做處理,這裡不做處理是因為當前位置的數是剛從大於區域交換過來的數,還沒做比較
4、i和大於區域的邊界相遇,停止操作
快排1.0:每次partion搞定一個位置
思路:在給定陣列上做partion,選定陣列最右側的位置上的數作為num,小於num的放在該陣列的左邊,大於num的放在該陣列的右邊。完成之後,把該陣列最右側的陣列num,交換到大於num區域的第一個位置,確保了交換後的num是小於等於區域的最後一個數(該數直至最後可以保持當前位置不變,屬於已經排好序的數),把該num左側和右側的數分別進行同樣的partion操作(遞迴)。相當於每次partion搞定一個數的位置,程式碼實現quickSort1
快排2.0:每次partion搞定一批位置
思路:藉助荷蘭國旗問題的思路,把arr進行partion,把小於num的數放左邊,等於放中間,大於放右邊。遞迴時把小於num的區域和大於num的區域做遞迴,等於num的區域不做處理。相當於每次partion搞定一批數,與標記為相等的數。程式碼實現quickSort2
第一版和第二版的快排時間複雜度相同O(N^2):用最差情況來評估,本身有序,每次partion只搞定了一個數是自身,進行了N次partion
快排3.0:隨機位置作為num標記位
隨機選一個位置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!");
}
}