🧩【牛客排序题详解】归并排序 & 快速排序深度解析(含 C 语言完整实现)
📌 前言
在排序算法的江湖中,归并排序 与快速排序 无疑是"分治思想"最经典的实践者。它们都凭借"拆分问题、解决子问题、合并结果"的核心逻辑,实现了高效排序效果,复杂度均达 O(n log n)(快排为平均情况)。
但看似同源的两者,在拆分方式、稳定性、空间占用、最坏表现等方面却截然不同。本文将结合牛客《排序》题(需要手写排序),从原理、对比、代码、适用场景四个维度彻底讲透两者差异,并给出可直接 AC 的 C 语言代码。
🧭 一、核心共识:分治思想的"三步走"
归并与快排,都遵循分治思想:
1️⃣ 分(Divide)
把数组拆成更小的子问题
2️⃣ 治(Conquer)
递归解决子问题
3️⃣ 合(Combine)
把子问题结果合并得到整体结果
然而------
归并排序与快速排序真正的不同,就藏在"怎么拆"和"怎么合"里。
🆚 二、两大排序的核心差异:从"分"到"合"的全面对比
我们从最关键的四个维度入手:
- 拆分逻辑
- 核心操作
- 性能特性
- 稳定性
并结合通俗类比 + C 代码片段,让你一眼看懂两者本质差异。
🔍 1. 拆分逻辑:机械拆 vs 智能分区
▶ 归并排序:按"位置"机械拆分(与元素大小无关)
归并排序的拆分,只依据下标位置,无论数组内容如何,都 按中点 一刀切:
c
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); // 关键:合并
}
}
类比:
切蛋糕时不看水果,只按中心线硬切,再重新排序拼回去。
▶ 快速排序:按"基准值"智能分区(拆分即排序)
快排的拆分完全依赖"基准值"(pivot):
c
int partition(int arr[], int left, int right) {
int pivot = arr[right];
int i = left - 1;
for (int j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i+1], &arr[right]);
return i+1;
}
拆分后立即得到:
- 左边全部 ≤ pivot
- 右边全部 ≥ pivot
类比:
先选一颗草莓做基准,把所有草莓放中间、葡萄放左边、芒果放右边,边切边分类。
🔍 2. 核心操作:合并 vs 分区
▶ 归并排序的灵魂:合并(需要额外空间)
核心代码片段:
c
void merge(int arr[], int left, int mid, int right) {
int lenL = mid - left + 1, lenR = right - mid;
int *tempL = malloc(lenL * sizeof(int));
int *tempR = malloc(lenR * sizeof(int));
for (int i=0; i<lenL; i++) tempL[i] = arr[left+i];
for (int j=0; j<lenR; j++) tempR[j] = arr[mid+1+j];
int i=0, j=0, k=left;
while (i<lenL && j<lenR) {
arr[k++] = (tempL[i] <= tempR[j]) ? tempL[i++] : tempR[j++];
}
while (i<lenL) arr[k++] = tempL[i++];
while (j<lenR) arr[k++] = tempR[j++];
free(tempL); free(tempR);
}
归并操作如同 两个有序队列合并
谁头小取谁,直到取完。
▶ 快速排序的灵魂:分区(在原地完成)
快排通过交换让数组自然被分成"小于基准"和"大于基准"的两部分:
- 不需要额外数组
- 分区完成后左右子数组天然局部有序
- 无需合并步骤
🔍 3. 性能特性:稳定 vs 不稳定、空间差异明显
| 特性 | 归并排序 | 快速排序 |
|---|---|---|
| 平均时间 | O(n log n) | O(n log n)(更快) |
| 最坏时间 | O(n log n)(绝对稳定) | O(n²)(基准选差) |
| 空间复杂度 | O(n)(临时数组) | O(log n)(递归栈) |
| 稳定性 | ✔ 稳定 | ✘ 不稳定 |
| 是否原地 | 否 | ✔ 是 |
🔍 4. 稳定性:归并是天生稳定排序
合并过程遵守:
相等元素优先取左边 ------ 保证稳定性。
快排由于交换操作,很容易把相等元素顺序打乱,因此天然不稳定。
🎯 三、适用场景:场景才是最优解
✔ 归并排序适合:
- 必须要 稳定排序 的场景
- 数据规模特别大(如外部排序)
- 数据可能接近有序(快排最坏情况触发概率增加)
如:
- 电商订单(价格相同时保留下单顺序)
- 学生成绩(按班级分,再按成绩排)
✔ 快速排序适合:
- 内存紧张,需原地排序
- 大多数工程场景(平均性能最好)
- 小规模数组(递归开销低)
如:
- C++
std::sort()(快排 + 堆排 + 插排 的混合) - Java
Arrays.sort(int[])(双枢轴快速排序)
💻 四、两大排序的 C 语言完整实现
(可以直接提交到牛客 AC)
🟦(一)归并排序 --- 稳定、有序合并
c
void merge(int *arr, int left, int mid, int right, int *temp) {
int i = left, j = mid + 1, 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];
}
void merge_sort(int *arr, int left, int right, int *temp) {
if (left >= right) return;
int mid = (left + right) / 2;
merge_sort(arr, left, mid, temp);
merge_sort(arr, mid + 1, right, temp);
merge(arr, left, mid, right, temp);
}
🟥(二)快速排序 --- 随机化 pivot,性能更稳
c
void swap(int *a, int *b){
int t = *a; *a = *b; *b = t;
}
int partition(int *arr, int left, int right){
int pivotIdx = left + rand() % (right - left + 1);
swap(&arr[pivotIdx], &arr[right]);
int pivot = arr[right];
int i = left - 1;
for(int j = left; j < right; j++){
if(arr[j] <= pivot){
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i+1], &arr[right]);
return i+1;
}
void quick_sort(int *arr, int left, int right){
if(left < right){
int p = partition(arr, left, right);
quick_sort(arr, left, p - 1);
quick_sort(arr, p + 1, right);
}
}
🟧(三)主函数:支持自由切换两种算法
c
int main() {
srand(time(NULL));
int n;
scanf("%d", &n);
int *arr = malloc(sizeof(int) * n);
for (int i = 0; i < n; i++)
scanf("%d", &arr[i]);
int mode;
scanf("%d", &mode); // 输入 1 用归并,2 用快排
if (mode == 1) {
int *temp = malloc(sizeof(int) * n);
merge_sort(arr, 0, n - 1, temp);
free(temp);
} else {
quick_sort(arr, 0, n - 1);
}
for (int i = 0; i < n; i++) {
if(i) printf(" ");
printf("%d", arr[i]);
}
free(arr);
return 0;
}
📝 五、总结:分治思想的两种哲学
归并排序:
分而治之 ------ 先机械拆分,再用合并解决问题
核心是"合"
快速排序:
治而分之 ------ 先通过分区解决问题,再继续拆分剩余子问题
核心是"分"
📌 归并稳定但占空间,快排原地但不稳定
📌 归并最坏 O(nlogn),快排可能退化到 O(n²)
📌 工程中一般首选快排,但稳定要求必须归并