排序演算法之歸併,快速,堆和桶

騎摩托馬斯發表於2019-03-02

歸併排序(Merge sort)

歸併排序是建立在歸併操作上的一種有效的排序演算法。該演算法是採用分治法的一個非常典型的應用,且各層分治遞迴可以同時進行

演算法思路

歸併演算法,指的是將兩個已經排序的序列合併成一個序列的操作

遞迴法(Top-down)

  1. 申請空間,使其大小為兩個已經排序序列之和,該空間用來存放合併後的序列
  2. 設定兩個指標,最初位置分別為兩個已經排序序列的起始位置
  3. 比較兩個指標所指向的元素,選擇相對小的元素放入到合併空間,並移動指標到下一位置
  4. 重複步驟3直到某一指標到達序列尾
  5. 將另一序列剩下的所有元素直接複製到合併序列尾

迭代法(Bottom-up) 原理如下(假設序列共有 n 個元素):

  1. 將序列每相鄰兩個數字進行歸併操作,形成 floor(n/2) 個序列,排序後每個序列包含兩/一個元素
  2. 若此時序列數不是 1 個則將上述序列再次歸併,形成 floor(n/4) 個序列,每個序列包含四/三個元素
  3. 重複步驟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),因此它是一種效果不錯的排序演算法

穩定性

堆排序需要不斷地調整堆,因此堆排序是一種不穩定的排序!

桶排序

請參考常見排序演算法 - 桶排序

相關文章