归并排序

Author Avatar
Brian Lee 8月 06, 2018
  • 在其它设备中阅读本文章

《Algorithm》(Sedgewick)笔记:归并排序


原理

归并

将两个有序数组归并成一个单一的有序数组

归并排序

  • 归并排序是一种递归的算法,采用分而治之的思想,持续地将一个数组分成两半。
  • 如果数组为空或只有一个元素,则此子数组被排序好(基本情况)。
  • 如果数组里元素超过一个,就把数组拆分为两部分,并分别调用递归排序。
  • 两部分排序好后,就可以对两部分数组进行归并。

动图演示

归并排序

动图来源(侵删)https://zhuanlan.zhihu.com/p/40695917


复杂度

时间复杂度

$O(NlogN)$

空间复杂度

$O(N)$


归并

作用

将a[lo..mid]与a[mid+1..hi]有序归并于a[lo..hi]

代码

public static void merge(Comparable[] a, int lo, int mid, int hi) {
        int i = lo, j = mid + 1;
        //将a[lo..hi]复制到aux[lo..hi]
        for (int k = lo; k <= hi; k++)
            aux[k] = a[k];
        for (int k = lo; k <= hi; k++) {
            if (i > mid)    //左半边用尽
                a[k] = aux[j++];
            else if (j > hi)    //右半边用尽
                a[k] = aux[i++];
            else if (less(aux[j], aux[i]))  //右半边元素小于左半边
                a[k] = aux[j++];
            else    //左半边元素小于右半边
                a[k] = aux[i++];
        }
    }

图示

归并过程

辅助数组

不能将辅助数组aux[]声明为merge()方法的局部变量。这是为了避免每次归并时,即使归并很小的数组,也要创建一个新数组。如果这么做,创建新数组将成为归并排序运行时间的主要部分。


自顶向下(递归)归并排序

代码

private static Comparable[] aux;    //辅助数组

public static void sort(Comparable[] a) {
        aux = new Comparable[a.length];
        sort(a, 0, a.length - 1);
    }

public static void sort(Comparable[] a, int lo, int hi) {
        if (lo >= hi)
            return;
        int mid = (lo + hi) / 2;
        sort(a, lo, mid);
        sort(a, mid + 1, hi);
        if (less(a[mid + 1], a[mid]))
            merge(a, lo, mid, hi);
    }

图示

自顶向下归并排序

调用轨迹

自顶向下调用轨迹

源码

https://github.com/XutongLi/Algorithm-Learn/blob/master/src/S2_Sorting/S2_2_2_Top_Down_MergeSort/MergeTD.java


自底向上(非递归)归并排序

先归并微型数组,再成对归并得到的子数组,直到将整个数组归并在一起

代码

private static Comparable[] aux;    //辅助数组

public static void sort(Comparable[] a) {
        int N = a.length;
        aux = new Comparable[N];
        for (int sz = 1; sz < N; sz += sz) {
            for (int lo = 0; lo < N; lo += 2 * sz) {
                merge(a, lo, lo + sz - 1, Math.min(lo + 2 * sz - 1, N - 1));
            }
        }
    }

自底向上的归并排序会多次遍历整个数组,根据子数组大小进行两两归并。子数组的大小sz的初始值为1,每次加倍。最后一个子数组的大小只有在数组大小是sz的偶数倍的时候才会等于sz(否则它会比sz小)。

自底向上的归并排序适合用链表组织的数据。

当数组长度为2的幂时,自顶向下和自底向上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同。

图示

自底向上归并排序

源码

https://github.com/XutongLi/Algorithm-Learn/blob/master/src/S2_Sorting/S2_2_3_Bottom_Up_Merge/MergeBU.java


时间复杂度分析

比较次数

依赖树分析

子数组依赖树

此依赖树的每个结点都表示一个sort()方法通过merge()方法归并而成的子数组。

这棵树有 $n$ 层,$n=logN$ 。

对于 $0$ 到 $n-1$ 之间的任意 $k$ ,自顶向下的第 $k$ 层有 $2^k$ 个子数组,每个子数组的长度为 $\frac{2^n}{2^k}=2^{n-k}$ ,归并最多需要 $2^{n-k}$ 次比较。

因此每层的比较次数为 $2^k \times 2^{n-k} = 2^n$ ,$n$ 层总共为 $n2^n=NlogN$ 。

数学计算

令 $C(n)$ 表示将一个长度为 $N$ 的数组排序时所需要的比较次数。

有 $C(0)= C(1) = 0$

对于 $N>0$ ,比较次数的上限为 :$C(N)\leq C(\lfloor{N/2}\rfloor)+C(\lceil{N/2}\rceil)+N$ 。(右边第一项为左半部分排序比较次数,第二项为右半部分排序比较次数,第三项是归并所用比较次数)

因为归并比较次数最少为 $\lfloor{N/2}\rfloor$ ,所以比较次数下限为:$C(N)\geq qC(\lfloor{N/2}\rfloor)+C(\lceil{N/2}\rceil)+\lfloor{N/2}\rfloor$

因为 $\lfloor{N/2}\rfloor=\lceil{N/2}\rceil=2^{n-1}$ ,

所以对于上限

$C(2^n)=2C(2^{n-1})+2^n$ ,

两边同时除以 $2^n$ ,得 $C(2^n)/2^n=C(2^{n-1})/2^{n-1}+1$ ,

迭代得 $C(2^n)/2^n=C(2^0)/2^0+n$

两边同时乘以 $2^n$ ,得 $C(N)=C(2^n)=n2^n=NlogN$ 。

对于下限

同理可得 $C(N)=\frac{1}{2}NlogN$ 。

所以对于长度为 $N$ 的任意数组,归并排序需要 $\frac{1}{2}NlogN$ 至 $NlogN$ 次比较。

数组访问次数

对于长度为 $N$ 的任意数组,每次归并需要访问数组 $6N$ 次:

$2N$ 次用来复制(访问a[]和aux[]各 $N$ 次)

$2N$ 次将排好序的元素移动回去(访问a[]和aux[]各 $N$ 次)

最多比较 $2N​$ 次。

总共进行 $logN$ 次归并,所以归并排序最多需要访问数组 $6NlogN$ 次。

综述

由比较次数与数组访问次数可知,归并排序的时间复杂度为 $O(NlogN)$ 。


缺点

辅助数组使用的额外空间和 $N$ 的大小成正比。


本博客采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!
本文链接:http://brianleelxt.top/2018/08/06/mergeSort/