归并排序 vs 快速排序:分治思想下的两大排序王者对决

105 阅读7分钟

在排序算法的江湖中,归并排序与快速排序是当之无愧的「顶流」。它们都深度贯彻了分治思想——将大问题拆解为小问题,逐个击破后再整合结果,但在实现逻辑、性能表现上却各有千秋。今天我们就从零开始,吃透这两种算法的核心原理、代码实现,并通过对比分析,搞懂不同场景下该如何选择。

先明确分治思想的核心三步法,这是理解两种算法的关键:

  1. 分解:将复杂问题拆分为若干个规模较小、结构相同的子问题;

  2. 求解:递归解决每个子问题(当子问题足够小时,可直接得到解);

  3. 合并:将子问题的解整合为原问题的最终解。

一、归并排序:稳扎稳打的「合并大师」

归并排序(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] 为例,拆分过程如下:

  1. 初始数组拆分为 [8,7,6,5][4,3,2,1]

  2. 继续拆分:[8,7][6,5][4,3][2,1]

  3. 最终拆分到单个元素:[8][7][6][5][4][3][2][1](单个元素视为有序)。

合并阶段

【此处可放第二张图片:归并排序合并过程示意图】

从最小的有序子数组开始,逐步合并为更大的有序数组:

  1. 合并 [8][7][7,8];合并 [6][5][5,6],以此类推得到 [3,4][1,2]

  2. 继续合并 [7,8][5,6][5,6,7,8];合并 [3,4][1,2][1,2,3,4]

  3. 最终合并 [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] 为例,流程如下:

  1. 选基准:取中间元素 5(索引2),从原数组中取出;

  2. 分区:遍历剩余元素,小于5的放入left([3,2,1]),大于5的放入right([8,7,6]);

  3. 递归处理left:[3,2,1] 选基准3,分区为 [2,1][]

  4. 继续递归left的子数组 [2,1],选基准2,分区为 [1][]

  5. 递归处理right:[8,7,6] 选基准7,分区为 [6][8]

  6. 合并所有结果:[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)(主要是递归栈,可优化为原地排序)
稳定性稳定不稳定
核心优势性能稳定、排序稳定平均速度快、空间占用少
核心劣势占用额外空间最坏情况性能差、不稳定
适用场景稳定性要求高、数据量大但空间充足空间紧张、对平均性能要求高

四、总结:如何选择?

归并排序和快速排序都是基于分治思想的高效排序算法,没有绝对的优劣,只有场景的适配:

  1. 如果需要稳定排序(如按价格排序商品,相同价格保留原顺序),或数据本身有序/接近有序(避免快速排序最坏情况),选归并排序;

  2. 如果追求极致的平均性能,或项目空间资源紧张(如嵌入式开发),选快速排序(记得优化基准选择,如随机基准);

  3. 日常开发中,快速排序因平均性能更优,应用更广泛(如 JavaScript 的 Array.sort() 底层就融合了快速排序的思想)。

理解两种算法的核心差异,不仅能帮助我们在实际开发中做出最优选择,更能加深对「分治思想」的理解——将复杂问题拆解为可解决的小问题,再整合求解,这才是算法设计的精髓。