基础算法【4】:归并排序MergeSort

  1. 归并排序:

    1. 给定一个N个项目的数组,归并排序将:

      1. 将每对单个元素(默认情况下,已排序)归并为2个元素的有序数组,

      2. 将2个元素的每对有序数组归并成4个元素的有序数组,重复这个过程......,

      3. 最后一步:归并2个N / 2元素的排序数组(为了简化讨论,我们假设N是偶数)以获得完全排序的N个元素数组。

    2. 这只是一般的想法,在我们可以讨论归并排序的真正形式之前,我们需要更多的细节。

  2. 重要的子程序,O(N)归并:

    1. 我们首先讨论归并排序算法的最重要的子程序:O( N )归并,然后解析这个归并排序算法。

    2. 给定两个大小为 N1 和 N2 的排序数组 A 和 B,我们可以在O( N ) 时间内将它们有效地归并成一个大小为 N = N1 + N2的组合排序数组。

    3. 这是通过简单地比较两个阵列的前面并始终取两个中较小的一个来实现的。 但是,这个简单但快速的O( N )合并子例程将需要额外的数组来正确地进行合并。

  3. 归并子程序C++实现方法:

    • void merge(int a[], int low, int mid, int high) {

      • // subarray1 = a[low..mid], subarray2 = a[mid+1..high], both sorted

      • int N = high-low+1;

      • int b[N]; // 讨论: 为什么我们需要一个临时的数组 b?

      • int left = low, right = mid+1, bIdx = 0;

      • while (left <= mid && right <= high) // 归并

        • b[bIdx++] = (a[left] <= a[right]) ? a[left++] : a[right++];

      • while (left <= mid) b[bIdx++] = a[left++]; // leftover, if any

      • while (right <= high) b[bIdx++] = a[right++]; // leftover, if any

      • for (int k = 0; k < N; k++) a[low+k] = b[k]; // copy back

    • }

  4. 分而治之的范式:

    1. 在我们继续之前,让我们先谈谈分而治之(Divide and Conquer,缩写为D&C),这是一个强大的解决问题的范例。

    2. 分而治之算法通过以下步骤解决(某种类型的)问题 - 比如我们的排序问题:

      1. 划分步骤:将大的原始问题划分成较小的子问题并递归地解决较小的子问题,

      2. 解决步骤:结合较小的子问题的结果来产生较大的原始问题的结果。

  5. 归并排序是分而治之的算法:

    1. 归并并排序是分而治之的排序算法。

    2. 划分步骤很简单:将当前数组分成两半(如果N是偶数,则将其完全平等,或者如果N是奇数,则一边稍大于一个元素),然后递归地对这两半进行排序。

    3. 解决步骤是最有效的工作:使用前面讨论的归并子例程合并两个(排序的)半部分以形成一个有序数组。

  6. 归并排序的实现方法:

    • void mergeSort(int a[], int low, int high) {

      • // 要排序的数组是 a[low..high]

      • if (low < high) { // base case: low >= high (0 or 1 item)

        • int mid = (low+high) / 2;

        • mergeSort(a, low , mid ); // 分成一半

        • mergeSort(a, mid+1, high); // 递归地将它们排序

        • merge(a, low, mid, high); // 解决: 归并子程序

      • }

    • }

  7. 第一部分,分析:

    1. 在归并排序中,大部分工作是在解决/归并的步骤中完成的,因为分解步骤并没有真正执行任何操作(视为O(1))。

    2. 当我们称之为归并的(a,低,中,高)时候,我们处理k =(高 - 低+ 1)项。 最多会有 k-1 个比较。 从原始数组 a 到临时数组 b 有 k 个移动,而另一个 k 移回。 总的来说,归并子例程内的操作次数 <3k-1 = O(k)。

    3. 重要的问题是这个归并子程序被调用了多少次?

  8. 第二部分,分析:

    1. 基础算法【4】:归并排序MergeSort

    2. 分析得出:递归的级别为lgn。

  9. 第三部分,分析:

    1. 级别1:2 ^ 0 = 1调用merge( ) 和 N / 2 ^ 1个项目,O(2 ^ 0 x 2 x N / 2 ^ 1)= O(N)

    2. 级别2:2 ^ 1 = 2调用 merge( ) 与N / 2 ^ 2个项目,O(2 ^ 1 x 2 x N / 2 ^ 2)= O(N)

    3. 级别3:2 ^ 2 = 4调用merge( ) 与N / 2 ^ O(2 ^ 2 x 2 x N / 2 ^ 3)= O(N)

    4. ...

    5. 级别(log N):2 ^(log N-1)(或N / 2)调用merge( ) ),其中N / 2 ^ log N(或1)个项目,O(N)

    6. 有 log(N) 个日志级别,每个级别都执行O(N)工作,因此总体时间复杂度为O(N log N)。 稍后我们会看到这是一种最佳(基于比较)的排序算法,即我们无法做得比这更好。

    7. 每个级别的时间复杂度由两部分的乘积构成:调用merge的次数 * 该级别merge的时间复杂度。以级别2为例:调用两次merge,每次merge的时间复杂度为N( 2 * N/(2*2) ),因此级别2的时间复杂度为O(2次 * 2 * N/(2*2)) = O(N)。

  10. 优缺点:

    1. 优点:

      1. 无论输入的原始顺序如何,归并排序中最重要的部分是其O(N log N)性能保证。 就这样,没有任何敌手测试用例可以使归并排序对于任何N个元素数组运行比O(N log N)更长的时间。

      2. 因此,归并排序非常适合分类非常大量的输入,因为O(N log N)比前面讨论的O(N2)排序算法增长得慢得多。

      3. 归并排序也是一个稳定的排序算法。 讨论:为什么?

    2. 缺点:

      1. 首先,从零开始实施起来并不容易(但我们不必这样做)。

      2. 其次,它在归并操作期间需要额外的O(N)存储,因此不是真正的存储效率和不到位

      3. 顺便说一句,如果你有兴趣看看为解决这些(经典)归并排序不那么好的部分做了什么,你可以阅读这个

  11. 代码:


