動畫:一篇文章快速學會歸併排序

內容介紹

歸併排序簡介

咱們之前讀書時,學校會舉行運動會,運動會上有不少比賽項目,好比跳遠比賽。當參加跳遠比賽人數比較多時,一般會將全部參賽選手分紅多組,每組的同窗比賽,並按成績進行排名,最後將全部組的學生成績彙總獲得全部學生的排名。java

上面案例中的全部學生跳遠排名就是歸併排序的思想。歸併排序是一個典型的基於分治的算法。歸併一詞的中文含義就是合併、併入的意思,歸併排序分紅兩個步驟:1.拆分,2.合併。算法

歸併排序的思想

歸併排序思想,將原數據序列分紅大小相等的兩個子序列,繼續劃分子序列,直到子序列有序時,將劃分的有序子序列合併成大的有序序列,最終合併成一個有序序列。 編程

歸併排序動畫演示

歸併排序分析

通常沒有特殊要求排序算法都是升序排序,小的在前,大的在後。 數組由{7, 3, 1, 9, 5, 2, 8, 6} 這8個無序元素組成。數組

歸併排序分紅兩個步驟:1.拆分,2.合併。微信

  1. 拆分 當咱們要排序由{7, 3, 1, 9, 5, 2, 8, 6}這樣一個數組的時候,歸併排序法首先將這個數組分紅兩半。 而後想辦法對左邊的數組進行排序,右邊的數組進行排序,而後再將它們歸併起來。很顯然,如今左邊和右邊兩個數組依然是無序的,接着再拆分,將左邊的數組分紅兩半,將右邊的數組分紅兩半,效果以下:

如今被拆分的每一個子數組長度是2,每一個子數組依然是無序的,接着拆分,效果以下:性能

拆分後的效果如上圖,通過此次拆分後,每一個子數組的長度被爲1,咱們認爲長度爲1的子數組是有序的。不須要再進行拆分了。到此拆分就結束了。優化

  1. 合併。 到如今每一個子數組長度爲1,是有序的子數組,須要將兩個長度爲1有序的子數組合併成一個長度爲2的有序數組。

動畫效果以下: 動畫

到如今每一個子數組長度爲2,是有序的子數組,須要將兩個長度爲2有序的子數組合併成一個長度爲4的有序數組。code

動畫效果以下: blog

到如今每一個子數組長度爲4,是有序的子數組,須要將兩個長度爲4有序的子數組合併成一個長度爲8的有序數組。

動畫效果以下:

歸併排序合併時的細節

歸併排序再進行將有序子數組合併成大一點有序數組時,須要對比左右兩個子數組哪一個元素小取哪一個元素。

  1. 左邊數組的元素大於右邊數組的元素,取右邊的小元素。
  2. 右邊數組的元素大於等於左邊數組的元素,取左邊的小元素。

以上兩種狀況是常見狀況,還有兩種特殊狀況須要注意:

  1. 左邊的所有是小值提早取完了,剩下右邊的直接賦值
  2. 右邊的所有是小值提早取完了,剩下左邊的直接賦值

歸併排序代碼編寫

代碼以下:

