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

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

相关推荐
ywwwwwwv1 小时前
力扣139
算法·leetcode·职场和发展
金牌归来发现妻女流落街头1 小时前
【阻塞队列的等待唤醒机制】
java·开发语言·阻塞队列
毕设源码-朱学姐1 小时前
【开题答辩全过程】以 基于Java技术的羽毛球积分赛管理系统的设计与实现 为例,包含答辩的问题和答案
java·开发语言
2501_941982051 小时前
Go 进阶:发送文件/图片消息的流程与实现
开发语言·后端·golang
smj2302_796826521 小时前
解决leetcode第3777题使子字符串变交替的最少删除次数
python·算法·leetcode
Tisfy1 小时前
LeetCode 2110.股票平滑下跌阶段的数目:数学(一次遍历)
数学·算法·leetcode·题解
1024小神1 小时前
swift中 列表、字典、集合、元祖 常用的方法
数据结构·算法·swift
ULTRA??1 小时前
Informed RRT*实现椭圆启发式采样
c++·算法
Swizard1 小时前
告别样本不平衡噩梦:Focal Loss 让你的模型学会“划重点”
算法·ai·训练
star learning white2 小时前
xm C语言12
服务器·c语言·前端