排序算法全景:冒泡 / 选择 / 插入
本节目标
- 手写冒泡、选择、插入三种基础排序
- 理解它们的稳定性与适用场景
- 建立"排序是算法基本功"的意识
// 运行环境: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]稳定性:相等元素相对顺序不变。冒泡、插入稳定;选择不稳定(交换可能跨过相等元素)。
名词解释
- 稳定性(Stability):排序后,值相等的元素保持原相对顺序。稳定排序在"先按 A 排、再按 B 排"的多级排序中很重要。插入、归并、冒泡稳定;选择、快排、堆排不稳定。
- 时间复杂度(基础三排序):冒泡/选择/插入均为 O(n²),但插入在"近乎有序"时接近 O(n),实际最好用。
- 原地排序(In-place):三者都只用到 O(1) 额外空间,都是原地排序。
课后练习
- 为什么插入排序在"基本有序"的数组上很快?
- 答案:此时内层
while几乎不执行(每个元素已在正确位置附近),比较/搬移极少,趋近 O(n);而冒泡/选择仍要 O(n²)。
- 答案:此时内层
- 选择排序为什么不稳定?
- 答案:它把"最小值"与前面元素交换,可能把靠后的相等元素换到更前,打破相等元素的原顺序。例 [2a, 2b, 1] 排完变 [1, 2b, 2a]。
总结
基础三排序(冒泡、选择、插入)是算法世界的"字母表"——简单到谁都能看懂,却是理解"比较排序"思想的起点。它们的复杂度都是 O(n²),看起来笨,但各自藏着教训:冒泡教会我们"相邻交换把极值推到边界";选择用"每轮选最小"示范了贪心;插入则揭示"维护一个已排区、把新元素插进去"这个在归并/快排里反复出现的母题。我想纠正一个误区:很多人觉得基础排序没用,因为 Array.sort 一行搞定。但亲手写一遍,你才真正理解"稳定性"这种细微性质,也才接得住后面 O(n log n) 排序的升级。插入排序尤其值得记住:它虽是 O(n²),却在小数组或近乎有序时比快排还快,很多语言的内置 sort 在小数组上就切换到插入排序。