排序算法完全指南(八):归并排序深度详解

引言

前面我们学习了快速排序和堆排序两种 O(n log n) 的排序算法。今天要讲的归并排序 ,同样是 O(n log n) 级别,但它有一个独特的优势------稳定

归并排序是分治思想最经典的体现:把一个大问题拆成两个小问题,分别解决后再合并。它的时间复杂度始终是 O(n log n),没有快排的最坏退化问题,因此在需要稳定排序的场景中(如对象按多个条件排序)是不可替代的选择。

第一部分:算法思想

一、合并两个有序数组

归并排序的核心是 Merge 操作:把两个已经有序的数组合并成一个有序数组。

合并有三种情况

情况 处理
两边都还有元素 比较 arr[i]arr[j],取较小的
左边空了(i > mid 直接取右边的
右边空了(j > right 直接取左边的

二、合并代码

复制代码
void Merge(int arr[], int left, int mid, int right) {
    // 申请临时数组
    int* brr = (int*)malloc(sizeof(int) * (right - left + 1));
    if (brr == NULL) return;

    int i = left;       // 左半指针
    int j = mid + 1;    // 右半指针
    int k = 0;          // 临时数组指针

    while (i <= mid || j <= right) {
        // 情况1:左边空了 → 直接取右边
        if (i > mid) {
            brr[k++] = arr[j++];
        }
        // 情况2:右边空了 → 直接取左边
        else if (j > right) {
            brr[k++] = arr[i++];
        }
        // 情况3:两边都有 → 比较取小的(等号保证稳定性)
        else if (arr[i] <= arr[j]) {
            brr[k++] = arr[i++];
        }
        else {
            brr[k++] = arr[j++];
        }
    }

    // 把临时数组拷贝回原数组
    for (int t = left; t <= right; t++) {
        arr[t] = brr[t - left];
    }

    free(brr);
}

注意 :比较条件是 arr[i] <= arr[j] 而不是 <等号保证左边等于右边时优先取左边 ,这是归并排序保持稳定的关键。


第二部分:完整实现

一、递归拆分

复制代码
void Dvide(int arr[], int left, int right) {
    if (left < right) {
        int mid = (left + right) / 2;
        
        Dvide(arr, left, mid);         // 左半递归
        Dvide(arr, mid + 1, right);    // 右半递归
        
        Merge(arr, left, mid, right);  // 合并左右
    }
}

void Merge_Sort(int arr[], int len) {
    Dvide(arr, 0, len - 1);
}

二、完整排序过程

第三部分:完整测试代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>

void Merge(int arr[], int left, int mid, int right) {
    int* brr = (int*)malloc(sizeof(int) * (right - left + 1));
    if (brr == NULL) return;

    int i = left, j = mid + 1, k = 0;

    while (i <= mid || j <= right) {
        if (i > mid)
            brr[k++] = arr[j++];
        else if (j > right)
            brr[k++] = arr[i++];
        else if (arr[i] <= arr[j])
            brr[k++] = arr[i++];
        else
            brr[k++] = arr[j++];
    }

    for (int t = left; t <= right; t++) {
        arr[t] = brr[t - left];
    }
    free(brr);
}

void Dvide(int arr[], int left, int right) {
    if (left < right) {
        int mid = (left + right) / 2;
        Dvide(arr, left, mid);
        Dvide(arr, mid + 1, right);
        Merge(arr, left, mid, right);
    }
}

void Merge_Sort(int arr[], int len) {
    Dvide(arr, 0, len - 1);
}

void printArray(int arr[], int len, const char* msg) {
    printf("%s", msg);
    for (int i = 0; i < len; i++) printf("%d ", arr[i]);
    printf("\n");
}

int main() {
    int arr1[] = {8, 3, 6, 1, 7, 2, 5, 4};
    int len1 = sizeof(arr1) / sizeof(arr1[0]);
    printArray(arr1, len1, "排序前:");
    Merge_Sort(arr1, len1);
    printArray(arr1, len1, "排序后:");

    int arr2[] = {8, 7, 6, 5, 4, 3, 2, 1};
    int len2 = sizeof(arr2) / sizeof(arr2[0]);
    printf("\n逆序测试:\n");
    printArray(arr2, len2, "排序前:");
    Merge_Sort(arr2, len2);
    printArray(arr2, len2, "排序后:");

    int arr3[] = {5, 2, 8, 2, 9, 1, 5};
    int len3 = sizeof(arr3) / sizeof(arr3[0]);
    printf("\n重复元素测试(稳定性验证):\n");
    printArray(arr3, len3, "排序前:");
    Merge_Sort(arr3, len3);
    printArray(arr3, len3, "排序后:");

    return 0;
}

第四部分:算法分析

一、时间复杂度

情况 时间复杂度 说明
最好 O(n log n) 始终如此
最坏 O(n log n) 始终如此
平均 O(n log n) 始终如此

推导

二、空间复杂度

O(n),需要临时数组存放合并结果。这是归并排序的主要缺点------不像快排和堆排是原地排序。

三、稳定性

归并排序是稳定的 。因为合并时 arr[i] <= arr[j] 优先取左边的,相等元素保持原有顺序。


第五部分:O(n log n) 排序终极对比

对比项 快速排序 堆排序 归并排序
平均 O(n log n) O(n log n) O(n log n)
最坏 O(n²) O(n log n) O(n log n)
空间 O(log n) O(1) O(n)
稳定性
实际速度 ⭐⭐⭐ ⭐⭐ ⭐⭐
适用场景 通用首选 内存受限 需要稳定

一句话选择指南

场景 选谁
通用排序 快速排序(三数取中优化)
内存极度受限 堆排序
需要稳定排序 归并排序
链表排序 归并排序(天然适合)
外部排序(大文件) 归并排序

第六部分:所有排序算法总结

排序算法 平均 最坏 空间 稳定 文章
冒泡排序 O(n²) O(n²) O(1) 第一篇
选择排序 O(n²) O(n²) O(1) 第二篇
插入排序 O(n²) O(n²) O(1) 第三篇
基数排序 O(d×n) O(d×n) O(n+r) 第四篇
快速排序 O(n log n) O(n²) O(log n) 第五篇
希尔排序 O(n^1.3) O(n²) O(1) 第六篇
堆排序 O(n log n) O(n log n) O(1) 第七篇
归并排序 O(n log n) O(n log n) O(n) 本篇

总结

一、核心要点

要点 内容
算法思想 分治:拆分到底 → 合并两个有序数组
时间复杂度 始终 O(n log n)
空间复杂度 O(n)(需要临时数组)
稳定性 ✅ 稳定(<= 优先取左边)
核心操作 Merge:比较两个有序数组,依次取小的

二、代码框架记忆

三、一句话记忆

归并排序先递归拆分成单个元素,再两两合并有序数组。合并时等号优先取左边保证稳定。时间复杂度始终 O(n log n),但需要 O(n) 额外空间,是链表排序和外部排序的首选。

相关推荐
kkeeper~11 小时前
0基础C语言积跬步之数据在内存中的存储
c语言·数据结构·算法
2401_8685347811 小时前
论企业网络设计
数据结构
wabs66612 小时前
关于贪心算法的一些自我总结【力扣45.跳跃游戏II】【灵感来源:代码随想录】
算法·贪心算法·复盘
2401_8769641312 小时前
【湖北专升本】2026湖北专升本真题PDF+备考资料汇总
数据结构·人工智能·经验分享·深度学习·算法·计算机视觉
嗝o゚13 小时前
CANN GE 算子融合——融合算法与调度策略
算法·昇腾·cann·ge
小江的记录本13 小时前
【JVM虚拟机】垃圾回收GC:垃圾回收算法:标记-清除、标记-复制、标记-整理、分代收集(附《思维导图》+《面试高频考点清单》)
java·jvm·后端·python·算法·安全·面试
Ulyanov14 小时前
用声明式语法重新定义Python桌面UI:QML+PySide6现代开发入门(一)
开发语言·python·算法·ui·系统仿真·雷达电子对抗仿真
数据科学小丫14 小时前
特征工程处理
人工智能·算法·机器学习
z落落15 小时前
C#参数区别
java·算法·c#