归并排序
作者
深入理解归并排序:分治策略的经典实现¶
大家好,我是你们的技术专栏主笔jed。在算法的世界里,排序算法无疑是基石般的存在。今天,我们要深入探讨的是一位“老将”——归并排序(Merge Sort)。
由计算机科学之父约翰·冯·诺依曼于 1945 年提出,归并排序至今仍活跃在大数据处理、链表排序等关键场景中。它不仅是分治策略(Divide and Conquer)的教科书式范例,更是理解算法稳定性与时间复杂度权衡的绝佳案例。
本文将带你从零开始,拆解归并排序的核心逻辑,并提供 C++、Python、Java 三种语言的实战代码,助你彻底掌握这一经典算法。
一、核心概念与特性¶
归并排序的核心思想非常直观:“分而治之”。它将一个复杂的大问题分解为若干个小问题,解决后再合并结果。
在深入代码之前,我们需要明确它的三个关键特性,这也是面试和工程选型中的高频考点:
- 时间复杂度:
无论最好、平均还是最坏情况,归并排序的时间复杂度均为 O(n log n)。这意味着它的性能非常稳定,不会像快速排序那样在极端情况下退化为 O(n²)。 - 空间复杂度:
为了实现合并操作,它需要 O(n) 的额外存储空间。这是它最主要的代价,也是空间换时间的典型体现。 - 稳定性:
归并排序属于稳定排序算法。如果在排序前,元素 A 等于元素 B,且 A 在 B 前面,排序后 A 依然在 B 前面。这对于多关键字排序(如先按成绩排序,再按姓名排序)至关重要。
二、算法步骤详解¶
归并排序的执行流程可以概括为三个关键步骤。我们可以将其想象为整理一副扑克牌的过程。
1. 分解(Divide)¶
递归地将数组从中间分割为两个子数组。这个过程一直持续,直到每个子数组仅含一个元素。因为单个元素天然是有序的。
2. 合并(Merge)¶
这是算法的灵魂所在。将两个有序子数组按顺序合并为一个有序数组。
* 创建一个新的临时数组。
* 使用两个指针分别指向两个子数组的起始位置。
* 比较指针指向的元素,将较小的元素放入临时数组,并移动相应指针。
* 重复此过程,直到所有元素都被填充到目标数组中。
3. 递归实现¶
通常采用自顶向下的递归方式。核心逻辑伪代码如下:
* 若左边界小于右边界:
1. 计算中点 mid。
2. 递归排序左子数组 [left, mid]。
3. 递归排序右子数组 [mid+1, right]。
4. 合并左右两个有序子数组。
三、多语言代码实战¶
理论结合实践才是掌握算法的正道。下面我分别使用 C++、Python 和 Java 实现标准的归并排序。
1. C++ 实现¶
C++ 版本注重内存管理和指针操作,适合理解底层逻辑。
#include <iostream>
#include <vector>
using namespace std;
// 合并两个有序子数组
void merge(vector<int>& arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
// 创建临时数组
vector<int> L(n1), R(n2);
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];
// 合并临时数组回原数组
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) { // <= 保证稳定性
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
// 复制剩余元素
while (i < n1) {
arr[k] = L[i];
i++; k++;
}
while (j < n2) {
arr[k] = R[j];
j++; k++;
}
}
// 递归排序
void mergeSort(vector<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);
}
}
int main() {
vector<int> arr = {38, 27, 43, 3, 9, 82, 10};
mergeSort(arr, 0, arr.size() - 1);
for (int num : arr) cout << num << " ";
return 0;
}
2. Python 实现¶
Python 版本代码简洁,利用切片特性可以更优雅地表达分治逻辑。
def merge_sort(arr):
if len(arr) <= 1:
return arr
# 分解
mid = len(arr) // 2
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
# 合并
return merge(left, right)
def merge(left, right):
result = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result.extend(left[i:])
result.extend(right[j:])
return result
# 测试
if __name__ == "__main__":
arr = [38, 27, 43, 3, 9, 82, 10]
print(merge_sort(arr))
3. Java 实现¶
Java 版本强类型结构清晰,适合企业级开发参考。
public class MergeSort {
public void sort(int[] arr) {
if (arr == null || arr.length <= 1) return;
mergeSort(arr, 0, arr.length - 1);
}
private 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);
}
}
private void merge(int[] arr, int left, int mid, int right) {
int n1 = mid - left + 1;
int n2 = right - mid;
int[] L = new int[n1];
int[] R = new int[n2];
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];
int i = 0, j = 0, k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = L[i];
i++; k++;
}
while (j < n2) {
arr[k] = R[j];
j++; k++;
}
}
public static void main(String[] args) {
int[] arr = {38, 27, 43, 3, 9, 82, 10};
new MergeSort().sort(arr);
for (int num : arr) System.out.print(num + " ");
}
}
四、适用场景与优劣分析¶
作为工程师,我们不仅要会写代码,更要懂得何时使用它。
1. 适用场景¶
- 大数据量排序:由于时间复杂度稳定在 O(n log n),它不会因数据分布不均而性能骤降。
- 链表排序:归并排序无需随机访问元素,非常适合链表结构,且链表合并时可节省部分空间开销。
- 外部排序:当数据量超出内存容量时,归并排序是外部排序(如磁盘文件合并)的首选算法。
2. 优势与劣势¶
| 维度 | 优势 | 劣势 |
|---|---|---|
| 效率 | 时间效率稳定,不受数据分布影响 | 小规模数据效率低于快速排序 |
| 并行化 | 子数组排序相互独立,易于并行化处理 | 需要额外 O(n) 空间,内存占用较高 |
| 结构 | 对链表结构友好 | 递归调用栈可能较深(自顶向下版本) |
3. 常见变体¶
为了适应不同需求,归并排序也演化出了多种变体:
* 自顶向下:标准的递归实现,逻辑清晰。
* 自底向上:迭代实现,避免了递归调用的栈开销。
* 原地归并排序:空间优化版本,试图将空间复杂度降低至 O(1),但实现复杂且往往牺牲时间效率,实际较少使用。
五、结语¶
归并排序是算法设计中分治思想的完美体现。虽然它需要额外的空间开销,但其稳定的时间复杂度和稳定性使其在特定领域(如大数据、链表、外部存储)不可替代。
希望通过今天的文章,你不仅能手写归并排序的代码,更能理解其背后的设计哲学。在实际工程中,没有最好的算法,只有最合适的算法。当你在面对海量数据或需要稳定性能的场景时,别忘了这位 1945 年诞生的“老将”。
我是你的技术主笔,我们下期再见。