合並排序算法


合並排序法的概念

合並排序法是最典型的分治(Divide and Conquer)演算法,將一整個序列分割成一個個元素,再兩兩一組依照元素大小填入至新的空間中來合並成新的,並且已經排序好的序列。

合並排序法的過程

假設現在有個陣列資料,內容如下:


索引    0   1   2   3   4   5   6   7   8   9
數值   69  81  30  38   9   2  47  61  32  79

要如何將它遞增排列呢?

首先將陣列對半切成:

索引    0   1   2   3   4   |   5   6   7   8   9
數值   69  81  30  38   9   |   2  47  61  32  79

再對半切成:

索引    0   1   |   2   3   4   ||   5   6   |   7   8   9
數值   69  81   |  30  38   9   ||   2  47   |  61  32  79

再對半切成:

索引    0   |   1   ||   2   |   3   4   |||   5   |   6   ||   7   |   8   9
數值   69   |  81   ||  30   |  38   9   |||   2   |  47   ||  61   |  32  79

再對半切成:

索引    0   ||   1   |||   2   ||   3   |   4   ||||   5   ||   6   |||   7   ||   8   |   9
數值   69   ||  81   |||  30   ||  38   |   9   ||||   2   ||  47   |||  61   ||  32   |  79

已經不能再切了,即可開始合並,一邊合並一邊把元素照順序排好:

索引    0    |   1    ||   2    |   3       4    |||   5   |    6    ||   7    |   8       9
數值   69    |  81    ||  30    |   9      38    |||   2   |   47    ||  61    |  32      79
                                   →───←                                     →───←

繼續合併:

索引    0        1     |   2        3       4     ||   5       6     |   7        8       9
數值   69       81     |   9       30      38     ||   2      47     |  32       61      79
       →────←       →────────←        →───←        →────────←

繼續合併:

索引    0        1         2        3       4      |   5       6         7        8       9
數值    9       30        38       69      81      |   2      32        47       61      79
       →─────────────────←         →─────────────────←

技術合併:

索引    0        1         2        3       4          5       6         7        8       9
數值    2        9        30       32      38         47      61        69       79      81
       →────────────────────────────────────────←

合並完成,也排序完成了!

merge-sort

以上過程看起來十分直覺。

合並排序法的程式實作

/**
 * 合並排序法(遞增),使用遞回。此為用來遞回呼叫的函數。
 */
public static void mergeSortRecursively(final int[] array, final int[] buffer, final int start, final int end) {
    final int length = end - start + 1;
 
    if (length < 2) {
        return;
    }
 
    final int middle = length / 2 + start;
 
    int ls = start;
    final int le = middle - 1;
    int rs = middle;
    final int re = end;
 
    mergeSortRecursively(array, buffer, ls, le);
    mergeSortRecursively(array, buffer, rs, re);
 
    int p = start;
 
    while (ls <= le && rs <= re) {
        buffer[p++] = array[ls] < array[rs] ? array[ls++] : array[rs++];
    }
 
    while (ls <= le) {
        buffer[p++] = array[ls++];
    }
 
    while (rs <= re) {
        buffer[p++] = array[rs++];
    }
 
    System.arraycopy(buffer, start, array, start, length);
}
 
/**
 * 合並排序法(遞增),使用遞回。
 */
public static void mergeSort(final int[] array) {
    final int[] buffer = new int[array.length];
 
    mergeSortRecursively(array, buffer, 0, array.length - 1);
}
 
/**
 * 合並排序法(遞減),使用遞回。此為用來遞回呼叫的函數。
 */
public static void mergeSortDescRecursively(final int[] array, final int[] buffer, final int start, final int end) {
    final int length = end - start + 1;
 
    if (length < 2) {
        return;
    }
 
    final int middle = length / 2 + start;
 
    int ls = start;
    final int le = middle - 1;
    int rs = middle;
    final int re = end;
 
    mergeSortDescRecursively(array, buffer, ls, le);
    mergeSortDescRecursively(array, buffer, rs, re);
 
    int p = start;
 
    while (ls <= le && rs <= re) {
        buffer[p++] = array[ls] > array[rs] ? array[ls++] : array[rs++];
    }
 
    while (ls <= le) {
        buffer[p++] = array[ls++];
    }
 
    while (rs <= re) {
        buffer[p++] = array[rs++];
    }
 
    System.arraycopy(buffer, start, array, start, length);
}
 
