归并排序算法

一、算法概述

归并排序是一种稳定的非原地排序算法,核心依托"分而治之"(Divide and Conquer)的思想,将复杂的排序问题拆解为若干个简单的子问题,通过解决子问题并合并结果,最终实现整体有序。

其核心流程分为"分""治"两个阶段:

  1. 分(Divide):将待排序序列递归地拆分为两个长度大致相等的子序列,直到每个子序列仅包含一个元素(单个元素天然有序);

  2. 治(Merge):将两个有序的子序列按照大小规则合并为一个有序的序列,逐层回溯合并,最终得到完整的有序序列。

二、核心原理与执行流程

归并排序的核心逻辑是"拆分无差别,合并有规则",其中合并操作是算法的灵魂。以下以升序排序为例,通过具体示例拆解完整执行流程。

2.1 核心思想拆解

  • 拆分阶段:采用二分法思想,将长度为n的序列从中间位置(mid = (left + right) / 2)拆分为左子序列left, mid和右子序列mid+1, right,对两个子序列递归执行拆分操作,直到子序列长度为1;

  • 合并阶段:初始化一个临时数组,用于存储合并后的有序序列。同时设置两个指针分别指向两个有序子序列的起始位置,依次比较指针指向的元素,将较小的元素放入临时数组并移动对应指针;当其中一个子序列遍历完毕后,将另一个子序列的剩余元素直接追加到临时数组末尾,最后将临时数组的有序数据复制回原序列的对应区间。

2.2 可视化执行流程(示例:\[8, 4, 5, 7, 1, 3, 6, 2\]

  1. 初始序列:8, 4, 5, 7, 1, 3, 6, 2,拆分区间0,7

  2. 第一次拆分:拆分为左8,4,5,70,3)、右1,3,6,24,7);

  3. 第二次拆分:左子序列拆分为8,40,1)、5,72,3);右子序列拆分为1,34,5)、6,26,7);

  4. 最终拆分:所有子序列拆分为单个元素:84571362

  5. 逐层合并:

    合并 844,8 ;合并 575,7 ;合并 4,85,74,5,7,8 ;合并 131,3 ;合并 622,6 ;合并 1,32,61,2,3,6 ;最终合并 4,5,7,81,2,3,61,2,3,4,5,6,7,8

三、算法实现(以Java为例)

归并排序有两种经典实现方式:递归实现 (逻辑直观,符合分治思想)和非递归实现(避免递归栈溢出,适合大规模数据)。同时,针对递归实现的空间开销,可通过"原地合并"进行优化。

3.1 递归实现

java 复制代码
/**
 * 归并排序递归实现(升序)
 * @param arr 待排序数组
 */
public static void mergeSortRecursive(int[] arr) {
    // 边界校验:空数组或长度小于2,直接返回
    if (arr == null || arr.length < 2) {
        return;
    }
    // 初始化临时数组,避免递归中重复创建,减少性能损耗
    int[] temp = new int[arr.length];
    // 调用核心递归方法
    mergeSort(arr, 0, arr.length - 1, temp);
}

/**
 * 递归拆分与合并核心方法
 * @param arr 待排序数组
 * @param left 左边界索引
 * @param right 右边界索引
 * @param temp 临时合并数组
 */
private static void mergeSort(int[] arr, int left, int right, int[] temp) {
    // 递归终止条件:左边界 >= 右边界(子序列长度为1)
    if (left >= right) {
        return;
    }
    // 计算中间索引,避免溢出
    int mid = left + (right - left) / 2;
    // 递归拆分左子序列 [left, mid]
    mergeSort(arr, left, mid, temp);
    // 递归拆分右子序列 [mid+1, right]
    mergeSort(arr, mid + 1, right, temp);
    // 合并两个有序子序列
    merge(arr, left, mid, right, temp);
}

/**
 * 合并两个有序子序列的核心方法
 * @param arr 原数组
 * @param left 左子序列起始索引
 * @param mid 左子序列结束索引(右子序列起始索引为mid+1)
 * @param right 右子序列结束索引
 * @param temp 临时数组
 */
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
    int i = left;    // 左子序列指针
    int j = mid + 1; // 右子序列指针
    int k = left;    // 临时数组指针

    // 1. 依次比较两个子序列元素,放入临时数组
    while (i <= mid && j <= right) {
        // 此处用 <= 保证排序的稳定性
        if (arr[i] <= arr[j]) {
            temp[k++] = arr[i++];
        } else {
            temp[k++] = arr[j++];
        }
    }

    // 2. 处理左子序列剩余元素
    while (i <= mid) {
        temp[k++] = arr[i++];
    }

    // 3. 处理右子序列剩余元素
    while (j <= right) {
        temp[k++] = arr[j++];
    }

    // 4. 将临时数组的有序数据复制回原数组
    System.arraycopy(temp, left, arr, left, right - left + 1);
}

