歸併排序(Merge sort)
歸併排序是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法的一個非常典型的應用,且各層分治遞迴可以同時進行
演算法思路
歸併演算法,指的是將兩個已經排序的序列合併成一個序列的操作
遞迴法(Top-down)
- 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
- 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
- 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
- 重複步驟3直到某一指標到達序列尾
- 將另一序列剩下的所有元素直接複製到合併序列尾
迭代法(Bottom-up)
原理如下(假設序列共有 n 個元素):
- 將序列每相鄰兩個數字進行歸併操作,形成 floor(n/2) 個序列,排序後每個序列包含兩/一個元素
- 若此時序列數不是 1 個則將上述序列再次歸併,形成 floor(n/4) 個序列,每個序列包含四/三個元素
- 重複步驟2,直到所有元素排序完畢,即序列數為 1
圖片源自Visualgo
實現
Java 遞迴版本
public class MergeSort {
public static void main(String[] args) {
int[] unsortedArray = new int[]{5, 3, 6, 2, 1, 9, 4, 8, 7};
MergeSort.mergeSort(unsortedArray);
System.out.println("After sorted: ");
for (int number : unsortedArray) {
System.out.print(" " + number);
}
}
public static void mergeSort(int[] arrayList) {
if (arrayList == null || arrayList.length == 0) {
return;
}
sort(arrayList, 0, arrayList.length - 1);
}
private static void sort(int[] arrayList, int leftStart, int rightEnd) {
if (leftStart >= rightEnd) {
return;
}
// 找出中間索引即左邊陣列的末尾位置
int leftEnd = (leftStart + rightEnd) >> 1;
// 右邊陣列的起始位置在左邊陣列末尾右側
int rightStart = leftEnd + 1;
// 對左邊陣列進行遞迴
sort(arrayList, leftStart, leftEnd);
// 對右邊陣列進行遞迴
sort(arrayList, rightStart, rightEnd);
// 合併
merge(arrayList, leftStart, leftEnd, rightStart, rightEnd);
}
private static void merge(int[] arrayList, int leftStart, int leftEnd, int rightStart, int rightEnd) {
int[] tempArray = new int[arrayList.length];
int tempIndex = leftStart;
int resultIndex = leftStart;
while (leftStart <= leftEnd && rightStart <= rightEnd) {
// 從兩個陣列中取出最小的放入臨時陣列
tempArray[tempIndex++] = arrayList[leftStart] <= arrayList[rightStart] ? arrayList[leftStart++] : arrayList[rightStart++];
}
// 剩餘部分依次放入臨時陣列(實際上兩個while只會執行其中一個)
while (leftStart <= leftEnd) {
tempArray[tempIndex++] = arrayList[leftStart++];
}
while (rightStart <= rightEnd) {
tempArray[tempIndex++] = arrayList[rightStart++];
}
// 將臨時陣列中的內容拷貝回原陣列中
//(原left-right範圍的內容被複制回原陣列)
while (resultIndex <= rightEnd) {
arrayList[resultIndex] = tempArray[resultIndex++];
}
}
}
複製程式碼
Java 迭代版本
public static void mergeSort(int[] arr) {
int len = arr.length;
int[] result = new int[len];
int block, start;
// 原版程式碼的迭代次數少了一次,沒有考慮到奇數列陣列的情況
for(block = 1; block < len; block *= 2) {
for(start = 0; start <len; start += 2 * block) {
int low = start;
int mid = (start + block) < len ? (start + block) : len;
int high = (start + 2 * block) < len ? (start + 2 * block) : len;
//兩個塊的起始下標及結束下標
int start1 = low, end1 = mid;
int start2 = mid, end2 = high;
//開始對兩個block進行歸併排序
while (start1 < end1 && start2 < end2) {
result[low++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
}
while(start1 < end1) {
result[low++] = arr[start1++];
}
while(start2 < end2) {
result[low++] = arr[start2++];
}
}
int[] temp = arr;
arr = result;
result = temp;
}
}
複製程式碼
時間複雜度
最好的情況:一趟歸併需要 n 次,總共需要 logN 次,因此為 O(N*logN)
最壞的情況: 接近於平均情況,為 O(N*logN)
說明:對長度為 n 的檔案,需進行 logN 趟二路歸併,每趟歸併的時間為O(n),故其時間複雜度無論是在最好情況下還是在最壞情況下均是 O(N*logN)
穩定性
歸併排序最大的特色就是它是一種穩定的排序演算法。歸併過程中是不會改變元素的相對位置的。
缺點是,它需要O(n)的額外空間。但是很適合於多連結串列排序。
快速排序(Quick Sort)
它是由氣泡排序改進而來的。相比氣泡排序,每次交換是跳躍式的。每次排序的時候設定一個基準點,將小於等於基準點的數全部放在基準點左邊,將大於等於基準點的數全部放在基準點右邊,重複此操作即可得到排序後的序列。
圖片源自Visualgo
實現
Java
public class QuickSort {
public static void sort(int[] array) {
if (array == null || array.length == 0) {
return;
}
int left = 0;
int right = array.length - 1;
quickSort(array, left, right);
}
private static void quickSort(int[] array, int left, int right) {
if (left > right) {
return;
}
int base = array[left];
int i = left;
int j = right;
while (i != j) {
while (array[j] > base && i < j) {
--j;
}
while (array[i] < base && i < j) {
++i;
}
if (i < j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
quickSort(array, left, i - 1);
quickSort(array, i + 1, right);
}
public static void main(String[] args) {
int[] unsortedArray = new int[]{6, 5, 3, 1, 8, 7, 2, 4};
QuickSort.sort(unsortedArray);
System.out.println("After sorted: ");
for (int number : unsortedArray) {
System.out.print(" " + number);
}
}
}
複製程式碼
時間複雜度
最好的情況:因為每次都將序列分為兩個部分(一般二分都複雜度都和 logN 相關),故為 O(N*logN)
最壞的情況:序列基本有序,選取的樞軸元素為最大值或最小值時,退化為氣泡排序,幾乎要比較 N x N / 2 次,故為 O(N*N)
如何避免最壞情況:為改進快速排序演算法,隨機選取界點或最左、最右、中間三個元素中的值處於中間的作為界點,通常可以避免原始序列有序的最壞情況
穩定性
由於每次都需要和中軸元素交換,因此原來的順序就可能被打亂。如序列為 5 3 3 4 3 8 9 10 11會將3的順序打亂。所以說,快速排序是不穩定的
快排在資料少的時候是不佔優勢的,所以一般是在資料少的時候用插入排序,資料多的時候用快排。
堆排序
時間複雜度
最壞情況下,接近於最差情況下:O(N*logN),因此它是一種效果不錯的排序演算法
穩定性
堆排序需要不斷地調整堆,因此堆排序是一種不穩定的排序!