归并排序详解:优雅的分治艺术
什么?归并排序?这让博主想起了大学那会被《数据结构与算法》支配的恐惧…
哈哈言归正传,一直想对算法做一个专栏,因为其实工作中很少很少有机会用到算法,倒是很多工具方法底层会使用,工作被各种需求业务“折磨”的让大家很少再花费精力去深入这些底层,更别说很少有人天天刷算法,导致以前在学校为了应付面试和笔试(也包括社招)学习的这些玩意和基础随着时间慢慢消磨殆尽,所以这篇专栏来了!每一篇博主都会详细说明这些算法的设计思想和代码实现,帮助博主和大家共同进步,每篇如果有错误和需要改进的地方欢迎大家提出!下面开始我们第一篇,也是基础的——归并排序。
文章目录
- 前言
- 🌟 核心思想
- ⚙️ Java实现
- 🔍 时间复杂度分析
- 📦 空间复杂度
- ✅ 算法特性
- ⚡ 优化技巧
- 💡 实际应用场景
- 🌐 总结
前言
归并排序是一种基于分治法的高效排序算法,由冯·诺依曼于1945年首次提出。它以其稳定的O(n log n)时间复杂度和优雅的实现方式在算法领域占据重要地位。下面我将详细介绍归并排序的原理,并提供完整的Java实现。
🌟 核心思想
先给出归并排序示意图:
归并排序的核心思想可概括为三步:
- 分解:将待排序数组递归地分成两个子数组。
- 解决:对子数组进行排序(递归排序)。
- 合并:将两个有序子数组合并成一个有序数组。
原始数组:[38, 27, 43, 3, 9, 82, 10]分解过程:
1. [38, 27, 43, 3] [9, 82, 10]
2. [38, 27] [43, 3] [9, 82] [10]
3. [38] [27] [43] [3] [9] [82] [10]合并过程:
1. [27, 38] [3, 43] [9, 82] [10]
2. [3, 27, 38, 43] [9, 10, 82]
最终结果:[3, 9, 10, 27, 38, 43, 82]
⚙️ Java实现
public class MergeSort {// 归并排序主方法public static void mergeSort(int[] arr) {if (arr == null || arr.length <= 1) {return;}int[] temp = new int[arr.length];mergeSort(arr, temp, 0, arr.length - 1);}// 递归排序private static void mergeSort(int[] arr, int[] temp, int left, int right) {if (left < right) {int mid = left + (right - left) / 2; // 防止整数溢出// 递归排序左半部分mergeSort(arr, temp, left, mid);// 递归排序右半部分mergeSort(arr, temp, mid + 1, right);// 合并两个有序子数组merge(arr, temp, left, mid, right);}}// 合并两个有序子数组private static void merge(int[] arr, int[] temp, int left, int mid, int right) {// 复制数据到临时数组System.arraycopy(arr, left, temp, left, right - left + 1);int i = left; // 左子数组起始索引int j = mid + 1; // 右子数组起始索引int k = left; // 合并数组的起始索引// 比较左右子数组元素,将较小者放入原数组while (i <= mid && j <= right) {if (temp[i] <= temp[j]) {arr[k++] = temp[i++];} else {arr[k++] = temp[j++];}}// 复制左子数组剩余元素while (i <= mid) {arr[k++] = temp[i++];}// 复制右子数组剩余元素while (j <= right) {arr[k++] = temp[j++];}}// 测试代码public static void main(String[] args) {int[] arr = {38, 27, 43, 3, 9, 82, 10};System.out.println("排序前: " + Arrays.toString(arr));mergeSort(arr);System.out.println("排序后: " + Arrays.toString(arr));}
}
算法解析
- mergeSort()方法:
- 公共入口方法。
- 创建临时数组避免在递归过程中重复创建。
- 调用私有递归方法。
- 递归排序:
- 计算中点:mid = left + (right - left)/2(避免整数溢出)。
- 递归排序左半部分:left到mid。
- 递归排序右半部分:mid+1到right。
- 合并两个有序子数组。
- merge()方法:
- 复制当前段数据到临时数组。
- 使用三个指针分别追踪:
- i:左子数组当前位置
- j:右子数组当前位置
- k:合并后数组当前位置
- 比较左右子数组元素,将较小者放入原数组。
- 处理剩余元素。
🔍 时间复杂度分析
操作 | 时间复杂度 |
---|---|
分解 | O(log n) |
合并 | O(n) |
总复杂度 | O(n log n) |
归并排序在任何情况下的时间复杂度都是稳定的O(n log n),这是它的最大优势。
📦 空间复杂度
- O(n):需要额外的空间存储临时数组
- 不是原地排序算法(in-place)
✅ 算法特性
- 稳定性:保持相等元素的原始顺序(关键:合并时left[i] <= right[j])
- 适用场景:大数据量排序、链表排序、外部排序
- 不适用场景:内存受限的小数据量排序
⚡ 优化技巧
- 小数组切换插入排序:当子数组长度较小时使用插入排序。
归并排序即使处理小规模数据仍需递归调用、分割合并等操作,其时间复杂度虽为O(n log n),但递归栈管理、函数调用等额外开销的常数因子较大,插入排序在小规模数据(如n≤15)时实际运行时间接近O(n)。数据量≤15时,元素局部有序概率高,实际操作次数接近线性,且其简单比较交换操作的常数因子极低,在阈值范围内性能显著优于归并排序。
ex: 常数因子指算法时间复杂度表示式中的固定系数(如O(2n)中的2),简单交换位移不需要递归方法调用也不需要分配空间,自然开销低。
private static final int INSERTION_THRESHOLD = 15;private static void mergeSort(int[] arr, int[] temp, int left, int right) {if (right - left <= INSERTION_THRESHOLD) {insertionSort(arr, left, right);return;}// 其余代码不变
}private static void insertionSort(int[] arr, int left, int right) {for (int i = left + 1; i <= right; i++) {int key = arr[i];int j = i - 1;while (j >= left && arr[j] > key) {arr[j + 1] = arr[j];j--;}arr[j + 1] = key;}
}
- 边界检查优化:在合并前检查左子数组最大值是否小于等于右子数组最小值。
// 在merge方法开始处添加
if (arr[mid] <= arr[mid + 1]) {return; // 已经有序,无需合并
}
- 避免重复复制:通过交换输入数组和临时数组的角色减少复制操作
💡 实际应用场景
- 大数据处理:MapReduce等分布式计算框架的底层排序
- 外部排序:处理超过内存容量的数据集
- 稳定排序需求:数据库排序、订单记录处理
- 链表排序:归并排序是链表最高效的排序方式(链表归并排序仅需修改节点指针指向,实现O(1)空间复杂度的原址排序)
🌐 总结
归并排序以其稳定高效的特性,成为处理大规模数据的首选算法之一。虽然需要额外空间,但它的分治思想在算法设计中具有重要地位,是理解递归和分治策略的经典案例。
学过Java的小伙伴,Arrays.sort()与Collections.sort()底层就是使用优化后的归并排序呦,感兴趣的小伙伴可以再深入研究下。