归并排序

简述

算法导论中,在第二章提及了归并排序,归并排序是分治思想的一个重要实现,只要提及分治算法,就不得不提及归并排序。

原理

归并排序有 2 个步骤:

  1. 将数据平均分成 2 个序列,递归,将 2 个部分继续分解。直到序列中只剩下一个元素。
  2. 分解成功后,递归返回,进行合并,使用一个临时数组,将 2 个序列的数据进行有序合并,通过遍历序列中的元素,依次将最小的元素插入到临时序列中。最后,将临时序列中的元素依次插入到原来的数组中。

下面有张图,简单说明了一组无序数组,最终变成有序数组的过程:

图中,黑色的线,表示分解。红色的线,表示合并,且合并的过程中,使用到了临时数组。

最终,无序数组,就变成了有序数组。

代码实现:

static public void mergeSort(int[] arr, int low, int high) {
        if (low >= high) {
            return;
        }
        int mid = low + (high - low) / 2;
        mergeSort(arr, low, mid);
        mergeSort(arr, mid + 1, high);

        merge(arr, low, mid, high);
    }

    static public void merge(int[] arr, int low, int mid, int high) {
        int i = low;
        // mid 是分隔点
        int j = mid + 1;

        int[] tmp = new int[high - low + 1];
        int tmpIndex = 0;

        while (j <= high && i <= mid) {
            if (arr[j] <= arr[i]) {
                tmp[tmpIndex++] = arr[j++];
            } else {
                tmp[tmpIndex++] = arr[i++];
            }
        }

        int start = I;
        int end = mid;

        // 由于最后是 i++, 所以,这里判断要加 1
        if (i >= mid + 1) {
            start = j;
            end = high;
        }

        // 剩余元素, 使用 <= end, 包括 end 元素.
        for (int k = start; k <= end; k++) {
            tmp[tmpIndex++] = arr[k];
        }

        for (int k = 0; k < tmpIndex; k++) {
            arr[low++] = tmp[k];
        }
    }

上面的代码中,步骤如下:

  1. 在 mergeSort 方法中,将序列平均分成 2 个,然后继续递归,知道元素数量是 1.
  2. 当 2 个递归序列都返回之后,我们使用 merge 函数,进行合并。

合并是 归并排序的重要部分。

合并过程:

  1. 我们必须知道原数组,low,mid ,high 数据。并创建一个长度为 hight - low + 1 的临时数组。
  2. 从 mid 进行分割,设计 2 个指针:i 和 j。i 表示 mid 左边的数据,j 表示 mid 右边的数据。
  3. i 和 j 分别自增进行比较,谁小,就把谁插入到临时数组中,同时,将该指针前进一步。
  4. 停止条件为 i <=mid,表示左边的序列比较完毕,j <= high,表示右边的序列遍历完毕。
  5. 遍历结束任一一边的序列,都应该停止,然后将另一边的序列剩余元素,顺序拷贝到临时数组。
  6. 最后,将临时数组从 low 下标开始,插入到原数组中,完成排序。

下面是一个图示:

如果我们现在有 2 个序列
{0,3,4,8,9} 和 {1,2,5,6},合并的过程如下:

1
2

这里,i 和 j 开始比较。i 小,那么,就需要把 i 的元素,插入到右边的临时数组中。然后 i 前进一步。

3

i 前进一步继续和 j 比较,此时 j 要小,那么就将 j 插入到临时数组中,同时,j 前进一步。

4

继续,j 又前进一步。

5

继续比较,此时 i 比 j 小,应该把 i 插入到临时数组中,i 前进一步。

6

继续比较,i 还是小,重复上一步。

7

此时 j 比 i 小,j 插入到临时数组中,j 前进一步。

8

此时,j 已经达到尽头,无法继续循环,应当停止,并且,将 i —— mid 的元素,追加到临时数组中。完成排序。

复杂度

计算归并排序需要用到递归式:

image.png

通过推演,我们知道:

最好时间复杂度:
最坏时间复杂度:
平均时间复杂度(期望运行时间):

归并排序是一个非常稳定的排序算法。

由于每次递归都需要拷贝一个临时数组,而这个临时数组的长度最大就是 n,所以:
空间复杂度:

归并排序是稳定的吗?

我们假设两个数组中,有 2 个相同的元素:

image.png

我们必须将左边的元素先拷贝,然后再拷贝右边的元素,保证他们的顺序是不变的。
因此,我们的代码比较应该是:


if(arr[i] <= arr[j] {
    插入 i;
}

因此,归并排序是稳定的。

另外,归并排序不是原地排序,因为,归并排序每次合并的过程中,都会使用一个临时数组。

你可能感兴趣的:(归并排序)