mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
928 字
2 分钟
归并排序
2026-06-09 03:42:57

1. 算法策略#

归并排序是建立在归并操作上的一种稳定排序算法,也是分治法的典型应用。

它的核心流程可以概括为三个动作:

  • :将序列一步步分解成小的子序列进行分段排序。
  • :将分段有序的子序列合并在一起,使得整个序列变得有序。

下图展示了分治策略的整体步骤:

归并排序分治步骤

1.1 先拆再并#

归并排序的思路可以概括为:

  1. 不断把数组从中间一分为二。
  2. 直到每个子数组只剩一个元素。
  3. 再把两个有序子数组合并成一个更大的有序数组。

例如数组 6 5 3 1 8 7 2 4,拆分过程大致如下:

6 5 3 1 8 7 2 4
├── 6 5 3 1
│ ├── 6 5
│ │ ├── 6
│ │ └── 5
│ └── 3 1
│ ├── 3
│ └── 1
└── 8 7 2 4
├── 8 7
│ ├── 8
│ └── 7
└── 2 4
├── 2
└── 4

等到最底层只剩单个元素时,每一段天然就是有序的,接下来只需要逐层合并即可。

分裂点一般写成:

int mid = left + (right - left) / 2;

这样写比直接用 (left + right) / 2 更安全,避免区间很大时出现整数溢出。


2. 合并过程#

下图以最后一步中合并分段有序的子序列为例:

归并排序合并过程

双指针落位与比较#

设立两个指针 leftright,分别指向左右两个待合并的已有序子数组 nums[low, mid]nums[mid + 1, high]

  • 在合并左右两个子数组时,若 nums[right] < nums[left],则将当前较小的 nums[right] 放入排序结果中。
  • 依次类推,直到两个子数组的所有元素都被合并,即可得到最终排好序的数组。

可以拿下面这组数据理解:

  • 左子数组:[2, 4, 6]
  • 右子数组:[1, 3, 5]

合并时先比较 21,取较小的 1;再比较 23,取 2;接着是 43,取 3,直到一边元素全部取完,再把另一边剩余元素直接追加进去。

为什么要用临时数组#

合并时不能直接在原数组上覆盖,否则后面的元素很容易被提前改掉。

因此通常会先用一个临时数组 tmp 保存合并结果,最后再统一写回原数组。

在 Java 里,更推荐在排序开始前只创建一次辅助数组,然后在递归过程中重复使用它,这样可以减少频繁创建临时对象带来的额外开销。

稳定性#

归并排序是稳定排序。

当左右两边出现相同元素时,先取左边的元素,就能保证相同元素的相对顺序不被打乱。


3. Java 实现#

下面给出 Java 语言的实现,关键步骤都加了注释。这里采用“只创建一次辅助数组”的写法:

import java.util.Arrays;
public class MergeSort {
public static void mergeSort(int[] arr) {
if (arr == null || arr.length <= 1) {
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) {
// 区间长度为 1 时自然有序,递归结束
if (left >= right) {
return;
}
// 分:把区间拆成左右两半
int mid = left + (right - left) / 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;
int j = mid + 1;
int t = 0;
// 比较左右两边的当前元素,把较小值依次放入辅助数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
// 左半边还有剩余,直接追加
while (i <= mid) {
temp[t++] = arr[i++];
}
// 右半边还有剩余,直接追加
while (j <= right) {
temp[t++] = arr[j++];
}
// 将合并结果写回原数组对应区间
t = 0;
int k = left;
while (k <= right) {
arr[k++] = temp[t++];
}
}
public static void main(String[] args) {
int[] arr = {38, 27, 43, 3, 9, 82, 10};
System.out.println("排序前: " + Arrays.toString(arr));
mergeSort(arr);
System.out.println("排序后: " + Arrays.toString(arr));
}
}
NOTE

在合并步骤中,如果遇到相等的元素,优先保留左侧子数组中的元素,这也保证了归并排序的稳定性。

TIP

如果想优化空间开销,可以把 tmp 复用成一个全局辅助数组,避免每次递归都重新创建临时数组。


5. 复杂度分析#

  • 时间复杂度O(nlogn)O(n \log n)。无论最好、最坏还是平均情况,归并排序都要经历“分解 + 合并”两类操作,所以它的时间复杂度都很稳定。
  • 空间复杂度O(n)O(n),主要来自辅助数组。递归调用本身还会带来 O(logn)O(\log n) 的栈空间,但通常不作为主导项。
  • 稳定性:稳定。合并时若相等元素优先取左边,就能保持相同元素的相对顺序。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

部分信息可能已经过时

目录