1. 程式人生 > 其它 >演算法-歸併排序

演算法-歸併排序

歸併排序

一、簡述

  • 歸併排序是將兩個或兩個以上的有序表組合成一個新的有序表。

    其基本思想是: 先將 N 個數據看成 N 個長度為 1 的表,將相鄰的表成對合並,得到長度為 2 的 N/2 個有序表,進一步將相鄰的合併,得到長度為 4 的 N/4 個有序表,以此類推,直到所有資料均合併成一個長度為 N 的有序表為止。每一次歸併過程稱做一趟。

  • 那麼歸併排序就可以分解為,分解歸併兩個子問題。

二、分解

分解方式分為“從上往下”“從下往上”兩種方式。如下圖所示:

2.1 從上往下(遞迴)

分解:將當前空間一分為二,即求分裂點 mid=(left+right)/2;

求解:遞迴地對兩個子區間 a[left...mid]和 a[mid+1...right]進行歸併排序。遞迴的結束條件為子區間長度為 1(或left>=right)。

    /**
     * 歸併排序 ,從上往下(遞迴)
     * @param arr 需要排序的陣列
     * @param left 左節點
     * @param right 右節點
     */
    public void mergeSort1(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }

        int mid = (left + right) / 2;
        // 左邊
        mergeSort1(arr,left,mid);
        // 右邊
        mergeSort1(arr, mid + 1, right);
        // 合併
        mergeArr(arr, left, mid, right);
    }

2.2 從下往上

將待排序的數列分成若干個長度為 1 的子數列,然後將這些數列兩兩合併;得到若干個長度為 2 的有序數列,再將這些數列兩兩合併,得到長度為 4 的有序數列,直到合併成一個完整數列為止。這就得到了我們想要的排序結果

    /**
     * 歸併排序 ,從下往上(非遞迴)
     *
     * @param arr   需要排序的陣列
     */
    public void mergeSort2(int[] arr) {
        if (null == arr || arr.length < 2) {
            return;
        }
        // 每組的個數
        int mergeSize = 1;
        int N = arr.length;
        while (mergeSize < N) {
            int l = 0;
            while (l < N) {
                int m = l + mergeSize - 1;
                while (m >= N) {
                    break;
                }
                int R = Math.min(m + mergeSize, N - 1);
                mergeArr(arr, l, m, R);

                l = R + 1;
            }
            
            // 越界處理,一般情況不會出現
            if (mergeSize > N / 2) {
                break;
            }
            mergeSize <<= 1;
        }
    }

2.3歸併方法

將兩個子區間 arr[low...mid]和 arr[mid+1...high]歸併為一個有序的區間 arr[low...high]。

在歸併的過程中需要申請一個臨時陣列空間,將待排序的兩陣列順序的儲存在該臨時空間中。最後將有序的臨時空間覆蓋到原陣列中。此時陣列就變為區域性有序了。

    /**
     * 合併左右兩個區間陣列
     * @param arr
     * @param left
     * @param mid
     * @param right
     */
    public void mergeArr(int[] arr, int left, int mid, int right) {
        // 臨時的陣列
        int[] temp = new int[right - left + 1];
        int tempIndex = 0;
        int p1 = left;
        int p2 = mid + 1;

        // p1 和p2 不越界的情況下
        while (p1 <= mid && p2 <= right) {
            temp[tempIndex++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }

        // 拷貝p1剩下部分
        while (p1 <= mid) {
            temp[tempIndex++] = arr[p1++];
        }
        // 拷貝p1剩下部分
        while (p2 <= right) {
            temp[tempIndex++] = arr[p2++];
        }
        // 將這部分排好序的部分刷入
        for (int i = 0; i < temp.length; i++) {
            arr[left + i] = temp[i];
        }
    }

三、效能分析

3.1 時間複雜度:

歸併排序的時間複雜度是O(nlgn)。

歸併排序的形式就是二叉樹,需要遍歷的次數就是二叉樹的深度,根據完全二叉樹的性質,其深度為lgn,則得出時間複雜度為O(n*lgn)。

public class MergeSortDemo1 {


    /**
     * 歸併排序 ,從上往下(遞迴)
     * @param arr 需要排序的陣列
     * @param left 左節點
     * @param right 右節點
     */
    public void mergeSort1(int[] arr, int left, int right) {
        if (left >= right) {
            return;
        }

        int mid = (left + right) / 2;

        // 左邊
        mergeSort1(arr,left,mid);

        // 右邊
        mergeSort1(arr, mid + 1, right);

        // 合併
        mergeArr(arr, left, mid, right);
    }


    /**
     * 歸併排序 ,從下往上(非遞迴)
     *
     * @param arr   需要排序的陣列
     */
    public void mergeSort2(int[] arr) {
        if (null == arr || arr.length < 2) {
            return;
        }
        // 每組的個數
        int mergeSize = 1;
        int N = arr.length;
        while (mergeSize < N) {
            int l = 0;
            while (l < N) {
                int m = l + mergeSize - 1;

                while (m >= N) {
                    break;
                }

                int R = Math.min(m + mergeSize, N - 1);

                mergeArr(arr, l, m, R);

                l = R + 1;
            }

            // 越界處理,一般情況不會出現
            if (mergeSize > N / 2) {
                break;
            }
            mergeSize <<= 1;
        }
    }

    /**
     * 合併左右兩個區間陣列
     * @param arr
     * @param left
     * @param mid
     * @param right
     */
    public void mergeArr(int[] arr, int left, int mid, int right) {
        // 臨時的陣列
        int[] temp = new int[right - left + 1];
        int tempIndex = 0;
        int p1 = left;
        int p2 = mid + 1;

        // p1 和p2 不越界的情況下
        while (p1 <= mid && p2 <= right) {
            temp[tempIndex++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
        }

        // 拷貝p1剩下部分
        while (p1 <= mid) {
            temp[tempIndex++] = arr[p1++];
        }
        // 拷貝p1剩下部分
        while (p2 <= right) {
            temp[tempIndex++] = arr[p2++];
        }
        // 將這部分排好序的部分刷入
        for (int i = 0; i < temp.length; i++) {
            arr[left + i] = temp[i];
        }
    }

    private void sort1(int[] arr) {
        mergeSort1(arr, 0, arr.length - 1);
    }

    private void sort2(int[] arr) {
        mergeSort2(arr);
    }

    @Test
    public void testMergeSort1() {
        int[] arr = {11, 7, 2,11, 9, 8, 18};
        sort1(arr);

        for (int i : arr) {
            System.out.print(i+" ");
        }

    }

    @Test
    public void testMergeSort2() {
        int[] arr = {11, 7, 2,11, 9, 8, 18};
        sort2(arr);

        for (int i : arr) {
            System.out.print(i+" ");
        }
    }


}