void merge(int arr[], int low, int mid, int high)
{
    int left = low;
    int right = mid + 1;
    int num = high - low + 1;
    int index = 0;
    int *pai_arr = NULL;

    pai_arr = malloc(num * sizeof(int));
    if (NULL == pai_arr) {
        return;
    }
    memset(pai_arr, 0, num * sizeof(int));

    printf("--------------------[index: %2d-%2d]--------------------\n", low, high);
    while (left <= mid && right <= high) {
        if (arr[left] < arr[right]) {
            printf("index:%2d, data:%2d\n", left, arr[left]);
            pai_arr[index++] = arr[left++];
        } else {
            printf("index:%2d, data:%2d\n", right, arr[right]);
            pai_arr[index++] = arr[right++];
        }
    }
    while (left <= mid) {
        printf("index:%2d, data:%2d\n", left, arr[left]);
        pai_arr[index++] = arr[left++];
    }
    while (right <= high) {
        printf("index:%2d, data:%2d\n", right, arr[right]);
        pai_arr[index++] = arr[right++];
    }

    /* copy back */
    for (index = 0; index < num; index++) {
        arr[low + index] = pai_arr[index]; // index of arr: low+index, not index.
    }

    if (NULL != pai_arr) {
        free(pai_arr);
        pai_arr = NULL;
    }

    return;
}

void MergeSort(int arr[], int low, int high)
{
    if (low < high) { // <, not <=
        int mid = (high + low) / 2; // high+low, not high-low
        MergeSort(arr, low, mid);
        MergeSort(arr, mid+1, high);
        merge(arr, low, mid, high);
    }

    return;
}