6 大经典排序,一步一步看图理解——每个算法配有 SVG 动态图解 + 生活类比 + 代码对照
🎯 想象一杯碳酸饮料:小气泡从杯底慢慢往上浮,越大的气泡浮得越快。
🔢 数组就是一杯水:每一轮,拿相邻两个数比较,大的往右"冒"(交换),一轮下来最大的就"浮"到了最右端。
🔁 重复 n-1 轮:每轮把当前最大的泡泡送到它该在的位置。
最好 O(n) — 已经有序 平均 O(n²) 最差 O(n²) — 逆序 空间 O(1)
✅ 稳定排序:相等元素不交换,相对顺序不变
💡 最好 O(n) 需加优化:如果某轮没有任何交换,说明已有序,直接退出
🎯 一篮苹果排排坐:你想按大小摆好,怎么选?
🔍 第一轮:扫一遍所有苹果,挑出最小的那个,放到第 1 个位置。
🔍 第二轮:从剩下的苹果里,再挑最小的,放到第 2 个位置。
🔁 重复 n-1 次:每次只选一个最小值放到前面,直到全部排好。
💡 和冒泡的区别:冒泡是"相邻交换",选择是"找到最小再一次性交换",每轮最多交换 1 次。
最好 O(n²) 平均 O(n²) 最差 O(n²) 空间 O(1)
❌ 不稳定排序:例如 [5, 5, 3],交换 5 和 3 时两个 5 的相对顺序可能改变
💡 不管数据是否有序,比较次数都一样,永远 O(n²)——因为每轮都要全部扫描一遍
❌ 数据量大时(O(n²) 太慢)
❌ 需要稳定排序时(选择排序不稳定)
❌ 数据几乎有序时(选择排序无法利用"几乎有序"的优势)
🎴 你手里已经有几张牌:3、5、8,按顺序排好。
🆕 摸到一张新牌"1":从右往左比,8>1→5>1→3>1→插到最前面!
🆕 再摸到"6":从右往左比,8>6→5<6→插到 5 和 8 之间!
💡 关键思想:左侧始终是有序区,新元素从右往左找到自己该插入的位置。
最好 O(n) — 已经有序 平均 O(n²) 最差 O(n²) — 逆序 空间 O(1)
✅ 稳定排序:相等元素不移动,相对顺序不变
💡 插入排序是对"几乎有序"数据最快的 O(n²) 排序——如果数据本身基本有序,while 循环几乎不执行,接近 O(n)
✅ JavaScript 引擎的 Array.prototype.sort() 对小数组用插入排序
✅ TimSort(Python/JS 默认排序)用插入排序处理小片段
✅ 适合数据量小或几乎有序的场景
📏 老师喊:1米7的站出来当标杆! 比1米7矮的站左边,高的站右边。
🔁 左边那组再来一次:1米5的当标杆,矮的左边,高的右边。
🔁 右边那组也来一次:1米8的当标杆……
✅ 每组只剩1个人时:排好了!所有人从左到右就是按身高排的。
💡 关键操作 = 分区(partition):选标杆 + 分两边 = 一轮分区
最好 O(n log n) — 每次均分 平均 O(n log n) 最差 O(n²) — 每次选到最大/最小 空间 O(log n) — 递归栈
❌ 不稳定排序:分区时交换可能改变相等元素的相对顺序
如果每次 pivot 都选到最大/最小值,就变成 O(n²)——等于每个元素都要和所有其他元素比较一遍!
💡 解决方案:① 随机选 pivot ② 三数取中(arr[low]+arr[mid]+arr[high]的中位数)③ 小段切插入排序
📝 老师收了两叠试卷:A 叠 [1,4,7],B 叠 [2,3,9],各叠已经按学号排好。
🔗 怎么合并成一叠?拿 A 的第一张和 B 的第一张比,小的放下来,再比下一张……
1<2→放1,4>2→放2,4>3→放3,4<9→放4,7<9→放7,最后放9 → [1,2,3,4,7,9]
💡 归并排序 = 先拆 + 再合:拆到每组1个(天然有序),再两两合并。
最好 O(n log n) 平均 O(n log n) 最差 O(n log n) 空间 O(n) — 需要辅助数组
✅ 稳定排序:合并时相等元素先取左边的,相对顺序不变
💡 归并排序的复杂度与数据分布无关——永远 O(n log n),不会退化!
✅ JavaScript 的 Array.prototype.sort() 在 V8 引擎中使用 TimSort(归并+插入的混合)
✅ 需要稳定排序且保证 O(n log n) 时的首选
✅ 适合链表排序(不需要随机访问,归并天然适合)
✅ 外部排序(数据大到内存放不下)的核心算法
🏢 公司是一棵树:CEO 在最顶端(根节点),下面是各部门经理(子节点)。
📋 大顶堆规则:每个经理的能力 ≤ 他上级的能力。所以 CEO 一定是能力最强的人。
🏆 选最牛的人:CEO(堆顶)就是最大值!拿走他。
🔁 谁顶上来?:把最底层最后一个人放堆顶,然后"下沉"——让他和下属比,不如就换下来,直到合适的位置。
💡 堆排序 = 建堆 + 循环(取堆顶 → 末尾元素上堆顶 → 下沉调整)
最好 O(n log n) 平均 O(n log n) 最差 O(n log n) 空间 O(1)
❌ 不稳定排序:下沉操作可能改变相等元素的相对顺序
💡 建堆过程本身是 O(n),不是 O(n log n)!这是一个常见的面试陷阱题
直觉上以为每个元素下沉 O(log n) × n 个 = O(n log n),但其实:底层节点多但下沉少,顶层节点少但下沉多。数学证明总比较次数 ≈ 2n,所以建堆是 O(n)。
| 算法 | 最好 | 平均 | 最差 | 空间 | 稳定 | 一句话总结 |
|---|---|---|---|---|---|---|
| 冒泡排序 | O(n) | O(n²) | O(n²) | O(1) | ✅ | 入门必学,实战不用 |
| 选择排序 | O(n²) | O(n²) | O(n²) | O(1) | ❌ | 交换次数最少,不稳定 |
| 插入排序 | O(n) | O(n²) | O(n²) | O(1) | ✅ | 小数据/几乎有序最快 |
| 快速排序 | O(n log n) | O(n log n) | O(n²) | O(log n) | ❌ | 实战最常用,面试必考 |
| 归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | ✅ | 稳定+保证 O(n log n) |
| 堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | ❌ | 原地+保证 O(n log n) |
冒泡选择插入 → O(n²) 三兄弟
快归堆 → O(n log n) 三剑客
稳定排序 → 冒泡 ✅ 插入 ✅ 归并 ✅
不稳定排序 → 选择 ❌ 快排 ❌ 堆排 ❌
空间 O(1) → 冒泡、选择、插入、堆排(原地排序)
空间 O(n) → 归并(需要辅助数组)
快排的常数因子更小:
① 快排是原地排序(不需要辅助数组),归并每次合并都要拷贝 → 快排缓存更友好
② 快排的分区操作是顺序扫描(CPU 缓存命中率高),归并是两个数组交替比较 → 快排实际运行更快
③ 虽然最差 O(n²),但随机化 pivot 后概率极低,平均 O(n log n) 的常数项约是归并的 60%~70%
V8 引擎(Chrome/Node.js)→ TimSort(归并+插入的混合排序)
SpiderMonkey(Firefox)→ 归并排序的变体
💡 为什么用 TimSort?因为需要稳定排序——equals() 相等的对象不应改变顺序,快排不稳定。
稳定性:如果两个元素值相等,排序后它们的相对顺序和排序前一样,就是稳定的。
举例:学生列表先按分数排序,再按班级排序。如果排序稳定,同一个班的学生仍保持分数有序。
💡 什么时候重要:多次排序、数据库 ORDER BY 多列、对象排序时。
① 随机化 pivot:避免最差情况(已排序/逆序数组退化到 O(n²))
② 三数取中:取 arr[low]、arr[mid]、arr[high] 的中位数当 pivot
③ 小段切插入排序:子数组长度较小时用插入排序(TimSort 就这么做的)
④ 三路快排:大量重复元素时,把 =pivot 的元素集中到中间,避免重复比较
⑤ 尾递归优化:较短的子数组递归,较长的用循环 → 递归深度降到 O(log n)
① 缓存不友好:堆排序是跳跃式访问(父→子:i→2i+1),CPU 缓存命中率低
② 常数项大:堆排序的 swap 次数比快排多
③ 不稳定:和快排一样不稳定,但比快排慢,优势只剩"保证 O(n log n)"
💡 但堆排序在Top-K 问题(只需前 K 大/小)中非常有用——建堆 O(n) + 取 K 次 O(K log n)
直觉:n 个节点,每个最多下沉 log n 层 → O(n log n)?❌
真相:底层 n/2 个节点只下沉 0~1 层,越往上节点越少但下沉越多。
数学:∑(h=0→log n) h × n/2^(h+1) = n × ∑(h/2^(h+1)) ≈ 2n → O(n)
💡 面试时说"从最后一个非叶节点往前,每个节点下沉,总比较次数约 2n,所以是 O(n)"即可。
必须能手写:快排(partition)、归并(merge)
必须能说清:冒泡(最简单)、插入(小数据最快)、堆排(Top-K)
必须能对比:6 种排序的时间/空间/稳定性 + Array.prototype.sort 底层
加分项:快排优化(三路、随机化、三数取中)、Top-K 问题、外部排序
图解算法与模式 · 排序算法图解 | 6 大排序 + SVG 图解 + 生活类比 + 代码对照