/**
 * 合並排序法(遞減),使用遞回。
 */
public static void mergeSortDesc(final int[] array) {
    final int[] buffer = new int[array.length];
 
    mergeSortDescRecursively(array, buffer, 0, array.length - 1);
}

在實作程式的時候,應該要避免使用遞回(Recursive)的結構,因為遞回需要不斷地重新建構函數的堆疊空間,硬體資源的負擔會比較大,且若是遞回層數過多還會導致堆疊溢出(Stack Overflow)。所以比較好的做法還是在一個函數內以回圈迭代的方式來完成。

為了簡化遞回轉迭代結構時的問題,在這里不采取從中間開始分割的方式,而是直接跳過分割的動作,從合並開始進行,在觀察合並排序算法的分割過程后,其實不難發現完整的子陣列的長度,合並過后的長度都會再乘以2,並且即便是未達2的冪長度的陣列,也能進行合並的動作,意思就是說,分割的動作其實並非必要,可以直接從前端開始直接合並2的冪個元素,先從1(20)個元素中兩兩開始合並,再從2(21)個元素兩兩合並,再從4(22)個元素兩兩開始合並,再從8(23)個元素兩兩開始合並,一次類推,因此可以寫出如下迭代版的合並排序算法。

/**
 * 合並排序法(遞增),使用回圈來迭代。
 */
public static void mergeSort(final int[] array) {
    final int length = array.length;
    final int lengthDec = length - 1;
    final int[] buffer = new int[length];
 
    for (int width = 1; width < length; width *= 2) {
        final int doubleWidth = width * 2;
        final int e = length - width;
 
        for (int start = 0; start < e; start += doubleWidth) {
            int end = start + doubleWidth - 1;
 
            if (end >= length) {
                end = lengthDec;
            }
 
            final int middle = start + width;
 
            int ls = start;
            final int le = middle - 1;
            int rs = middle;
            final int re = end;
 
            int p = start;
 
            while (ls <= rs && rs <= re) {
                if (array[ls] < array[rs]) {
                    buffer[p++] = array[ls];
                } else {
                    buffer[p++] = array[rs++];
                }
            }
 
            while (ls <= le) {
                buffer[p++] = array[ls++];
            }
 
            while (rs <= re) {
                buffer[p++] = array[rs++];
            }
 
            System.arraycopy(buffer, start, array, start, end - start + 1);
        }
    }
}
 
/**
 * 合並排序法(遞減),使用回圈來迭代。
 */
public static void mergeSortDesc(final int[] array) {
    final int length = array.length;
    final int lengthDec = length - 1;
    final int[] buffer = new int[length];
 
    for (int width = 1; width < length; width *= 2) {
        final int doubleWidth = width * 2;
        final int e = length - width;
 
        for (int start = 0; start < e; start += doubleWidth) {
            int end = start + doubleWidth - 1;
 
            if (end >= length) {
                end = lengthDec;
            }
 
            final int middle = start + width;
 
            int ls = start;
            final int le = middle - 1;
            int rs = middle;
            final int re = end;
 
            int p = start;
 
            while (ls <= rs && rs <= re) {
                if (array[ls] > array[rs]) {
                    buffer[p++] = array[ls];
                } else {
                    buffer[p++] = array[rs++];
                }
            }
 
            while (ls <= le) {
                buffer[p++] = array[ls++];
            }
 
            while (rs <= re) {
                buffer[p++] = array[rs++];
            }
 
            System.arraycopy(buffer, start, array, start, end - start + 1);
        }
    }
}

實際上使用合並排序法時,常會去檢查子序列的大小是否過長(長度大於7~15),如果子序列長度不長,會使用適合拿來排序少量資料的插入排序等演算法來完成排序。

合並排序法的復雜度


項目 備注
最差時間復雜度 O(nlogn)O(nlog⁡n)  
最佳時間復雜度 O(nlogn)O(nlog⁡n)  
平均時間復雜度 O(nlogn)O(nlog⁡n)  
額外最差空間復雜度 O(n)O(n)  
是否穩定


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM