基础排序算法(七)归并排序
一 归并排序
归并排序是一种基于分治法的高效、稳定的排序算法。其核心思想是将两个或多个已排序的序列合并成一个新的有序序列。
1.1 算法特性
归并排序特性总结
| 特性 | 说明 |
|---|---|
| 核心思想 | 分治法:将数组递归地分成两半,分别排序后,再将两个有序子数组合并成一个有序数组 |
| 时间复杂度 | 最好、最坏、平均情况下均为 O(n log n) |
| 空间复杂度 | O(n)。需要与原始数组等长的额外空间用于合并操作 |
| 稳定性 | 稳定。在合并过程中,当两个元素相等时,优先取前一子数组的元素,保持相对顺序 |
| 主要优势 | 时间复杂度稳定;适用于大规模数据;稳定排序;易于并行化 |
| 主要劣势 | 需要额外空间,不是原地排序;对于小规模数据,常数因子可能使其不如简单排序算法高效 |
1.2 算法原理
归并排序是一种基于分治法的递归算法。其核心流程是先将大问题分解为小问题,解决小问题后,再将结果合并起来。具体到排序中,过程如下:
- 分解:将当前待排序的数组从中间位置分成两个子数组(递归进行,直到每个子数组只包含一个元素,单个元素自然有序)。
- 解决:递归地对两个子数组进行归并排序。
- 合并:这是算法的关键步骤。将两个已经有序的子数组合并成一个新的有序数组。合并时,使用双指针法,比较两个子数组当前指针所指的元素,将较小的元素放入临时数组,然后移动指针,直到某个子数组遍历完,再将另一个子数组的剩余部分直接复制到临时数组末尾。最后将临时数组的内容复制回原数组。
1.3 复杂度分析
- 时间复杂度 O(n log n) 的由来:归并排序将数组二分的过程形成了一棵递归树,树的高度为 log₂n。在每一层,都需要对当前所有元素进行一次合并操作,合并操作的时间复杂度是 O(n)。因此,总的时间复杂度为 O(n) × O(log n) = O(n log n)。重要的是,这个效率在最好、最坏和平均情况下都是一致的,不会因为输入数据的原始顺序而退化。
- 空间复杂度 O(n) 的由来:合并两个有序子数组时,无法在原地完成,需要借助一个与原始数组等大的临时数组来存储中间结果。这是归并排序的主要缺点。
1.4 使用场景
归并排序在以下场景中表现出色:
- 大规模数据排序:由于其稳定的 O(n log n) 时间复杂度,在处理海量数据时性能可靠。
- 需要稳定性的排序:在如先按成绩排序再按姓名排序这类场景中,保持相等元素的初始顺序很重要,归并排序是稳定算法。
- 外部排序:当数据量太大无法全部装入内存时,归并排序是外部排序(如多路归并)的基础算法,因为它可以高效地合并多个已排序的大文件。
- 链表排序:归并排序非常适用于链表结构,因为链表节点在合并时可以通过改变指针实现,不需要额外空间。
1.5 代码实现
1.5.1 c语言代码实现
#include <stdio.h>
#include <stdlib.h>
// 合并两个有序子数组 arr[left...mid] 和 arr[mid+1...right]
void Merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1; // 左子数组长度
int n2 = right - mid; // 右子数组长度
// 创建临时数组存放左右两部分的数
int *L = (int *)malloc(n1 * sizeof(int));
int *R = (int *)malloc(n2 * sizeof(int));
// 将数据拷贝到临时数组 L[] 和 R[] 中
for (int i = 0; i < n1; i++)
L[i] = arr[left + i];
for (int j = 0; j < n2; j++)
R[j] = arr[mid + 1 + j];
// 合并临时数组回原数组 arr[left..right]
int i = 0; // 初始化左子数组的索引
int j = 0; // 初始化右子数组的索引
int k = left; // 初始化合并子数组的索引
while (i < n1 && j < n2) {
if (L[i] <= R[j]) { // 使用 <= 保证稳定性
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// 将 L[] 剩余的元素复制回来(如果有)
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
// 将 R[] 剩余的元素复制回来(如果有)
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
// 释放临时数组的内存
free(L);
free(R);
}
// 归并排序的主递归函数
void MergeSort(int arr[], int left, int right) {
if (left < right) {
// 计算中间点,避免溢出
int mid = left + (right - left) / 2;
// 递归排序左半部分和右半部分
MergeSort(arr, left, mid);
MergeSort(arr, mid + 1, right);
// 合并排序好的两部分
Merge(arr, left, mid, right);
}
}
// 打印数组函数
void PrintArray(int arr[], int size) {
for (int i = 0; i < size; i++)
printf("%d ", arr[i]);
printf("\n");
}
// 主函数测试
int main() {
int arr[] = {12, 11, 13, 5, 6, 7};
int arr_size = sizeof(arr) / sizeof(arr[0]);
printf("排序前的数组: \n");
PrintArray(arr, arr_size);
MergeSort(arr, 0, arr_size - 1);
printf("排序后的数组: \n");
PrintArray(arr, arr_size);
return 0;
}
1.5.2 代码关键点解释
- Merge函数:这是核心。它负责将两个有序的子数组合并成一个有序数组。通过双指针遍历,比较元素大小,并按序放入原数组。if (L[i] <= R[j])中的 <=确保了排序的稳定性。
- MergeSort函数:通过递归不断将数组对半分割,直到子数组长度为1(自然有序),然后调用 Merge函数从最小的子数组开始向上合并。
1.6 常用算法比较
排序算法比较
| 算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 | 主要特点与适用场景 |
|---|---|---|---|---|---|
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 | 性能稳定,可靠,适合大数据量、要求稳定性或外部排序的场景 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 | 平均性能极佳,是许多标准库的实现选择,但对初始数据敏感(如已有序序列会导致性能退化) |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 | 最坏情况也能保证O(n log n),且是原地排序,但常数因子较大,缓存不友好 |
| 插入排序 | O(n²) | O(n²) | O(1) | 稳定 | 对小规模或基本有序数据效率很高;简单 |
从对比中可以看出,归并排序的主要优势在于其稳定的时间复杂度和稳定性。然而,其需要额外O(n)空间的特性是其主要劣势。在实际应用中,快速排序因其平均情况下的高效和原地排序的特性,通常是内排序的首选,但如果你无法承受快速排序的最坏情况,或者必须要求排序稳定,那么归并排序就是更好的选择。
1.7 总结
总的来说,归并排序是一种基于分治策略的高效、稳定的排序算法。其最坏情况下仍能保持O(n log n)的时间复杂度是其最大优点,使其在处理大规模数据或对性能有稳定要求的场景中非常可靠。缺点是需要O(n)的额外空间。理解归并排序有助于深入掌握分治思想,并为学习更复杂的算法(如外部排序)打下基础。
posted on 2025-11-03 14:25 weiwei2021 阅读(3) 评论(0) 收藏 举报
浙公网安备 33010602011771号