基础排序算法(七)归并排序

一 归并排序

归并排序是一种基于分治法的高效、稳定的排序算法。其核心思想是将两个或多个已排序的序列合并成一个新的有序序列。

1.1 算法特性

归并排序特性总结

特性 说明
核心思想 分治法:将数组递归地分成两半,分别排序后,再将两个有序子数组合并成一个有序数组
时间复杂度 最好、最坏、平均情况下均为 O(n log n)
空间复杂度 O(n)。需要与原始数组等长的额外空间用于合并操作
稳定性 稳定。在合并过程中,当两个元素相等时,优先取前一子数组的元素,保持相对顺序
主要优势 时间复杂度稳定;适用于大规模数据;稳定排序;易于并行化
主要劣势 需要额外空间,不是原地排序;对于小规模数据,常数因子可能使其不如简单排序算法高效

1.2 算法原理

归并排序是一种基于​​分治法​​的递归算法。其核心流程是先将大问题分解为小问题,解决小问题后,再将结果合并起来。具体到排序中,过程如下:

  1. ​​分解​​:将当前待排序的数组从中间位置分成两个子数组(递归进行,直到每个子数组只包含一个元素,单个元素自然有序)。
  2. ​​解决​​:递归地对两个子数组进行归并排序。
  3. ​​合并​​:这是算法的关键步骤。将两个已经有序的子数组合并成一个新的有序数组。合并时,使用双指针法,比较两个子数组当前指针所指的元素,将较小的元素放入临时数组,然后移动指针,直到某个子数组遍历完,再将另一个子数组的剩余部分直接复制到临时数组末尾。最后将临时数组的内容复制回原数组。

1.3 复杂度分析

  • ​时间复杂度 O(n log n) 的由来​​:归并排序将数组二分的过程形成了一棵递归树,树的高度为 log₂n。在每一层,都需要对当前所有元素进行一次合并操作,合并操作的时间复杂度是 O(n)。因此,总的时间复杂度为 O(n) × O(log n) = ​​O(n log n)​​。重要的是,这个效率在​​最好、最坏和平均情况下都是一致的​​,不会因为输入数据的原始顺序而退化。
  • ​​空间复杂度 O(n) 的由来​​:合并两个有序子数组时,无法在原地完成,需要借助一个与原始数组等大的​​临时数组​​来存储中间结果。这是归并排序的主要缺点。

1.4 使用场景

归并排序在以下场景中表现出色:

  • ​​大规模数据排序​​:由于其稳定的 O(n log n) 时间复杂度,在处理海量数据时性能可靠。
  • ​​需要稳定性的排序​​:在如先按成绩排序再按姓名排序这类场景中,保持相等元素的初始顺序很重要,归并排序是稳定算法。
  • ​​外部排序​​:当数据量太大无法全部装入内存时,归并排序是外部排序(如多路归并)的基础算法,因为它可以高效地合并多个已排序的大文件。
  • ​​链表排序​​:归并排序非常适用于链表结构,因为链表节点在合并时可以通过改变指针实现,不需要额外空间。

1.5 代码实现

1.5.1 c语言代码实现

#include <stdio.h>
#include <stdlib.h>

// 合并两个有序子数组 arr[left...mid] 和 arr[mid+1...right]
void Merge(int arr[], int left, int mid, int right) {
    int n1 = mid - left + 1; // 左子数组长度
    int n2 = right - mid;    // 右子数组长度

    // 创建临时数组存放左右两部分的数
    int *L = (int *)malloc(n1 * sizeof(int));
    int *R = (int *)malloc(n2 * sizeof(int));

    // 将数据拷贝到临时数组 L[] 和 R[] 中
    for (int i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];

    // 合并临时数组回原数组 arr[left..right]
    int i = 0;    // 初始化左子数组的索引
    int j = 0;    // 初始化右子数组的索引
    int k = left; // 初始化合并子数组的索引
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) { // 使用 <= 保证稳定性
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    // 将 L[] 剩余的元素复制回来(如果有)
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }

    // 将 R[] 剩余的元素复制回来(如果有)
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }

    // 释放临时数组的内存
    free(L);
    free(R);
}

// 归并排序的主递归函数
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);
    }
}

// 打印数组函数
void PrintArray(int arr[], int size) {
    for (int i = 0; i < size; i++)
        printf("%d ", arr[i]);
    printf("\n");
}

// 主函数测试
int main() {
    int arr[] = {12, 11, 13, 5, 6, 7};
    int arr_size = sizeof(arr) / sizeof(arr[0]);

    printf("排序前的数组: \n");
    PrintArray(arr, arr_size);

    MergeSort(arr, 0, arr_size - 1);

    printf("排序后的数组: \n");
    PrintArray(arr, arr_size);
    return 0;
}

1.5.2 ​代码关键点解释​​

  • Merge函数:这是核心。它负责将两个有序的子数组合并成一个有序数组。通过双指针遍历,比较元素大小,并按序放入原数组。if (L[i] <= R[j])中的 <=确保了排序的稳定性。
  • MergeSort函数:通过递归不断将数组对半分割,直到子数组长度为1(自然有序),然后调用 Merge函数从最小的子数组开始向上合并。

1.6 常用算法比较

排序算法比较

算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 主要特点与适用场景
归并排序 O(n log n) O(n log n) O(n) 稳定 性能稳定,可靠,适合大数据量、要求稳定性或外部排序的场景
快速排序 O(n log n) O(n²) O(log n) 不稳定 平均性能极佳,是许多标准库的实现选择,但对初始数据敏感(如已有序序列会导致性能退化)
堆排序 O(n log n) O(n log n) O(1) 不稳定 最坏情况也能保证O(n log n),且是原地排序,但常数因子较大,缓存不友好
插入排序 O(n²) O(n²) O(1) 稳定 对小规模或基本有序数据效率很高;简单

从对比中可以看出,归并排序的主要优势在于其​​稳定的时间复杂度​​和​​稳定性​​。然而,其需要​​额外O(n)空间​​的特性是其主要劣势。在实际应用中,​​快速排序​​因其平均情况下的高效和原地排序的特性,通常是内排序的首选,但如果你​​无法承受快速排序的最坏情况​​,或者​​必须要求排序稳定​​,那么归并排序就是更好的选择。

1.7 总结

总的来说,归并排序是一种基于分治策略的高效、稳定的排序算法。其​​最坏情况下仍能保持O(n log n)的时间复杂度​​是其最大优点,使其在处理大规模数据或对性能有稳定要求的场景中非常可靠。缺点是​​需要O(n)的额外空间​​。理解归并排序有助于深入掌握分治思想,并为学习更复杂的算法(如外部排序)打下基础。

posted on 2025-11-03 14:25  weiwei2021  阅读(3)  评论(0)    收藏  举报