歸并排序 O(nLogn)

歸并排序

歸并排序的思想是分治法+回溯,將一個無序的數組先按照原來的一半進行拆分,一直拆分到最后一個元素,然后開始回溯,排序開始的過程是再回溯時開始排序的。

算法.png

思想總結:

  1. 將源數組進行拆分,每次拆分一半,由圖可以分析出,當arr.length=n,需要拆分log2^8=3次
  2. 當拆分到不能再拆分,也就是分組到每個組只有1個元素,停止拆分
  3. 開始排序并回溯排序,每次排序的時間復雜度為O(n)
  4. 總的時間復雜度為n x log2^n,時間復雜度不考慮系數和底數,所以n x log2^n等價于 O(nlogn)

==其實歸并總結就是2部分 1. 先拆分 2 回溯排序==

代碼分析
從分析我們知道,想要實現回溯,那么通常是使用遞歸的。那么回溯的問題解決了,我們要如何實現O(n)的時間復雜度呢?。以下圖i=2對應回溯為例子:

1592837889(1).jpg

如果我們分別對數組arr1={2,3,6,8},和arr2={1,4,5,7},如果使用選擇,插入,冒泡等排序都是O(n^2), 顯然最后算法變成n^2*logn。我們可以換個思路:

算法.png

說下回溯的過程:

  1. 當我們回溯排序時,源數組不變,我們將拆分后的數組歷史保存到另外的數組中,如臨時數組arr1={2,3,6,8},臨時數組arr2={1,4,5,7},臨時數組總空間O(n),(源arr的順序是上次回溯后排序得到的)。
  2. 然后我們定義幾個指針:

left->arr1 起始位置
mid->arr1結束的位置
right->arr2->結束的位置
i指向左邊待排序的元素
j指向右邊待排序的位置
k指向源數組中排好序的元素的下一個位置(指向新排序元素將要放置的位置)

  1. left,mid,right作為判斷結束的條件,i,j不斷移動指向左右數組要排序的元素,k用來作為存放元素的位置
    //對數組[l...r] 全閉空間排序
    public static void mergeSort(int[] arr) {

        mergeSort(arr, 0, arr.length - 1);
    }

    public static void mergeSort(int[] arr, int l, int r) {
        if (l == r) {//只有一個元素了,那么它就是有序的
            return;
        } else {
            //找到中間邊界mid 拆分2個數組[l...mid]和[mid+1...r]
            int mid = (r - l) / 2 + l;
            //左邊繼續拆分
            mergeSort(arr, l, mid);
            //右邊邊繼續拆分
            mergeSort(arr, mid + 1, r);

            //一直拆分到l==r 說明只有一個元素 retuen
            //然后開始回溯合并排序
            merge(arr, l, mid, r);
        }
    }

    public static void merge(int[] arr, int l, int mid, int r) {
        //1. l不一定是從0開始的 2.因為是 對數組[l...r] 全閉空間排序 所以要+1
        int[] temp = new int[r - l + 1];

        //賦值臨時數組 也就是拆分左右數組,將arr拆分成arr1和arr2
        for (int i = l; i <= r; i++) {
            //上面說過l不是從0開始的  但是我們的temp是0開始的,所以要進行l的偏移
            temp[i - l] = arr[i];
        }

        int i = l, j = mid + 1;
        for (int k = l; k <= r; k++) {
            //當arr2排序完成 但是arr1還有元素 那么直接賦值
            if (j > r && i <= mid) {
                arr[k] = temp[i - l];
                i++;
                 //當arr1排序完成 但是arr2還有元素 那么直接賦值
            } else if (i > mid && j <= r) {
                arr[k] = temp[j - l];
                j++;
            } else if (temp[i - l] < temp[j - l]) {
                arr[k] = temp[i - l];
                i++;
            } else {
                arr[k] = temp[j - l];
                j++;
            }
        }
    }

首先根據代碼理解思想。要是還是有點似懂非懂的話。下面這張圖應該能幫到你。下面描述的情況是2-2歸并。最后左面4個元素有序,右邊4個元素有序(對應上面分析圖的上一步)

算法.png

歸并排序--迭代法

歸并排序,還有一種迭代法。遞歸法歸并排序使用的是先自上而下拆分(分治),再自底向上歸并,那么如果我們直接通過遞歸,將數組按照size=1,2,4,8...n 去拆分,那么合并的數組為arr1-arr2: 1-1.2-2,4-4,8-8...左右兩邊分別代表arr1和arr2的長度。代碼:

