现在位置: 首页 > 数据结构 > 正文

归并排序

归并排序(英语:Merge sort,或mergesort),是建立在归并操作上的一种有效的排序算法。

归并排序是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

想象一下,你要整理一副完全打乱的扑克牌。一个高效的方法是:先把这副牌分成两小堆,分别把每一小堆整理好,最后再把这两堆已经有序的牌合并成一整副有序的牌。归并排序 正是基于这种 分而治之 思想的经典排序算法。

它的核心操作可以概括为两个步骤:

  • :递归地将一个大的无序列表,不断地从中间劈开,分解成若干个小的子列表,直到每个子列表只包含一个元素(一个元素的列表自然是有序的)。
  • :递归地将这些有序的子列表两两合并,在合并的过程中进行排序,最终合并成一个完整的有序列表。

适用说明

归并排序适用于数据量大,并且对稳定性有要求的场景。

归并排序是一种 稳定 的排序算法(即相等元素的相对顺序在排序后保持不变),其时间复杂度在任何情况下都是 O(n log n),这使其在处理大规模数据时非常高效,但它的空间复杂度为 O(n),因为合并过程需要额外的数组来暂存数据。

下面的流程图清晰地展示了归并排序分与治的全过程:

过程图示

归并排序是递归算法的一个实例,这个算法中基本的操作是合并两个已排序的数组,取两个输入数组 A 和 B,一个输出数组 C,以及三个计数器 i、j、k,它们初始位置置于对应数组的开始端。

A[i] 和 B[j] 中较小者拷贝到 C 中的下一个位置,相关计数器向前推进一步。

当两个输入数组有一个用完时候,则将另外一个数组中剩余部分拷贝到 C 中。

自顶向下的归并排序,递归分组图示:

对第三行两个一组的数据进行归并排序

对第二行四个一组的数据进行归并排序

整体进行归并排序

Java 实例代码

源码包下载:Download

MergeSort.java 文件代码:

public class MergeSort {

    // 将arr[l...mid]和arr[mid+1...r]两部分进行归并
    private static void merge(Comparable[] arr, int l, int mid, int r) {

        Comparable[] aux = Arrays.copyOfRange(arr, l, r + 1);

        // 初始化,i指向左半部分的起始索引位置l;j指向右半部分起始索引位置mid+1
        int i = l, j = mid + 1;
        for (int k = l; k <= r; k++) {

            if (i > mid) {  // 如果左半部分元素已经全部处理完毕
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {   // 如果右半部分元素已经全部处理完毕
                arr[k] = aux[i - l];
                i++;
            } else if (aux[i - l].compareTo(aux[j - l]) < 0) {  // 左半部分所指元素 < 右半部分所指元素
                arr[k] = aux[i - l];
                i++;
            } else {  // 左半部分所指元素 >= 右半部分所指元素
                arr[k] = aux[j - l];
                j++;
            }
        }
    }

    // 递归使用归并排序,对arr[l...r]的范围进行排序
    private static void sort(Comparable[] arr, int l, int r) {
        if (l >= r) {
            return;
        }
        int mid = (l + r) / 2;
        sort(arr, l, mid);
        sort(arr, mid + 1, r);
        // 对于arr[mid] <= arr[mid+1]的情况,不进行merge
        // 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
        if (arr[mid].compareTo(arr[mid + 1]) > 0)
            merge(arr, l, mid, r);
    }

    public static void sort(Comparable[] arr) {

        int n = arr.length;
        sort(arr, 0, n - 1);
    }

    // 测试MergeSort
    public static void main(String[] args) {

        int N = 1000;
        Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
        sort(arr);
        //打印数组
        SortTestHelper.printArray(arr);
    }
}

算法原理与步骤详解

归并排序的实现主要依赖于两个核心函数:merge_sort(负责"分")和 merge(负责"治")。

1. 分解函数 merge_sort

这个函数采用递归的方式工作:

基准情况:如果当前数组的长度小于等于 1,那么它已经是有序的,直接返回。

递归情况

