在排序算法的江湖中,归并排序与快速排序是当之无愧的「顶流」。它们都深度贯彻了分治思想——将大问题拆解为小问题,逐个击破后再整合结果,但在实现逻辑、性能表现上却各有千秋。今天我们就从零开始,吃透这两种算法的核心原理、代码实现,并通过对比分析,搞懂不同场景下该如何选择。
先明确分治思想的核心三步法,这是理解两种算法的关键:
-
分解:将复杂问题拆分为若干个规模较小、结构相同的子问题;
-
求解:递归解决每个子问题(当子问题足够小时,可直接得到解);
-
合并:将子问题的解整合为原问题的最终解。
一、归并排序:稳扎稳打的「合并大师」
归并排序(Merge Sort)的核心思路是「先拆后合」:先把数组拆到最小单位(单个元素,天然有序),再通过合并两个有序子数组,逐步构建出完整的有序数组。整个过程像搭积木一样,稳扎稳打,性能稳定。
1.1 核心代码实现(5.js)
const arr = [8, 7, 6, 5, 4, 3, 2, 1]
// 归并排序主函数:负责拆分
function mergeSort(arr) {
const len = arr.length
// 递归终止条件:子数组只有1个元素(有序)
if (len <= 1) {
return arr
}
// 1. 分解:从中间将数组拆分为左右两个子数组
const mid = Math.floor(len / 2)
const leftArr = mergeSort(arr.slice(0, mid)) // 递归拆分左半部分
const rightArr = mergeSort(arr.slice(mid, len)) // 递归拆分右半部分
// 2. 合并:将两个有序子数组合并为一个有序数组
return mergeArr(leftArr, rightArr)
}
// 辅助函数:合并两个有序数组
function mergeArr(arr1, arr2) {
let res = []
let i = 0 // arr1指针
let j = 0 // arr2指针
// 双指针遍历,比较两个数组元素,按顺序存入结果
while (i < arr1.length && j < arr2.length) {
if (arr1[i] <= arr2[j]) {
res.push(arr1[i])
i++
} else {
res.push(arr2[j])
j++
}
}
// 处理剩余未遍历的元素(直接拼接,因为子数组本身有序)
if (i < arr1.length) {
res = res.concat(arr1.slice(i))
} else {
res = [...res, ...arr2.slice(j)]
}
return res
}
console.log(mergeSort(arr)); // 输出:[1,2,3,4,5,6,7,8]
1.2 工作流程拆解
归并排序的过程可以清晰分为「拆分阶段」和「合并阶段」,配合图示理解更直观:
拆分阶段
【此处可放第一张图片:归并排序拆分过程示意图】
以数组 [8,7,6,5,4,3,2,1] 为例,拆分过程如下:
-
初始数组拆分为
[8,7,6,5]和[4,3,2,1]; -
继续拆分:
[8,7]、[6,5]、[4,3]、[2,1]; -
最终拆分到单个元素:
[8]、[7]、[6]、[5]、[4]、[3]、[2]、[1](单个元素视为有序)。
合并阶段
【此处可放第二张图片:归并排序合并过程示意图】
从最小的有序子数组开始,逐步合并为更大的有序数组:
-
合并
[8]与[7]→[7,8];合并[6]与[5]→[5,6],以此类推得到[3,4]、[1,2]; -
继续合并
[7,8]与[5,6]→[5,6,7,8];合并[3,4]与[1,2]→[1,2,3,4]; -
最终合并
[5,6,7,8]与[1,2,3,4]→[1,2,3,4,5,6,7,8]。
1.3 归并排序核心特点
| 评估维度 | 具体说明 |
|---|---|
| 时间复杂度 | 最好/最坏/平均均为 O(n log n),拆分过程是 log n 层,每层合并是 O(n),整体稳定 |
| 空间复杂度 | O(n),需要额外空间存储合并过程中的临时数组(res)和递归调用栈 |
| 稳定性 | 稳定排序(当两个元素相等时,会保留原有相对顺序,因合并时判断条件是 arr1[i] ≤ arr2[j]) |
| 适用场景 | 适合对稳定性要求高、数据量较大的场景(如对象数组排序),缺点是占用额外空间 |
二、快速排序:剑走偏锋的「分区快手」
快速排序(Quick Sort)同样基于分治思想,但思路更「激进」:它不先拆到最小单位,而是通过「选基准、分区」直接将数组拆分为「小于基准」和「大于基准」两部分,再递归处理这两部分。核心优势是原地排序(基本不用额外空间),平均性能极佳。
2.1 核心代码实现(6.js)
const arr = [8, 7, 6, 5, 3, 2, 1]
// 快速排序主函数
function quickSort(arr) {
// 递归终止条件:子数组只有1个元素(有序)
if (arr.length <= 1) {
return arr
}
// 1. 选基准:取数组中间元素作为基准(也可选择首/尾/随机元素)
let midIndex = Math.floor(arr.length / 2)
let mid = arr.splice(midIndex, 1)[0] // 取出基准元素,原数组被修改
// 2. 分区:将数组分为小于基准和大于基准的两个子数组
let left = [] // 存储小于基准的元素
let right = [] // 存储大于基准的元素
const len = arr.length
for (let i = 0; i < len; i++) {
if (arr[i] < mid) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
// 3. 递归处理左右子数组,再合并结果(left + 基准 + right)
return [...quickSort(left), mid, ...quickSort(right)]
}
console.log(quickSort(arr)); // 输出:[1,2,3,5,6,7,8]
2.2 工作流程拆解
快速排序的核心是「分区」,整个过程可以概括为「选基准 → 分区 → 递归」,配合图示更易理解:
【此处可放第三张图片:快速排序完整流程示意图】
以数组 [8,7,6,5,3,2,1] 为例,流程如下:
-
选基准:取中间元素
5(索引2),从原数组中取出; -
分区:遍历剩余元素,小于5的放入left(
[3,2,1]),大于5的放入right([8,7,6]); -
递归处理left:
[3,2,1]选基准3,分区为[2,1]和[]; -
继续递归left的子数组
[2,1],选基准2,分区为[1]和[]; -
递归处理right:
[8,7,6]选基准7,分区为[6]和[8]; -
合并所有结果:
[1] + [2] + [3] + [5] + [6] + [7] + [8]→ 最终有序数组。
2.3 快速排序核心特点
| 评估维度 | 具体说明 |
|---|---|
| 时间复杂度 | 平均 O(n log n),最坏 O(n²)(当数组有序且选首/尾为基准时),最好 O(n log n);可通过「随机选基准」优化最坏情况 |
| 空间复杂度 | O(log n)~O(n),主要来自递归调用栈(理想情况是 log n 层,最坏是 n 层),本实现因使用splice和数组拼接,额外空间略多,优化版可实现原地排序(O(log n)) |
| 稳定性 | 不稳定排序(分区过程中,相等元素的相对顺序可能被打乱,如 [3,2,3,1] 选3为基准时) |
| 适用场景 | 适合数据量较大、对空间要求高的场景(如大数据排序),是实际开发中最常用的排序算法之一 |
三、归并排序 vs 快速排序:核心对比
为了更清晰地看出两种算法的差异,我们整理了一张全面对比表:
| 对比维度 | 归并排序 | 快速排序 |
|---|---|---|
| 分治核心 | 先拆分到最小单位,再合并有序子数组 | 先分区(按基准拆分),再递归处理子数组 |
| 时间复杂度 | 稳定 O(n log n)(所有情况) | 平均 O(n log n),最坏 O(n²)(可优化) |
| 空间复杂度 | O(n)(必须额外空间存储临时数组) | O(log n)~O(n)(主要是递归栈,可优化为原地排序) |
| 稳定性 | 稳定 | 不稳定 |
| 核心优势 | 性能稳定、排序稳定 | 平均速度快、空间占用少 |
| 核心劣势 | 占用额外空间 | 最坏情况性能差、不稳定 |
| 适用场景 | 稳定性要求高、数据量大但空间充足 | 空间紧张、对平均性能要求高 |
四、总结:如何选择?
归并排序和快速排序都是基于分治思想的高效排序算法,没有绝对的优劣,只有场景的适配:
-
如果需要稳定排序(如按价格排序商品,相同价格保留原顺序),或数据本身有序/接近有序(避免快速排序最坏情况),选归并排序;
-
如果追求极致的平均性能,或项目空间资源紧张(如嵌入式开发),选快速排序(记得优化基准选择,如随机基准);
-
日常开发中,快速排序因平均性能更优,应用更广泛(如 JavaScript 的 Array.sort() 底层就融合了快速排序的思想)。
理解两种算法的核心差异,不仅能帮助我们在实际开发中做出最优选择,更能加深对「分治思想」的理解——将复杂问题拆解为可解决的小问题,再整合求解,这才是算法设计的精髓。