拆分与合并的艺术·分治思想:Java归并排序深度解析

拆分与合并的艺术·分治思想:Java归并排序深度解析

面试高频 | 稳定排序 | 含过程演示 | 新手友好


先说结论

归并排序是基于「分治思想」的排序算法,核心是先拆分、后合并 ,时间复杂度稳定为 O(nlogn),且是稳定排序(相等元素相对位置不变),这两点是它比快排更受某些场景青睐的原因。

操作过程(两步核心)

  1. 拆分:把无序数组从中间拆分成左右两部分,递归拆分直到每个子数组只剩 1 个元素(单个元素天然有序);
  2. 合并:把相邻的两个有序子数组,用双指针按大小顺序合并成一个新的有序数组,重复合并直到所有子数组合并为一个完整数组。

代价是需要额外 O(n) 的空间存放临时数组。


一、核心思想(用生活举例)

想象你是个老师,你有一摞试卷,你该怎么样使他的分数有序排列呢?

复制代码
第一步:拆分(先拆小,拆到没法拆)
把50份试卷先分成两堆(25份+25份)→ 再把每堆拆成12+13份 → 继续拆...
直到每一堆都只剩1份试卷(单份试卷不用排序,天然"有序")。

第二步:合并(再合大,合的时候排序)
把相邻的两小堆试卷合并:
- 比如先合"78分"和"85分"→ 排成[78,85]
- 再合"92分"和"88分"→ 排成[88,92]
- 接着合[78,85]和[88,92]→ 排成[78,85,88,92]

层层往上合并,每合并一次都按分数排好,最终所有试卷合成一大摞,就是有序的

二、过程演示

[8, 3, 9, 1, 7, 2, 5] 为例:

拆分阶段(递归往下):

复制代码
 0  1  2  3  4  5  6      代表每个数对应的下标,mid = left + (right - left) / 2 = 0 + (6 - 0) / 2 = 3
[8, 3, 9, 1, 7, 2, 5]     拆为:[left, mid]和[mid + 1, right].以此类推
          ↓ 对半拆
[8, 3, 9, 1]    [7, 2, 5]
     ↓                ↓
[8, 3]  [9, 1]   [7, 2]  [5]
  ↓        ↓       ↓
[8][3]  [9][1]  [7][2]

每个子数组只剩1个元素,停止拆分。

合并阶段(从底层往上):

复制代码
[8][3]  → 合并 → [3, 8]
[9][1]  → 合并 → [1, 9]
[3,8]+[1,9] → 合并 → [1, 3, 8, 9]

[7][2]  → 合并 → [2, 7]
[2,7]+[5] → 合并 → [2, 5, 7]

[1,3,8,9]+[2,5,7] → 合并 → [1, 2, 3, 5, 7, 8, 9] ✅

三、完整代码(含详细注释)

java 复制代码
public class MergeSort {

    public static void main(String[] args) {
        int[] arr = {8, 3, 9, 1, 7, 2, 5};
        mergeSort(arr);
        System.out.println(Arrays.toString(arr));
        // 输出:[1, 2, 3, 5, 7, 8, 9]
    }

    public static void mergeSort(int[] arr) {
        if (arr == null || arr.length < 2) return;  // 空数组或单元素,不用排
        int[] temp = new int[arr.length];            // 提前创建临时数组,避免每次合并都新建
        sort(arr, 0, arr.length - 1, temp);
    }

    private static void sort(int[] arr, int left, int right, int[] temp) {
        if (left >= right) return;  // 子数组只剩1个元素,停止拆分

        int mid = left + (right - left) / 2;  // 防溢出写法,比 (left+right)/2 更安全
        sort(arr, left, mid, temp);            // 递归拆左半部分
        sort(arr, mid + 1, right, temp);       // 递归拆右半部分
        merge(arr, left, mid, right, temp);    // 左右都拆完,合并
    }

    private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
        int i = left;     // 左子数组的指针,从 left 开始
        int j = mid + 1;  // 右子数组的指针,从 mid+1 开始
        int k = left;     // 临时数组的写入指针

