1. 程式人生 > >歸併排序--一種高階排序演算法

歸併排序--一種高階排序演算法

歸併排序(mergeSort)介紹

歸併排序是一種高階排序演算法,速度僅次於快速排序,為穩定排序演算法,沒有對應的簡單排序演算法。

思路推演:

1、想法應該是從發現兩個有序數組合成一個有序陣列是O(n)開始的,如何將兩個有序資料合成一個有序陣列?依次從兩個陣列中取數進行比較,將較小的放入新陣列。
準備兩個索引i=0和j=0,逐個比較arrA[i]和arrB[j],將較小值給新陣列,並將較小值所在資料的索引++,一旦某個索引++之後超出了陣列的長度,說明這個陣列全部放過去了,這時就可以將另一個數組餘下值按次序放入新陣列。
一次迴圈比較即可完成,上面的邏輯的時間複雜度是O(n)
2、假設將一個大陣列,一分為2,二分為4,,,最終拆分成最多隻有兩個元素的陣列,將這兩個元素的陣列比較一次,排個序。然後逐個順序合併起來,就是歸併排序。

時間複雜度:

O(log(n))
拆開陣列,O(logn),2個元素的陣列每個比較一次O(1)*n/2 = O(n/2),合併的時候會每一次合併是n,一共logn層,所以合併是O(nlogn),總時間是O(nlogn) + O(n/2) + O(logn),去除低次的就是O(nlogn)的時間複雜度,空間複雜度看怎麼用,可以在每一次合併的時候建立一個temp陣列,這樣需要從2+ 4+ 8+ …+ n個長度的陣列,也就是O(n^2)的空間。也可以從頭到尾使用一個temp陣列,在一開始的時候根據arr的length建立,帶著走完整個演算法,根據起止位置,分段取用。

穩定性:

穩定排序演算法

屬於相鄰比較,交換,所以可以實現為穩定的。是否穩定只是說能不能實現穩定,假設對於兩個元素的陣列排序時,採用前面大等於後面則交換,那就是不穩定的,採用前面大於後面才交換,就是穩定的。
高階演算法唯一能實現為穩定演算法的。

程式碼思路:

這個程式碼生生去想,很難直接寫出來,掉了不少次坑才寫出來。
1、前面說的拆分成最多2個長度的陣列和後續的比較合併,其實都可以在一個數組內完成,藉助遞迴,一分為二、二分為四,也就是每次要從中間分開(當然也可以不從中間分開,哪怕我們強行分成前面2個元素和後面所有元素也可以,但是二分能達到logn。所以要理解演算法,而不是死記硬背)。
2、傳入開始index和結束index
3、判斷startIndex是否小於endIndex,遞迴應該結束的情況是endIndex - startIndex == 1或者等於0,(最後發現這個其實也可以放到遞迴裡,但是那是優化之後的,一開始按照最原始的演算法思路來實現)
4、如果endIndex-startIndex > 1(比如0 1 2),那麼陣列就是可以繼續拆分的,繼續拆成前半部分和後半部分,將前半部分和後半部分分好後,merge成一個大陣列。
5、虛擬碼

// 為了方便運算,endIndex第一次傳入為(陣列長度-1)。
mergeSort(arr, startIndex, endIndex){
    if(startIndex != endIndex){
        if(endIndex - startIndex == 1){
            // 將這個最小陣列交換
        }else{
            int midIndex = (startIndex + endIndex) >> 1;
            //前半部分startIndex, midIndex
            //後半部分midIndex + 1, endIndex
            //合併
        }
    }
}

//
mergeArray(arr, startIndex, midIndex, endIndex){
// 根據這三個確定兩個相鄰的資料段,認為是兩個陣列,進行資料整合
// 這個時候每次需要新建一個長度為(endIndex - startIndex + 1)的temp陣列,才方便合併,或者最開始新建一個大的長度為原始陣列長度的temp陣列,然後一直帶著走。理論上後者效率更高一些。
}

Java程式碼實現:

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

/**
 * 作為一個引子,因為下面的方法需要用到遞迴,這樣寫好看一寫,也可以直接寫到main方法中
 * @param arr
 */
private static void mergeSort(int[] arr){
    doMergeSort(arr, 0, arr.length - 1);
}

/**
 * 遞迴 1 3 2 4
 * @param arr
 * @param startIndex
 * @param endIndex
 * @return
 */