public class MergeSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {7, 3, 1, 9, 5, 2, 8, 6};
        mergeSort(arr);
    }

    // 歸併排序的方法
    public static void mergeSort(int[] arr) {
        // 使用一個額外的數組,容量和要排序的數組容量同樣大
        int[] aux = new int[arr.length];
        mergePass(arr, 0, arr.length - 1, aux);
        // mergePass2(arr, 0, arr.length - 1, aux);
    }

    // 對arr數組的[low, hight]索引元素進行歸併排序
    private static void mergePass(int[] arr, int low, int high, int[] aux) {
        // 遞歸的終止條件,當拆分後,小索引和大索引是相同的,也就是一個元素的時候就終止拆分
        if (low >= high)
            return;

        // 將一個數組拆分爲兩個數組
        int mid = (low + high) / 2;
        mergePass(arr, low, mid, aux);
        mergePass(arr, mid + 1, high, aux);
        // 對拆分後的這兩個數組進行排序
        merge(arr, low, mid, high, aux);
    }

    // 將arr[low, mid]和arr[mid+1, high]兩部分進行歸併
    private static void merge(int[] arr, int low, int mid, int high, int[] aux) {
        // 臨時數組的內容就是兩個待排序數組的內容,排好序的內容會放到arr中
        for (int k = low; k <= high; k++) {
            aux[k] = arr[k];
        }

        // i是左邊數組的索引
        // j是右邊數組的索引
        int i = low;
        int j = mid + 1;

        // 遍歷左邊和右邊的小數組合併到一個大數組中
        for (int k = low; k <= high; k++) {
            if (i > mid) {
                // 左邊的所有是小值提早取完了,剩下右邊的直接賦值
                arr[k] = aux[j];
                j++;
            } else if (j > high) {
                // 右邊的所有是小值提早取完了,剩下左邊的直接賦值
                arr[k] = aux[i];
                i++;
            } else if (aux[i] > aux[j]) {
                // 左邊數組的元素大於右邊數組的元素,取右邊的小元素
                arr[k] = aux[j];
                j++;
            } else { // aux[left] <= aux[right]
                // 右邊數組的元素大於等於左邊數組的元素,取左邊的小元素
                arr[k] = aux[i];
                i++;
            }
        }
    }
}

歸併排序代碼優化1

歸併排序代碼優化1,小數據規模時改用插入排序。

若是待排序的數據量很大,當子數組拆分的比較小時,能夠改爲插入排序,由於插入排序的小數據規模時效率很高,這種優化方式能夠改進大多數遞歸排序算法的性能。

優化後代碼以下:

public class MergeSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {7, 3, 1, 9, 5, 2, 8, 6};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    public static void mergeSort(int[] arr) {
        // 使用一個額外的數組,容量和要排序的數組容量同樣大
        int[] aux = new int[arr.length];
        mergePass2(arr, 0, arr.length - 1, aux);
    }

    // 優化: 對小規模子數組使用插入排序
    // 對arr數組的[low, hight]索引元素進行歸併排序
    private static void mergePass2(int[] arr, int low, int high, int[] aux) {
        if (low >= high) return;
        // 對小規模子數組使用插入排序
        if (high - low <= 15) {
            insertionSort(arr, low, high);
        }

        // 將一個數組拆分爲兩個數組進行排序
        int mid = (low + high) / 2;
        mergePass2(arr, low, mid, aux);
        mergePass2(arr, mid + 1, high, aux);
        // 拆分完成後須要對這兩個數組進行排序
        merge(arr, low, mid, high, aux);
    }

    // 對數組指定索引範圍的元素使用插入排序
    public static void insertionSort(int[] arr, int low, int high) {
        for (int i = low + 1; i < high; i++) {
            int e = arr[i]; // 獲得當前這個要插入的元素
            int j;
            for (j = i; j > 0 && arr[j-1] > e; j--) {
                arr[j] = arr[j-1];
            }
            arr[j] = e;
        }
    }

    // 將arr[low, mid]和arr[mid+1, high]兩部分進行歸併
    private static void merge(int[] arr, int low, int mid, int high, int[] aux) {
        // 臨時數組的內容就是兩個待排序數組的內容,排好序的內容會放到arr中
        for (int k = low; k <= high; k++) {
            aux[k] = arr[k];
        }

        // i是左邊數組的索引
        // j是右邊數組的索引
        int i = low;
        int j = mid + 1;

        // 遍歷左邊和右邊的小數組合併到一個大數組中
        for (int k = low; k <= high; k++) {
            if (i > mid) {
                // 左邊的所有是小值提早取完了,剩下右邊的直接賦值
                arr[k] = aux[j];
                j++;
            } else if (j > high) {
                // 右邊的所有是小值提早取完了,剩下左邊的直接賦值
                arr[k] = aux[i];
                i++;
            } else if (aux[i] > aux[j]) {
                // 左邊數組的元素大於右邊數組的元素,取右邊的小元素
                arr[k] = aux[j];
                j++;
            } else { // aux[left] <= aux[right]
                // 右邊數組的元素大於等於左邊數組的元素,取左邊的小元素
                arr[k] = aux[i];
                i++;
            }
        }
    }
}

歸併排序代碼優化2

