进阶排序:快排 / 归并 / 堆排序

本节目标

// 运行环境:Node.js 14+
// 保存为 dsa-l18.js,执行:node dsa-l18.js

// 快速排序:选基准 pivot,分区使左小右大,递归两侧
function quickSort(a) {
  if (a.length <= 1) return a
  const [pivot, ...rest] = a
  const left = rest.filter(x => x < pivot)
  const right = rest.filter(x => x >= pivot)
  return [...quickSort(left), pivot, ...quickSort(right)]
}
// 原地版(更省空间,用双指针分区,经典写法):
function quickSortInPlace(a, lo = 0, hi = a.length - 1) {
  if (lo >= hi) return a
  let i = lo
  for (let j = lo; j < hi; j++) if (a[j] < a[hi]) { [a[i], a[j]] = [a[j], a[i]]; i++ }
  [a[i], a[hi]] = [a[hi], a[i]] // 基准归位
  quickSortInPlace(a, lo, i - 1); quickSortInPlace(a, i + 1, hi)
  return a
}

// 归并排序:分两半各自排好,再合并两个有序数组
function mergeSort(a) {
  if (a.length <= 1) return a
  const mid = a.length >> 1
  return merge(mergeSort(a.slice(0, mid)), mergeSort(a.slice(mid)))
}
function merge(L, R) {
  const out = []; let i = 0, j = 0
  while (i < L.length && j < R.length) out.push(L[i] <= R[j] ? L[i++] : R[j++])
  return out.concat(L.slice(i), R.slice(j))
}

// 堆排序:建最大堆,反复把堆顶(最大)换到末尾
function heapSort(a) {
  a = a.slice()
  const n = a.length
  const siftDown = (i, size) => {
    while (true) {
      let m = i, l = 2*i+1, r = 2*i+2
      if (l < size && a[l] > a[m]) m = l
      if (r < size && a[r] > a[m]) m = r
      if (m === i) break
      [a[m], a[i]] = [a[i], a[m]]; i = m
    }
  }
  for (let i = (n >> 1) - 1; i >= 0; i--) siftDown(i, n) // 建堆
  for (let end = n - 1; end > 0; end--) { [a[0], a[end]] = [a[end], a[0]]; siftDown(0, end) }
  return a
}

console.log(quickSort([5,2,4,1,3]))        // [1,2,3,4,5]
console.log(quickSortInPlace([5,2,4,1,3])) // [1,2,3,4,5]
console.log(mergeSort([5,2,4,1,3]))        // [1,2,3,4,5]
console.log(heapSort([5,2,4,1,3]))         // [1,2,3,4,5]

复杂度与最坏:快排平均 O(n log n),最坏(已排序 + 选末位 pivot)O(n²),可随机选 pivot 规避;归并稳定 O(n log n) 但需 O(n) 空间;堆排 O(n log n) 原地但不稳定。

名词解释

课后练习

  1. 快排最坏 O(n²) 怎么避免?
    • 答案:随机选 pivot(或三数取中),让"每次都取到极端值"的概率降到可忽略,实际几乎总是 O(n log n)。
  2. 为什么归并排序稳定而快排不稳定?
    • 答案:归并合并时遇到相等元素优先取左半的,保持原序;快排的分区交换会跨过相等元素,破坏原序。

总结

快排、归并、堆排是"O(n log n) 三杰",理解了它们,排序这一关就真正过了。三者殊途同归都靠"分治"把问题拆小,却各有性格:快排最"快"——平均最快、缓存友好、原地,是工程默认;归并最"稳"——无论数据如何都是稳稳的 O(n log n) 且稳定,适合外部排序;堆排最"省"——原地零额外空间,但不稳定。这里有个重要工程认知:没有"最好"的排序,只有"最合适"的。V8 的 Array.sort 对短数组用插入、长数组用快排变体(TimSort 思路),正是这种"混合策略"的体现。我建议把快排的分区思想(选基准、小于的放左、大于的放右)当作核心套路反复练——它不只用于排序,还在"第 k 大""快速选择"里大显身手。