归并排序

一、定义

归并排序分为两种:

  1. 自顶向下”的归并排序。
    该类归并排序是算法设计中“分治”思想的典型应用。
    其实就是将数组分为两部分,然后递归地将两部分分别排序,将排序后的两个数组进行归并。

  2. 自底向上”的归并排序。
    该类归并排序是先两两归并size=1的小数组,形成size=2的各对数组;然后再两两归并size=2数组,直到归并size=N的大数组。
    “自底向上”不需要用到递归,适合以链表形式组织的数据。

二、实现

2.1 自顶向下的归并排序

2.1.1 基本思想

将数组分为2部分,递归对每部分进行排序后,再将2部分进行归并。


2-1-1 自顶向下的示意图
2.1.2 运行轨迹

运行轨迹示意图:


2-1-2 运行轨迹图

2-1-3 递归调用图
2.1.3 源码
public class Merge {
    public static void sort(Comparable[] a) {
        // 辅助数组,用于归并(此处未用原地归并)
        Comparable[] aux = new Comparable[a.length];
        sort(a, aux, 0, a.length - 1);
        assert isSorted(a);
    }
 
    // mergesort a[lo..hi] using auxiliary array aux[lo..hi]
    private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
        if (hi <= lo)
            return;
        int mid = lo + (hi - lo) / 2;
        // 对a[lo,mid]进行归并排序
        sort(a, aux, lo, mid);
        // 对a[mid+1,hi]进行归并排序
        sort(a, aux, mid + 1, hi);
        // 对已经有序的a[lo .. mid] 和 a[mid+1 ..hi] 进行归并
        merge(a, aux, lo, mid, hi);
    }
}

2.2 自底向上的归并排序

2.2.1 基本思想

“自底向上”的归并排序不需要用到递归,而是采用两两归并的方式,将小数组逐渐归并成大数组,具体步骤如下:

  1. 首先对size=1的数组进行两两归并,得到size=2的数组;
  2. 对size=2的数组进行两两归并,得到size=4的数组;
  3. 依次类推,直到对size=N的数组归并完成。
2.2.2 运行轨迹

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


2-2-2 运行轨迹图
2.2.3 源码
public static void sort(Comparable[] a) {
    int N = a.length;
    Comparable[] aux = new Comparable[N]; // 辅助数组,用于归并(此处未用原地归并)

    for (int sz = 1; sz < N; sz *= 2) { // sz表示待归并的子数组大小
        for (int lo = 0; lo < N - sz; lo += sz + sz) {
            int mid = lo + sz - 1;
            int hi = Math.min(lo + sz + sz - 1, N - 1);
            merge(a, aux, lo, mid, hi); // 对a[lo,mid]和a[mid+1,hi]进行归并
        }
    }
    assert isSorted(a);
}

三、原地归并

归并排序的难点在于如何将两个有序数组进行原地归并。
通常情况下,如果不进行原地归并,需要一个大小为N的辅助数组。

3.1 原地归并的思想

原地归并排序运用了“内存反转”的思想(也称为手摇算法),具体可以参见《编程珠玑》。

原地归并关键在于merge这个归并函数,步骤如下:

  1. 初始时,两段分别递增的子数组arr[begin…mid]arr[mid+1…end]i=beginj=mid+1
    原则:始终保证指针i之前的元素为两个序列中最小的那些元素,即i之前为已经归并好的元素。
  2. i往后移动,找到第一个arr[i]>arr[j]的索引


  3. j往后移动,再找第一个arr[j]>arr[i]的索引。

    此时arr[begin…i-1]序列是整个数组的最小序列;arr[mid+1…j-1]序列是整个数组的次小序列。
  4. arr[i…mid]的内存块和arr[mid+1…j-1]的内存块对调,这样较小的部分就调到前面去了。
    对调方法(手摇算法):先reverse(arr[i…mid]),再reverse(arr[mid+1…j-1]),最后reverse(arr[i…j-1])

  5. 之后,i=i + j - mid - 2mid=j-1a[i,mid]a[j,end]又是两个递增的子数组,继续迭代即可。

3.2 源码

public class Merge {
    /**
     * 原地归并
     * <P>
     * arr[begin,mid],arr[mid+1,end]为分别递增有序的子数组
     * <P>
     */
    public static void merge(int[] arr, int begin, int mid, int end) {
        int i = begin, j = mid + 1;
 
        while (i < j && j <= end) {
            // 找到第一个arr[i]>=arr[j]的索引
            while (i <= mid && arr[i] < arr[j]) {
                i++;
            }
 
            // 找到第一个arr[j]>=arr[i]的索引
            while (j <= end && arr[j] < arr[i]) {
                j++;
            }
 
            if (i == j)
                break;
 
            // 将arr[i…mid]的内存块和arr[mid+1…j-1]的内存块对调
            swapBlock(arr, i, mid, mid + 1, j - 1);
 
            i = i + j - mid - 2;
            mid = j - 1;
        }
    }
 
    private static void swapBlock(int[] arr, int i, int mid, int j, int k) {
        reverse(arr, i, mid);   // 逆序arr[i…mid]
        reverse(arr, j, k);     // 逆序arr[j…k]
        reverse(arr, i, k);     // 逆序arr[i…k]
    }
 
    private static void reverse(int[] arr, int i, int k) {
        // TODO Auto-generated method stub
    }
}

四、性能分析

  • 时间复杂度
    最坏情况下,为O(NlogN)
  • 空间复杂度
    当使用辅助数组进行归并时为O(N),原地归并不需要额外空间
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容