合并排序算法

合并排序法的概念

合并排序法是最典型的分治(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)  
是否稳定
posted @ 2019-05-08 17:31  田维常  阅读(3065)  评论(0编辑  收藏  举报