当分糖果遇到算法设计
小时候分糖果的经历(记得吗?),总是先把整包糖分成两半,再继续对半分——这和今天要讲的归并排序核心思想不谋而合!这个看似简单的分治法(Divide and Conquer),在算法世界里可是扛把子级别的存在!
拆解归并排序四大核心步骤
第一步:暴力拆分(重要!)
把数组想象成一根法棍面包,从中间切开两半。比如数组[5,3,9,1,7,2],第一次拆分就变成:
左半部 → [5,3,9]
右半部 → [1,7,2]
(关键细节)不断递归拆分,直到每个子数组只剩1个元素。这时候每个单元素数组自然就是有序的!
第二步:有序合并(核心操作)
当拆到最小单元后,开始像拼积木一样合并。比如合并[3,5]和[1,9]:
- 创建临时数组
- 比较两数组首位 → 1 < 3 → 放入1
- 继续比较 → 3 < 7 → 放入3
- 重复直到所有元素归位
最终得到[1,3,5,7,9]
第三步:双指针魔法
合并过程中需要两个指针:
int i = left; // 左数组起点
int j = mid+1; // 右数组起点
通过这两个指针的移动,实现**O(n)**时间复杂度的合并操作!
第四步:临时数组的妙用
重点来了(敲黑板)!合并时必须使用临时数组存储中间结果,合并完成后再把数据拷贝回原数组。这个操作虽然增加了空间复杂度,但却是保证稳定性的关键!
手把手C语言实现
#include <stdio.h>
#include <stdlib.h>
void merge(int arr[], int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
// 创建临时数组
int L[n1], R[n2];
// 拷贝数据
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];
// 合并操作
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// 处理剩余元素
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
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);
}
}
性能分析(重点!)
指标 | 数值 | 说明 |
---|---|---|
时间复杂度 | O(n log n) | 最坏/平均情况都稳定(划重点!) |
空间复杂度 | O(n) | 需要额外存储空间 |
稳定性 | 稳定 | 相同元素顺序不变 |
什么时候该用它?
- 处理大数据量文件排序(比如GB级日志文件)
- 需要稳定排序的场景(比如先按姓名排序再按年龄)
- 内存足够大的情况下(毕竟要额外空间)
- 链表排序的最佳选择(空间复杂度可降为O(1))
真实开发中的坑
去年处理用户行为日志时,用快排遇到最坏情况直接O(n²)超时(血泪教训!)。换成归并排序后,50GB日志文件20分钟搞定,真香!
结语
归并排序就像算法界的太极——看似缓慢拆分,实则暗藏合并杀招。下次面试被问排序算法,把这个分治思路甩出来,绝对让面试官眼前一亮!