【牛客排序题详解】归并排序 & 快速排序深度解析(含 C 语言完整实现)

🧩【牛客排序题详解】归并排序 & 快速排序深度解析(含 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²)

📌 工程中一般首选快排,但稳定要求必须归并

相关推荐
minji...2 小时前
算法---模拟/高精度/枚举
数据结构·c++·算法·高精度·模拟·枚举
Vic101012 小时前
Java 序列化与反序列化:深入解析与实践
java·开发语言
Sirius Wu2 小时前
开源训练框架:MS-SWIFT详解
开发语言·人工智能·语言模型·开源·aigc·swift
后端小张2 小时前
【JAVA 进阶】Spring Cloud 微服务全栈实践:从认知到落地
java·开发语言·spring boot·spring·spring cloud·微服务·原理
从零开始学习人工智能2 小时前
USDT区块链转账 vs SWIFT跨境转账:技术逻辑与场景博弈的深度拆解
开发语言·ssh·swift
星释2 小时前
Rust 练习册 31:啤酒歌与字符串格式化艺术
开发语言·网络·rust
百***58843 小时前
MacOS升级ruby版本
开发语言·macos·ruby
执笔论英雄3 小时前
【大模型训练】forward_backward_func返回多个micro batch 损失
开发语言·算法·batch
序属秋秋秋4 小时前
《Linux系统编程之进程基础》【进程优先级】
linux·运维·c语言·c++·笔记·进程·优先级