/**
     * 自定向上排序
     *
     * @param arr 待排序數組
     * @param n   數組的長度
     */
    public static void mergeSort(int[] arr, int n) {
        /*
         * 數組分2層循環 第一層是確定分組后每個組的長度,按照上面圖的圖示所知
         * size分別為1,2,4,8...
         * 當size=1 那么arr1.length=1 arr2.length=1 所以 1-1 排序 最終 “每” 2個元素有序
         * 當size=2 那么arr1.length=2 arr2.length=2 所以 2-2 排序 最終 “每” 4個元素有序
         * 當size=4 那么arr1.length=4 arr2.length=4 所以 4-4 排序 最終 “每” 8個元素有序
         * 所以這層循環 就是為了幫助我們創建符合要求變化的size大小
         * 最開始數組長度=1 也就是1個元素 他就是有序的 那么size = 2 * size這個算式就幫助我們迭代創建size分別為1,2,4,8...
         *
         */
        for (int size = 1; size <= n; size = 2 * size) {
            /*
             *i表示的是每個要合并分作的起始坐標也就是left 我們知道 left-right是通過mid分成arr1和arr2的
             * 也就是[l...mid]-[mid+1...r] 等價于[l...size-1]和[size...r]
             * 所以i的取值變化為i = i + 2 * size
             * 同時i不能越界 所以i<n
             */
            for (int i = 0; i + size < n; i = i + 2 * size) {
                /*
                 * 上面分析[l...mid]-[mid+1...r] 等價于[l...size-1]和[size...r]
                 * 所以 i等價l i + size - 1等價mid i + size + size - 1等價r
                 * 雖然i<n合法 但是i + size可能越界,
                 * 同時當i+size>=n說明 只有arr1 arr2為null,那么也就不歸并了 他就是有序的
                 * 因為從上面得知,歸并的前提是arr1和arr2是有序的
                 * 
                 * 
                 * i + size + size - 1相當于r 他可能越界 所以Math.min(i + size + size - 1, n - 1)
                 * 
                 */
                merge(arr, i, i + size - 1, Math.min(i + size + size - 1, n - 1));
            }
        }
    }

歸并排序優化

現在直接上代碼

 public static void mergeSort(int[] arr, int l, int r) {
        if (r-l<=15) {
           insertSort(arr,l,r);// 1 .
            return;
        } else {
            //找到中間邊界mid 拆分2個數組[l...mid]和[mid+1...r]
            int mid = (r - l) / 2 + l;
            //左邊繼續拆分
            mergeSort(arr, l, mid);
            //右邊邊繼續拆分
            mergeSort(arr, mid + 1, r);

            //一直拆分到l==r 說明只有一個元素 retuen
            //然后開始回溯合并排序
            if (arr[mid] > arr[mid + 1])//2.
                merge(arr, l, mid, r);
        }
    }
  1. 當拆分到足夠小的時候選擇使用插入排序,原始是插入排序對相對有序的數組效率比較高,所以當數組越小的時候有序的幾率就越大,所以使用插入排序
  2. (arr[mid] <= arr[mid + 1]) 也就是所arr1中的最大的元素已經比arr2最小的元素還要小,那么arr1所有元素就小于等于arr2的所有元素,因為再歸并中arr1和arr2都是有序的,那么此時[l...r]就是有序的,所以當(arr[mid] > arr[mid + 1])時我們才需要排序

思考

逆序對 ?

什么是逆序對.逆序對是判斷一個數組有序程度的一個標示。一個完全有序的數組,逆序對個數=0
 1 2 3 4 5 6 8 7 一個逆序對   
 利用歸并思想,當向上歸并:
 
 2 3 6 8 | 1 4 5 7 
 
 2與1比較 1<2  那么1比左邊2 3 6 8 都要小那么此時逆序對4 
 繼續歸并到
 1 2 3 當4<6時 4比左邊6 8 小 逆序對未2  
 
 最終將計數相加

感謝:

https://juejin.im/post/5a96d6b15188255efc5f8bbd
https://juejin.im/post/5ab4c7566fb9a028cb2d9126
https://blog.csdn.net/dugudaibo/article/details/79508198
算法與數據結構-綜合提升 C++版

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。