排序算法全景:冒泡 / 选择 / 插入

本节目标

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

// 冒泡:相邻比较,大的往后冒;每轮把最大值推到末尾
function bubbleSort(a) {
  a = a.slice() // 不破坏原数组
  for (let i = 0; i < a.length; i++)
    for (let j = 0; j < a.length - 1 - i; j++)
      if (a[j] > a[j + 1]) [a[j], a[j + 1]] = [a[j + 1], a[j]]
  return a
}

// 选择:每轮选最小值,放到已排区末尾
function selectionSort(a) {
  a = a.slice()
  for (let i = 0; i < a.length; i++) {
    let min = i
    for (let j = i + 1; j < a.length; j++) if (a[j] < a[min]) min = j
    [a[i], a[min]] = [a[min], a[i]]
  }
  return a
}

// 插入:像理牌,把当前元素插到前面已排区的正确位置
function insertionSort(a) {
  a = a.slice()
  for (let i = 1; i < a.length; i++) {
    const key = a[i]
    let j = i - 1
    while (j >= 0 && a[j] > key) { a[j + 1] = a[j]; j-- }
    a[j + 1] = key
  }
  return a
}

console.log(bubbleSort([5, 2, 4, 1, 3]))     // [1,2,3,4,5]
console.log(selectionSort([5, 2, 4, 1, 3]))   // [1,2,3,4,5]
console.log(insertionSort([5, 2, 4, 1, 3]))   // [1,2,3,4,5]

稳定性:相等元素相对顺序不变。冒泡、插入稳定;选择不稳定(交换可能跨过相等元素)。

名词解释

课后练习

  1. 为什么插入排序在"基本有序"的数组上很快?
    • 答案:此时内层 while 几乎不执行(每个元素已在正确位置附近),比较/搬移极少,趋近 O(n);而冒泡/选择仍要 O(n²)。
  2. 选择排序为什么不稳定?
    • 答案:它把"最小值"与前面元素交换,可能把靠后的相等元素换到更前,打破相等元素的原顺序。例 [2a, 2b, 1] 排完变 [1, 2b, 2a]。

总结

基础三排序(冒泡、选择、插入)是算法世界的"字母表"——简单到谁都能看懂,却是理解"比较排序"思想的起点。它们的复杂度都是 O(n²),看起来笨,但各自藏着教训:冒泡教会我们"相邻交换把极值推到边界";选择用"每轮选最小"示范了贪心;插入则揭示"维护一个已排区、把新元素插进去"这个在归并/快排里反复出现的母题。我想纠正一个误区:很多人觉得基础排序没用,因为 Array.sort 一行搞定。但亲手写一遍,你才真正理解"稳定性"这种细微性质,也才接得住后面 O(n log n) 排序的升级。插入排序尤其值得记住:它虽是 O(n²),却在小数组或近乎有序时比快排还快,很多语言的内置 sort 在小数组上就切换到插入排序。