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

引言

前面我们学习了快速排序和堆排序两种 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) 额外空间,是链表排序和外部排序的首选。

相关推荐
在书中成长23 分钟前
HarmonyOS 小游戏《对战五子棋》开发第18篇 - 棋盘设计
算法·harmonyos
Frostnova丶30 分钟前
(12)LeetCode 76. 最小覆盖子串
算法·leetcode·职场和发展
灯澜忆梦1 小时前
GO_函数_1
算法
小七在进步1 小时前
数据结构:栈与队列之栈的实现
数据结构
言乐61 小时前
Python实现建造微服务商城后台
开发语言·python·算法·微服务·架构
凉云生烟2 小时前
机器学习 02- KNN算法
人工智能·算法·机器学习
wabs6662 小时前
关于动态规划【力扣583.两个字符串的删除操作的思考】
算法·leetcode·动态规划
可编程芯片开发2 小时前
空气流量和空气压力参数解耦系统simulink建模与仿真
算法
小小龙学IT2 小时前
C++ 并发编程深度解析:从内存模型到无锁数据结构
数据结构·c++
_Doubletful2 小时前
妙用位运算:解构汉明距离至100%(提供分析与多解)
c语言·算法·leetcode