3.2 非递归实现(迭代版,避免栈溢出)

递归实现在处理超大规模数据时,可能因递归深度过大导致栈溢出。非递归实现通过手动控制拆分步长,逐层合并,彻底解决该问题。

java 复制代码
/**
 * 归并排序非递归实现(升序)
 * @param arr 待排序数组
 */
public static void mergeSortIterative(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    int n = arr.length;
    int[] temp = new int[n];
    // 步长从1开始,每次翻倍(1→2→4→8...),直到步长 >= 数组长度
    for (int step = 1; step < n; step *= 2) {
        // 按步长遍历数组,合并相邻的两个子序列
        for (int left = 0; left < n; left += 2 * step) {
            // 计算中间索引和右边界索引,避免越界
            int mid = Math.min(left + step - 1, n - 1);
            int right = Math.min(left + 2 * step - 1, n - 1);
            // 合并当前两个有序子序列
            merge(arr, left, mid, right, temp);
        }
    }
}

3.3 优化实现(原地合并,减少空间开销)

java 复制代码
/**
 * 原地合并两个有序子序列(替代临时数组,优化空间)
 * @param arr 原数组
 * @param left 左边界
 * @param mid 中间边界
 * @param right 右边界
 */
private static void mergeInPlace(int[] arr, int left, int mid, int right) {
    int i = left;
    int j = mid + 1;
    // 当左子序列的末尾 <= 右子序列的开头,直接有序,无需合并
    if (arr[mid] <= arr[j]) {
        return;
    }
    // 原地合并核心逻辑:通过移位实现元素插入
    while (i <= mid && j <= right) {
        if (arr[i] <= arr[j]) {
            i++;
        } else {
            // 保存右子序列当前元素
            int val = arr[j];
            int k = j;
            // 将[i, k-1]区间的元素右移一位
            while (k > i) {
                arr[k] = arr[k - 1];
                k--;
            }
            // 将val插入到i位置
            arr[i] = val;
            // 指针集体后移
            i++;
            mid++;
            j++;
        }
    }
}

// 调用时,将merge方法替换为mergeInPlace即可

四、关键细节

归并排序的实现看似规整,但在工程落地中,以下细节直接决定算法的正确性和性能:

4.1 稳定性的保障

归并排序是稳定排序,核心在于合并阶段的比较逻辑 。在 merge 方法中,必须使用 arr[i] <= arr[j] 而非 arr[i] < arr[j]。若使用 <,当两个元素相等时,会优先选择右子序列的元素,导致相等元素的相对位置颠倒,破坏稳定性。

4.2 避免数组越界

在非递归实现中,步长翻倍时可能超出数组长度,因此必须通过 Math.min 计算 midright,确保索引不越界。例如,当数组长度为7,步长为4时,右边界应取6而非7。

4.3 减少临时数组的创建

递归实现中,临时数组应在入口方法初始化 ,而非在 merge 方法中每次创建。频繁创建数组会带来大量的内存分配与回收开销,严重降低性能。

4.4 小规模数据的优化

当子序列长度较小时(如n \\leq 15),归并排序的递归开销会大于排序本身的开销。此时可在递归终止条件中,改用插入排序处理子序列,结合插入排序在小规模数据下的高效性,提升整体算法性能。

相关推荐
就改了几秒前
ElasticsearchRestTemplate使用方法详解!!!
java·elasticsearch·springboot
林森lsjs3 分钟前
【日耕一题】5. 青春常数(17届蓝桥杯C++B组第一题)
算法·蓝桥杯
Tisfy6 分钟前
LeetCode 3838.带权单词映射:求和、取模、拼接(附python一行版)
python·算法·leetcode·字符串·题解·模拟·取模
独隅6 分钟前
IntelliJ IDEA 在 Linux 上的完整安装与使用指南
java·linux·intellij-idea
SimonKing7 分钟前
别再自己写脚本了!DeepSeek三秒生成,豆包直接出片
java·后端·程序员
め.12 分钟前
GJK算法实现细节
算法
AI科技星13 分钟前
第六卷:量天尺传奇(几何学)
网络·人工智能·算法·概率论·学习方法·几何学·拓扑学
Y_Bk15 分钟前
第十七届蓝桥杯C/C++A组省赛
c语言·数据结构·c++·算法·蓝桥杯
飞天狗11115 分钟前
零基础JavaWeb入门——第4课:表单处理 —— 浏览器怎么把数据发给服务器
java·开发语言·前端·后端·servlet
帅小伙―苏20 分钟前
力扣76最小覆盖子串
算法·leetcode