《Algorithm》笔记:归并排序

本文深入讲解了归并排序的基本原理及其实现方式,包括自顶向下递归与自底向上非递归两种方法,并详细分析了其时间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文地址:http://brianleelxt.top/2018/08/06/mergeSort/


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


原理

归并

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

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

动图演示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kMsVZAGQ-1592197188115)(http://p5mjtapdi.bkt.clouddn.com/img/sort/merge.gif “归并排序”)]

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


复杂度

时间复杂度

O(NlogN)O(NlogN)O(NlogN)

空间复杂度

O(N)O(N)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++];
        }
    }
图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N2Cm2oii-1592197188116)(http://p5mjtapdi.bkt.clouddn.com/img/sort/merge1.PNG “归并”)]

辅助数组

不能将辅助数组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);
    }
图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3VxdFOIx-1592197188117)(http://p5mjtapdi.bkt.clouddn.com/img/sort/merge2.PNG “自顶向下归并排序”)]

调用轨迹

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V6apgaHA-1592197188119)(http://p5mjtapdi.bkt.clouddn.com/img/sort/merge3.PNG “自顶向下调用轨迹”)]

源码

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的幂时,自顶向下和自底向上的归并排序所用的比较次数和数组访问次数正好相同,只是顺序不同。

图示

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSFsAIMI-1592197188120)(http://p5mjtapdi.bkt.clouddn.com/img/sort/merge4.PNG “自底向上归并”)]

源码

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


时间复杂度分析

比较次数
依赖树分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cK4vvm5Y-1592197188121)(http://p5mjtapdi.bkt.clouddn.com/img/sort/merge5.PNG “子数组依赖树”)]

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

这棵树有 nnn 层,n=logNn=logNn=logN

对于 000n−1n-1n1 之间的任意 kkk ,自顶向下的第 kkk 层有 2k2^k2k 个子数组,每个子数组的长度为 2n2k=2n−k\frac{2^n}{2^k}=2^{n-k}2k2n=2nk ,归并最多需要 2n−k2^{n-k}2nk 次比较。

因此每层的比较次数为 2k×2n−k=2n2^k \times 2^{n-k} = 2^n2k×2nk=2nnnn 层总共为 n2n=NlogNn2^n=NlogNn2n=NlogN

数学计算

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

C(0)=C(1)=0C(0)= C(1) = 0C(0)=C(1)=0

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

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

因为 ⌊N/2⌋=⌈N/2⌉=2n−1\lfloor{N/2}\rfloor=\lceil{N/2}\rceil=2^{n-1}N/2=N/2=2n1

所以对于上限

C(2n)=2C(2n−1)+2nC(2^n)=2C(2^{n-1})+2^nC(2n)=2C(2n1)+2n

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

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

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

对于下限

同理可得 C(N)=12NlogNC(N)=\frac{1}{2}NlogNC(N)=21NlogN

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

数组访问次数

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

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

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

最多比较 2N​2N​2N 次。

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

综述

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


缺点

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值