  • 找到数组的中间位置 mid
  • 对左半部分 arr[:mid] 递归调用 merge_sort
  • 对右半部分 arr[mid:] 递归调用 merge_sort
  • 最后,调用 merge 函数将已经排好序的左右两部分合并起来。

2. 合并函数 merge

这是算法的精髓所在,负责将两个已经有序的小数组合并成一个大的有序数组。过程类似于我们同时翻阅两本已经按字母排序的通讯录,合并成一本。

  1. 创建一个临时数组 temp,以及三个指针:i(指向左数组当前元素),j(指向右数组当前元素),k(指向临时数组的填充位置)。
  2. 比较 left[i]right[j],将较小的那个元素放入 temp[k],然后相应指针和 k 向前移动一步。
  3. 重复步骤 2,直到其中一个数组的所有元素都被放入临时数组。
  4. 将另一个数组中剩余的所有元素(它们本来就比已放入的元素大,且已有序)直接按顺序追加到临时数组的末尾。
  5. 将临时数组 temp 中的有序序列复制回原数组的对应位置。

代码实现与逐行解析

让我们用 Python 来实现归并排序。我们将提供一个完整的、注释详尽的版本。

测试数据

首先,我们准备一个无序的列表作为测试数据:

实例

# 测试数据:一个无序的整数列表
test_array = [38, 27, 43, 3, 9, 82, 10]
print("原始数组:", test_array)

完整实现

实例

def merge_sort(arr):
    """
    归并排序的主函数。
    参数:
        arr (list): 待排序的列表。
    返回:
        list: 排序后的新列表(为了清晰,本实现返回新列表)。
    """

    # 基准情况:如果数组长度为0或1,则已经有序
    if len(arr) <= 1:
        return arr

    # 1. 分:找到中间点,将数组分成两半
    mid = len(arr) // 2
    left_half = arr[:mid]  # 左半部分
    right_half = arr[mid:] # 右半部分

    # 递归地对左右两半进行排序
    left_sorted = merge_sort(left_half)
    right_sorted = merge_sort(right_half)

    # 2. 治:合并两个已排序的子数组
    return merge(left_sorted, right_sorted)


def merge(left, right):
    """
    合并两个已排序的列表。
    参数:
        left (list): 第一个已排序的列表。
        right (list): 第二个已排序的列表。
    返回:
        list: 合并后的有序列表。
    """

    sorted_list = []  # 用于存放合并结果的临时列表
    i = j = 0         # i 和 j 分别是遍历 left 和 right 的指针

    # 3. 比较并合并:当两个列表都还有元素时
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:  # 注意使用 <= 以保持排序的稳定性
            sorted_list.append(left[i])
            i += 1
        else:
            sorted_list.append(right[j])
            j += 1

    # 4. 处理剩余元素:将左列表或右列表中剩余的元素全部追加到结果中
    #    由于 left 和 right 本身已有序,剩余的元素一定比已合并的元素都大
    sorted_list.extend(left[i:])
    sorted_list.extend(right[j:])

    return sorted_list


# 使用测试数据运行算法
sorted_array = merge_sort(test_array.copy())  # 使用copy避免修改原数组
print("归并排序后:", sorted_array)

代码运行与输出

当你运行上面的代码时,你会看到如下输出:

原始数组: [38, 27, 43, 3, 9, 82, 10]
归并排序后: [3, 9, 10, 27, 38, 43, 82]

算法复杂度分析

理解一个算法的效率至关重要。我们通过下表来总结归并排序的性能:

指标 复杂度 说明
时间复杂度 O(n log n) 这是归并排序最显著的优势。log n 来自于递归的层数(将数组对半分),n 来自于每一层合并操作需要遍历所有元素。无论数据初始状态如何(最好、最坏、平均情况),时间复杂度都是 O(n log n)。
空间复杂度 O(n) 合并过程需要与原始数组等大的额外空间来存储临时数组。这是归并排序的主要缺点。
稳定性 稳定 merge 函数中,当 left[i] == right[j] 时,我们优先取 left[i](使用了 <= 比较),这保证了相等元素的原始相对顺序不变。
是否原地排序 需要额外的内存空间。

更多代码展示