private static void doMergeSort(int[] arr, int startIndex,int endIndex){
    // 等於當前陣列的前半部分和後半部分先mergeSort再mergeArray
    // 前半部分和後半部分長度小於等於2(差小等於1)的時候,分拆的遞迴結束,開始往回收攏,逐步合成一個大陣列
    if(startIndex < endIndex){
    // 找到中間位置
        int midIndex = (startIndex + endIndex) >> 1;
        doMergeSort(arr, startIndex, midIndex);
        doMergeSort(arr, midIndex + 1, endIndex);
        mergeArray(arr, startIndex ,midIndex,endIndex);
    }
    /*if(startIndex != endIndex){
        // 如果間隔是1,進行排序,如果間隔是0,不排序,如果間隔大於1繼續分拆
        if(endIndex - startIndex == 1){
            if(arr[startIndex] > arr[endIndex]){
                int tmp = arr[startIndex];
                arr[startIndex] = arr[endIndex];
                arr[endIndex] = tmp;
            }
        }else{
            int midIndex = (startIndex + endIndex) >> 1;
            doMergeSort(arr, startIndex, midIndex);
            doMergeSort(arr, midIndex + 1, endIndex);
            mergeArray(arr, startIndex ,midIndex,endIndex);
        }
    }*/
}

/**
 * 假設陣列為1 3 2 4或者 1 2 4
 *  程式碼難以直接寫出來的時候,舉幾個簡單的例子激發一下思路
 * @param arr
 * @param startIndex
 * @param midIndex
 * @param endIndex
 * @return
 */
private static void mergeArray(int[] arr, int startIndex, int midIndex,int endIndex){
    //
    int len = endIndex - startIndex + 1;
    int[] temp = new int[len];
    int i = startIndex;
    int j = midIndex + 1;
    int tempIndex = 0;
    // temp用來簡化操作,先放到temp中,再全部丟到arr中
    // 直到一方走完
    while(true){
        if(arr[i] <= arr[j]){
            temp[tempIndex] = arr[i];
            i++;
        }else{
            temp[tempIndex] = arr[j];
            j++;
        }
        tempIndex++;
        if(i > midIndex){
            // j中剩下的不動,arr中的也不用動
            break;
        }
        if(j > endIndex){
            // i中剩下的需要全部移動到temp中國
            // 剩下的個數為m = midIndex - i + 1
            // 剩下的全部填滿(也就是填滿是len - m)
            for(int z = i; z <= midIndex;z++){
                temp[len - (midIndex - z + 1)] = arr[z];
                tempIndex++;
            }
            break;
        }
    }

   // merge過來,一直merge到startIndex + tempIndex,因為temp的有效資料到tempIndex(不包括tempIndex)
    for(i = startIndex; i < startIndex + tempIndex; i++){
        arr[i] = temp[i - startIndex];
    }
}

使用一個temp陣列的寫法

// 使用一個temp的歸併排序
public static void main(String[] args) {
        int[] arr = new int[]{10, 8, 5, 1, 4, 2, 3};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
    }

    private static void mergeSort(int[] arr) {
        int[] temp = new int[arr.length];
        doMergerSort(arr, 0, arr.length - 1, temp);
    }

    private static void doMergerSort(int[] arr, int startIndex, int endIndex, int[] temp) {
        if (startIndex < endIndex) {
            // 如果等於1,遞迴結束
            // 比較最小的元素,但是比較最小的元素,也可以看成兩個長度為1的陣列的合併,所以邏輯可以寫到最後,直到startIndex == endIndex
//            if(endIndex - startIndex == 1){
//            }else{
//            }
            // 將左邊進行mergeSort
            // 將右邊進行mergeSort
            // 合併左右兩邊
            int midIndex = (startIndex + endIndex) >> 1;
            doMergerSort(arr, startIndex, midIndex, temp);
            doMergerSort(arr, midIndex + 1, endIndex, temp);
            mergeArray(arr, startIndex, midIndex, endIndex, temp);
        }
    }

    /**
     * 合併陣列
     *
     * @param arr
     * @param startIndex
     * @param midIndex
     * @param endIndex
     * @param temp
     * @return
     */
    private static void mergeArray(int[] arr, int startIndex, int midIndex, int endIndex, int[] temp) {
        // temp陣列的索引,從startIndex到最終停止的位置的資料將會被複制到arr
        int tempIndex = startIndex;
        // 左邊陣列起始位置
        int i = startIndex;
        // 右邊陣列起始位置
        int j = midIndex + 1;

        // 從兩個陣列中逐個取第一個未被選定的數進行比較,取較小值放入temp中
        while (true) {
            // 為保證穩定性,前面小於等於後面
            if (arr[i] <= arr[j]) {
                temp[tempIndex] = arr[i];
                i++;
            } else {
                temp[tempIndex] = arr[j];
                j++;
            }
            tempIndex++;

            // 如果i超出了邊界,tempIndex會停留在j中的某個位置,不需要複製,迴圈停止
            if (i > midIndex) {
                break;
            }

            // 如果j超出了邊界,需要將i中剩餘的資料,複製到對應的位置
            if (j > endIndex) {
                // 複製資料
                for (; i <= midIndex; tempIndex++, i++) {
                    temp[tempIndex] = arr[i];
                }
                break;
            }
        }

        // 將temp中的資料複製到arr中,上面每次都是tempIndex++然後再退出迴圈,也就是tempIndex多加了1,需要注意邊界
        for (; startIndex < tempIndex; startIndex++) {
            arr[startIndex] = temp[startIndex];
        }
}