        // 双指针比较,小的先放进临时数组
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++];  // 用 <= 而不是 <,保证稳定性(相等时左边先放)
            } else {
                temp[k++] = arr[j++];
            }
        }

        // 左边还有剩余,直接放入(右边已经用完)
        while (i <= mid) temp[k++] = arr[i++];
        // 右边还有剩余,直接放入(左边已经用完)
        while (j <= right) temp[k++] = arr[j++];

        // 把临时数组的结果写回原数组
        for (int p = left; p <= right; p++) {
            arr[p] = temp[p];
        }
    }
}

四、递归执行顺序(很多人卡在这里)

归并排序的递归顺序是:先把左边拆到底,再拆右边,拆完再合并,层层往上

用代码对应一下:

java 复制代码
sort(arr, left, mid, temp);         // ① 先一路拆左边,拆到单个元素
sort(arr, mid + 1, right, temp);    // ② 左边拆完,再拆右边
merge(arr, left, mid, right, temp); // ③ 左右都拆完,合并

不是"拆一层合一层",而是"左边全部拆完 → 右边全部拆完 → 才开始合并"。

奇数个数组怎么拆?

复制代码
[8, 3, 9, 1, 7]  长度5,mid = 0 + (4-0)/2 = 2
左:[8, 3, 9](下标0-2)
右:[1, 7](下标3-4)

mid 向下取整,左边可能比右边多一个,完全正常,递归会自动处理,不会出现 left > right


五、面试高频问题

① 为什么终止条件是 left >= right 而不是 left == right

正常递归只会触发 left == right,但写 >=防御性编程,防止代码写错时 left 意外超过 right 导致栈溢出。多一个等号,零性能损耗,规避极端 bug。

② 为什么 mid 用 left + (right - left) / 2

防止整数溢出。当 left 和 right 都很大时,left + right 可能超过 Integer.MAX_VALUE,结果变成负数。先算差值再取中,完全安全。

③ 为什么归并排序是稳定的?

合并时用的是 arr[i] <= arr[j],相等时优先放左边的元素,相等元素的相对顺序不变,所以稳定。

④ 归并 vs 快排,什么时候用哪个?

对比 归并排序 快速排序
稳定性 ✅ 稳定 ❌ 不稳定
最坏复杂度 O(n log n) O(n²)
额外空间 O(n) O(log n)
实际速度 稍慢 更快
适用场景 需要稳定排序 大多数通用场景

需要稳定排序(比如按多字段排序)用归并,其他情况快排更常用。


六、核心考点速查

项目 结论
时间复杂度(最好/平均/最坏) 均为 O(n log n),不会退化
空间复杂度 O(n)(临时数组)
稳定性 ✅ 稳定排序
递归终止条件 left >= right
mid 写法 left + (right - left) / 2
稳定性保证 合并时用 <=,相等优先放左边

作者:[识君啊]

相关推荐
Hello.Reader1 小时前
深入浅出 Adam 优化算法从直觉到公式
深度学习·算法
左左右右左右摇晃1 小时前
Java Object 类笔记
java·笔记
Trouvaille ~1 小时前
【贪心算法】专题(六):降维打击与错位重构的终极收官
c++·算法·leetcode·面试·贪心算法·重构·蓝桥杯
2301_800895101 小时前
dijkstra求最短路径--备考蓝桥杯版
算法
小箌1 小时前
JavaWeb & SpringBoot 总结
java·spring boot·后端
葡萄9891 小时前
蓝桥杯k倍区间(前缀和、余数统计)
数据结构·算法
智者知已应修善业2 小时前
【任何一个自然数m的立方均可写成m个连续奇数之和】2024-10-17
c语言·数据结构·c++·经验分享·笔记·算法
SimonKing2 小时前
震撼:Qoder 搭载Qwen3.2-Plus模型,5步实现完整的多数据源切换
java·后端·程序员
东离与糖宝2 小时前
Langflow-ai OpenRAG实战:Java+Spring Boot搭建企业级私有知识库(从0到1)
java·人工智能