Merge Sort – 归并排序详解与优化

归并排序

思想

自顶向下划分

序列 [l, r] 被逐步二分为更小的序列 [l, m] 、[m+1, r]

自底向上归并

划分的最小有序序列为元素本身,子序列之间两两归并,归并成为新的有序序列之后再次两两合并,直到调和成为完整的有序序列

其中的合并操作为 合并两个有序序列:从两个有序序列逐一取值,按大小次序依次插入新的序列,可合并为一个新的有序序列。新序列若取原序列本身,可能会覆盖序列原值导致错误,需要一个 辅助序列aux 来保存合并结果

性能分析

时间复杂度 O(nlogn)
空间复杂度 O(n)

实现与优化

递归实现数组排序

子递归结果存入aux,父调用时基于aux的副本进行合并,即在合并前复制

class Solution {
    public int[] sortArray(int[] nums) {
        int[] aux = new int[nums.length];
        mergeSort(nums, 0, nums.length - 1, aux);
        return aux;
    }
 
    public void mergeSort(int[] nums, int i, int j, int[] aux) {
        // 递归边界:不合法返回
        if (i > j) return;
 
        // 递归边界:最小子序列是元素本身,合并入新序列aux
        if (i == j) {
            aux[i] = nums[i];
            return;
        }
 
        // 自顶向下划分子序列,递归返回时两部分的aux已经分别有序
        int m = i + ((j - i) >> 1);
        mergeSort(nums, i, m, aux);
        mergeSort(nums, m + 1, j, aux);
 
        // 后续归并基于分别有序的子序列,原地归并会覆盖原值,将部分有序结果复制回nums
        for (int k = i; k <= j; k++) {
            nums[k] = aux[k];
        }
 
        // 基于nums中的两有序序列[l, m] 、[m+1, r]合并到aux中,合并时考虑指针到头情况
        int s = i, f = m + 1, id = i;
        while (s <= m || f <= j) {
            if (s > m) aux[id++] = nums[f++];
            else if (f > j) aux[id++] = nums[s++];
            else if (nums[s] <= nums[f]) aux[id++] = nums[s++];
            else aux[id++] = nums[f++];
        }
    }
}

优化递归数组排序

优化点1:避免不必要的合并,两有序序列本身已经组成新的有序序列时,省去合并
优化点2:合并递归边界减少复制,单个元素自成序不再处理,意味着保存两个有序序列的是nums,需要始终将合并结果存入nums。合并操作基于nums输出到辅助数组aux后需要复制回到nums

class Solution {
    public int[] sortArray(int[] nums) {
        int[] aux = new int[nums.length];
        mergeSort(nums, 0, nums.length - 1, aux);
        return nums;
    }
 
    public void mergeSort(int[] nums, int i, int j, int[] aux) {
        // 递归边界,i > j不合法,i = j时nums[i]自成序
        if(i >= j) return;
 
        // 子递归划分和归并,执行完后左右部分各自有序
        int m = i + ((j - i) >> 1);
        mergeSort(nums, i, m, aux);
        mergeSort(nums, m + 1, j, aux);
 
        // 左右部分在本轮合并前已有序,剪枝
        if(nums[m] < nums[m+1]) return;
 
        // 基于有序的nums进行归并
        int s = i, f = m + 1, id = i;
        while(s <= m || f <= j) {
            if(s > m) aux[id++] = nums[f++];
            else if(f > j) aux[id++] = nums[s++];
            else if(nums[s] > nums[f]) aux[id++] = nums[f++];
            else aux[id++] = nums[s++];
        }
 
        // 恢复nums的有序性,供下轮aux合并使用
        for(int k = i; k <= j; k++) {
            nums[k] = aux[k];
        }
    }
}

递归实现链表排序

自顶向下划分时需要找到链表中点并切分为两个链表,清空第一个表尾next域
在合并阶段可使用 虚拟头节点 ,避免复杂的边界条件判断
合并时两个链表已经有序,其中一个链表到达尽头,直接将另一条链表接到表尾即可

class RecursiveMergeSort {
    public ListNode sortList(ListNode head) {
        return mergeSort(head);
    }
 
    public ListNode mergeSort(ListNode head) {
        // 递归边界,空节点或单个节点自有序
        if (head == null || head.next == null) {
            return head;
        }
 
        // 得到链表中点后切开两个链表
        ListNode fast = head, mid = head, clr = null;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            clr = mid;
            mid = mid.next;
        }
        clr.next = null;
 
        // 自顶向下划分得到两个有序序列
        head = mergeSort(head);
        mid = mergeSort(mid);
 
        // 自底向上归并
        ListNode dum = new ListNode(Integer.MIN_VALUE, null);
        ListNode cur = dum;
        while (head != null && mid != null) {
            if (head.val < mid.val) {
                cur.next = head;
                head = head.next;
            } else {
                cur.next = mid;
                mid = mid.next;
            }
            cur = cur.next;
        }
 
        // 处理一个指针到头的情况,此时两链表均有序
        if (head == null) cur.next = mid;
        if (mid == null) cur.next = head;
 
        // 返回合并后链表的新头节点
        return dum.next;
    }
}

迭代

偶数对可以两两合并,四四合并,八八合并

偶数对合并过程示例
01 23 45 67
0123 4567
01234567

奇数对元素的合并处理过程需要特殊处理

奇数对合并过程示例
01 23 45 6
0123 456
0123456

奇数对合并过程示例
01 23 4
0123 4
01234

奇数对合并过程示例
01 23 45
0123 45
012345
class MergeSortIteration {
    public int[] sortArray(int[] nums) {
        int[] aux = new int[nums.length];
 
        // sz扩到何时停止?由于奇数的特殊性不能使sz在N/2时停止,应扩到和数组同长度时,同长度为最后一轮已成序无需归并
        for (int sz = 1; sz < nums.length; sz <<= 1) {
 
            // lo移到何时停止?最后部分小于sz时lo + sz < nums.length
            for (int lo = 0; lo + sz < nums.length; lo += (sz << 1)) {
                int s = lo, st = lo + sz;
 
                // 不足sz的后半部分末尾设定为num.length
                int f = lo + sz, ft = Math.min(lo + (sz << 1), nums.length);
                int id = lo;
 
                // 合并 [s, st) [f, ft)
                while (s < st && f < ft) {
                    if (nums[s] < nums[f]) aux[id++] = nums[s++];
                    else aux[id++] = nums[f++];
                }
 
                // 以nums为基础归并到aux中
                if (s >= st) while (f < ft) aux[id++] = nums[f++];
                if (f >= ft) while (s < st) aux[id++] = nums[s++];
 
                // 复制回nums
                for (int k = lo; k < ft; k++)
                    nums[k] = aux[k];
            }
        }
        return nums;
    }
}

参考资料

Sedgewick R, Wayne K. Algorithms, Fourth Edition[M]. 4. 人民邮电出版社, 2012.

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

©2018-2024 Howell版权所有 备案号:冀ICP备19000576号