在掌握了快速排序之后,学习同样基于分治法思想,但思路迥异且非常稳定的归并排序。
| 特性 | 归并排序 | 快速排序 |
|---|---|---|
| 核心思想 | 先分后治:递归地将数组对半拆分,再将有序子数组合并。 | 分而治之:选取基准进行分区,递归排序左右区间。 |
| 时间复杂度 | 最好、最坏、平均情况均为 O(n log n) | 平均 O(n log n),最坏 O(n²) |
| 空间复杂度 | O(n) ,需要额外的空间来合并数组 | 平均 O(log n) |
| 稳定性 | 稳定,相等元素的相对位置在排序后不变 | 不稳定,分区过程可能打乱相等元素的顺序 |
| 效率影响 | 性能稳定,不受输入数据特性影响 | 受输入数据影响,最坏情况可能退化为O(n²) |
🔄 归并排序的核心思想
归并排序严格遵循分治法,其过程可以清晰地分为三个步骤 :
- 分解:将当前需要排序的数组,从中间位置递归地拆分成两个子数组,直到每个子数组只包含一个元素(一个元素本身就是有序的)。
- 解决:递归地对这两个子数组进行归并排序。
- 合并:这是归并排序的核心步骤。将两个已经排好序的子数组合并成一个新的有序数组。
🧩 算法步骤与示例
我们通过数组 [8, 3, 5, 4, 7, 1, 9, 2]来理解归并排序的过程 :
1. 分解阶段:不断将数组对半拆分,直到每个子数组只剩一个元素。
初始数组: [8, 3, 5, 4, 7, 1, 9, 2]
第一次拆分: [8, 3, 5, 4] 和 [7, 1, 9, 2]
第二次拆分: [8, 3], [5, 4], [7, 1], [9, 2]
最终拆分: [8], [3], [5], [4], [7], [1], [9], [2] (每个子数组都已自然有序)
2. 合并阶段:从最小的子数组开始,两两合并成有序的新数组。
- 合并
[8]和[3]得到[3, 8] - 合并
[5]和[4]得到[4, 5] - 合并
[3, 8]和[4, 5]得到[3, 4, 5, 8] - 同理,合并右侧数组得到
[1, 2, 7, 9] - 最后,合并
[3, 4, 5, 8]和[1, 2, 7, 9]得到最终的有序数组[1, 2, 3, 4, 5, 7, 8, 9]
合并两个有序子数组的过程是归并排序的关键。它通过双指针遍历两个子数组,比较指针所指元素,将较小的元素放入临时数组,直到某个子数组遍历完,再将另一个子数组的剩余部分直接追加到临时数组末尾 。
💻 Java代码实现
以下是归并排序的Java实现,包含了递归排序和合并两个有序数组的核心方法。
public class MergeSort {
// 归并排序的入口方法
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
int[] temp = new int[arr.length]; // 创建临时数组,避免在递归中频繁创建
mergeSort(arr, 0, arr.length - 1, temp);
}
// 递归排序的主方法
private static void mergeSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = left + (right - left) / 2; // 计算中点,防止整数溢出
// 递归排序左半部分
mergeSort(arr, left, mid, temp);
// 递归排序右半部分
mergeSort(arr, mid + 1, right, temp);
// 合并两个有序部分
merge(arr, left, mid, right, temp);
}
}
// 合并两个有序子数组 arr[left...mid] 和 arr[mid+1...right]
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 左子数组的起始索引
int j = mid + 1; // 右子数组的起始索引
int t = 0; // 临时数组的当前索引
// 比较两个子数组的元素,将较小的放入temp
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t++] = arr[i++];
} else {
temp[t++] = arr[j++];
}
}
// 将左子数组剩余元素拷贝到temp
while (i <= mid) {
temp[t++] = arr[i++];
}
// 将右子数组剩余元素拷贝到temp
while (j <= right) {
temp[t++] = arr[j++];
}
// 将temp数组中合并好的数据拷贝回原数组arr
t = 0;
while (left <= right) {
arr[left++] = temp[t++];
}
}
public static void main(String[] args) {
int[] arr = {8, 3, 5, 4, 7, 1, 9, 2};
System.out.println("排序前: " + java.util.Arrays.toString(arr));
mergeSort(arr);
System.out.println("排序后: " + java.util.Arrays.toString(arr));
}
}
⚖️ 归并排序的优缺点与应用场景
优点:
- 稳定的时间复杂度:在任何情况下都能保持O(n log n)的高效率,性能可预测。
- 稳定性:是稳定的排序算法,适用于需要保持相等元素初始顺序的场景。
- 适合大数据量:特别适合处理无法一次性装入内存的大规模数据(外部排序)。
缺点:
- 空间复杂度高:需要O(n)的额外空间,在内存受限的环境下可能成为问题。
- 递归开销:递归实现可能导致栈溢出,尽管可以采用迭代方式实现以避免。
应用场景:
- 当需要稳定的排序且对空间要求不苛刻时。
- 用于链表排序时,归并排序只需要修改指针,空间复杂度可降为O(1)。
- 外部排序,例如对大文件进行排序。
希望这份详细的归并排序讲解能帮助你更好地理解这个经典算法!理解了归并排序和快速排序的异同,你对分治法的应用会有更深刻的认识。