歸併排序使用遞歸進行排序,在代碼編寫和閱讀上比較清晰,容易理解,可是當待排數據量很大時,遞歸會形成時間和空間上的性能損耗,而且可能會形成棧溢出。咱們排序追求的就是效率,能夠將遞歸轉化成迭代,也就是自底向上歸併排序。從而改善歸併排序的性能。

自底向上歸併排序能夠分爲兩個過程:

  1. 合併排序:從子序列長度爲1開始,進行兩兩合併排序,獲得2倍長度的有序大序列。
  2. 循環:子序列長度從1開始,循環讓子序列長度是原先的兩倍,不斷進行合併排序。

自底向上歸併排序動畫效果以下:

自底向上歸併排序代碼以下:

public class BottomUpMergeSortTest2 {
    public static void main(String[] args) {
        int[] arr = new int[] {7, 3, 1, 9, 5, 2, 8, 6};
        mergeSortBottomUp(arr);
        System.out.println(Arrays.toString(arr));
    }

    // 不使用遞歸,自底向上的歸併排序
    public static void mergeSortBottomUp(int[] arr) {
        // 使用一個額外的數組,容量和要排序的數組容量同樣大
        int[] aux = new int[arr.length];

        // 在for循環中對數組進行拆分
        // size爲子數組的長度
        for (int size = 1; size < arr.length; size += size) { // size = 1, 2, 4
            for (int low = 0; low < arr.length -size; low += size+size) {
                // 優化: 若是左邊子數組的最後一個元素大於右邊子數組的最小值說明須要排序
                if (arr[low+size-1] > arr[low+size]) {
                    merge(arr, low, low + size -1, Math.min(low + size + size - 1, arr.length - 1), aux);
                }
            }
        }
    }

    // 將arr[low, mid]和arr[mid+1, high]兩部分進行歸併
    private static void merge(int[] arr, int low, int mid, int high, int[] aux) {
        // 臨時數組的內容就是兩個待排序數組的內容,排好序的內容會放到arr中
        for (int k = low; k <= high; k++) {
            aux[k] = arr[k];
        }

        // i是左邊數組的索引
        // j是右邊數組的索引
        int i = low;
        int j = mid + 1;

        // 遍歷左邊和右邊的小數組合併到一個大數組中
        for (int k = low; k <= high; k++) {
            if (i > mid) {
                // 左邊的所有是小值提早取完了,剩下右邊的直接賦值
                arr[k] = aux[j];
                j++;
            } else if (j > high) {
                // 右邊的所有是小值提早取完了,剩下左邊的直接賦值
                arr[k] = aux[i];
                i++;
            } else if (aux[i] > aux[j]) {
                // 左邊數組的元素大於右邊數組的元素,取右邊的小元素
                arr[k] = aux[j];
                j++;
            } else { // aux[left] <= aux[right]
                // 右邊數組的元素大於等於左邊數組的元素,取左邊的小元素
                arr[k] = aux[i];
                i++;
            }
        }
    }
}

自底向上歸併排序,避免了遞歸時深度爲log2n的棧空間,額外使用了aux數組和原數組同樣大小的空間,所以空間複雜度爲0(n),而且避免了遞歸在時間性能上有必定的提高,因此使用歸併排序時,優先考慮非遞歸方法。

歸併排序複雜度

一張圖看懂歸併排序時間複雜度,以下圖:

歸併排序會將數據規模爲n的數據拆分紅

每次比較n次,所以總的時間複雜度爲

歸併排序空間複雜度複雜度:由於歸併排序過程當中須要使用一個和原始數組相同大小的輔助數組,因此歸併排序的空間複雜度爲O(n)。

總結

  1. 歸併排序思想,將原數據序列分紅大小相等的兩個子序列,繼續劃分子序列,直到子序列有序時,將劃分的有序子序列合併成大的有序序列,最終合併成一個有序序列。歸併排序分紅兩個步驟:1.拆分,2.合併。
  2. 歸併排序代碼優化1,小數據規模時改用插入排序。
  3. 自底向上歸併排序,避免遞歸時間和空間上的額外消耗。

原創文章和動畫製做真心不易,您的點贊就是最大的支持! 想了解更多文章請關注微信公衆號:表哥動畫學編程

相關文章
相關標籤/搜索