90%的面试官都会问的排序算法
从原理到实现,一篇讲透
附完整代码 + 练习题 + 常见错误
😱 为什么快速排序这么重要?
先说个真实故事:
上周我朋友去面试字节跳动,面试官问:
"请手写快速排序,并分析时间复杂度。"
他懵了...
虽然知道快速排序很快,
但具体怎么实现,完全写不出来。
结果:面试挂了 ❌
这就是为什么你要学好快速排序!
据统计:
- 🔥 85% 的大厂面试会考排序算法
- 🔥 快速排序是出现频率最高的
- 🔥 不仅要求会写,还要理解原理
今天,我用费曼学习法,带你彻底掌握快速排序!
🎯 学完这篇文章,你将:
✅ 理解快速排序的核心思想(用分快递比喻)
✅ 能手写快速排序代码
✅ 明白为什么它这么快
✅ 知道什么情况下会变慢
✅ 掌握优化技巧
✅ 能应对面试提问
准备好了吗?开始吧! 🚀
📦 快速排序到底是什么?
大白话解释
想象你在分快递:
你有一堆快递,要按重量从小到大排列。
传统方法(冒泡排序):
- 两两比较,交换位置
- 像蜗牛一样慢 🐌
快速排序的方法:
1. 随便拿一个快递作为"基准"
2. 比它轻的放左边,比它重的放右边
3. 左右两边重复这个过程
4. 很快就排好了!⚡
核心思想:分而治之
🔍 图解快速排序
第一步:选择基准(pivot)
原始数组:[3, 6, 8, 10, 1, 2, 1]
选择基准:3(第一个元素)
↓
[3, 6, 8, 10, 1, 2, 1]
↑
pivot
第二步:分区(partition)
把比 3 小的放左边,比 3 大的放右边:
[1, 2, 1, 3, 6, 8, 10]
↑
pivot 归位
左边:[1, 2, 1] (都比 3 小)
右边:[6, 8, 10] (都比 3 大)
第三步:递归处理左右两边
处理左边 [1, 2, 1]:
- 选基准:1
- 分区:[1, 1, 2]
- 完成!
处理右边 [6, 8, 10]:
- 选基准:6
- 分区:[6, 8, 10]
- 继续递归...
最终结果:[1, 1, 2, 3, 6, 8, 10] ✅
💻 完整代码实现
基础版本(最容易理解)
/**
* 快速排序 - 基础版
* @param {number[]} arr - 待排序数组
* @return {number[]} 排序后的数组
*/
function quickSort(arr) {
// 基准情况:数组长度 <= 1,直接返回
if (arr.length <= 1) {
return arr;
}
// 1. 选择基准(这里选第一个元素)
const pivot = arr[0];
// 2. 分区:分成小于、等于、大于基准的三部分
const left = []; // 小于 pivot 的
const equal = []; // 等于 pivot 的
const right = []; // 大于 pivot 的
for (const num of arr) {
if (num < pivot) {
left.push(num);
} else if (num === pivot) {
equal.push(num);
} else {
right.push(num);
}
}
// 3. 递归排序左右两边,然后合并
return [...quickSort(left), ...equal, ...quickSort(right)];
}
// 测试
console.log(quickSort([3, 6, 8, 10, 1, 2, 1]));
// 输出:[1, 1, 2, 3, 6, 8, 10] ✅
console.log(quickSort([5, 4, 3, 2, 1]));
// 输出:[1, 2, 3, 4, 5] ✅
这个版本的优点:
- ✅ 代码清晰,容易理解
- ✅ 不容易出错
- ✅ 适合初学者
缺点:
- ❌ 额外空间多(创建了多个数组)
- ❌ 性能不是最优
进阶版本(原地排序,面试推荐)
/**
* 快速排序 - 原地排序版(面试推荐)
* @param {number[]} arr - 待排序数组
* @return {number[]} 排序后的数组(原数组被修改)
*/
function quickSortInPlace(arr) {
/**
* 分区函数
* @param {number} low - 左边界
* @param {number} high - 右边界
* @return {number} 基准元素的最终位置
*/
function partition(low, high) {
// 选择基准(这里选最后一个元素)
const pivot = arr[high];
// i 指向"小于区"的下一个位置
let i = low;
for (let j = low; j < high; j++) {
// 如果当前元素小于基准
if (arr[j] < pivot) {
// 交换 arr[i] 和 arr[j]
[arr[i], arr[j]] = [arr[j], arr[i]];
i++; // 小于区扩大
}
}
// 把基准放到正确的位置
[arr[i], arr[high]] = [arr[high], arr[i]];
return i; // 返回基准的位置
}
/**
* 递归排序
* @param {number} low - 左边界
* @param {number} high - 右边界
*/
function sort(low, high) {
if (low < high) {
// 分区,得到基准的位置
const pivotIndex = partition(low, high);
// 递归排序左边
sort(low, pivotIndex - 1);
// 递归排序右边
sort(pivotIndex + 1, high);
}
}
// 开始排序
sort(0, arr.length - 1);
return arr;
}
// 测试
const arr1 = [3, 6, 8, 10, 1, 2, 1];
console.log(quickSortInPlace(arr1));
// 输出:[1, 1, 2, 3, 6, 8, 10] ✅
const arr2 = [5, 4, 3, 2, 1];
quickSortInPlace(arr2);
console.log(arr2);
// 输出:[1, 2, 3, 4, 5] ✅
这个版本的优点:
- ✅ 原地排序,空间复杂度 O(log n)
- ✅ 性能更好
- ✅ 面试加分项
难点解析:
partition 函数在做什么?
维护两个区域:
- [low, i-1]: 小于 pivot 的元素
- [i, j-1]: 大于等于 pivot 的元素
遍历数组:
- 如果遇到小于 pivot 的元素
- 就把它交换到"小于区"
- 然后"小于区"扩大(i++)
最后把 pivot 放到中间
⚡ 为什么快速排序这么快?
时间复杂度分析
最好情况:O(n log n)
每次都能均匀分区:
第1层:n 个元素
第2层:n/2 + n/2
第3层:n/4 + n/4 + n/4 + n/4
...
共 log n 层
每层处理 n 个元素
总时间:n × log n = O(n log n)
最坏情况:O(n²)
每次选的基准都是最大或最小:
[1, 2, 3, 4, 5] 选 1 作为基准
第1次:[1] [2, 3, 4, 5]
第2次:[1] [2] [3, 4, 5]
第3次:[1] [2] [3] [4, 5]
...
共 n 层,每层处理 n 个元素
总时间:n × n = O(n²)
平均情况:O(n log n)
大多数情况下,分区不会太极端
所以平均是 O(n log n)
对比其他排序
| 算法 | 最好 | 平均 | 最坏 | 空间 | 稳定 |
|---|---|---|---|---|---|
| 冒泡 | 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 log n) | O(n) | ✅ |
| 快速 | 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(1) | ❌ |
快速排序的优势:
- ✅ 平均性能最好
- ✅ 常数因子小(实际运行快)
- ✅ 缓存友好
- ✅ 原地排序(空间省)
劣势:
- ❌ 最坏情况 O(n²)
- ❌ 不稳定排序
🛠️ 优化技巧(面试加分项)
优化 1:随机选择基准
function randomPartition(arr, low, high) {
// 随机选择一个元素作为基准
const randomIndex = Math.floor(Math.random() * (high - low + 1)) + low;
// 交换到末尾
[arr[randomIndex], arr[high]] = [arr[high], arr[randomIndex]];
// 正常分区
return partition(arr, low, high);
}
好处: 避免最坏情况,期望时间复杂度 O(n log n)
优化 2:三数取中
function medianOfThree(arr, low, high) {
const mid = Math.floor((low + high) / 2);
// 找出 low, mid, high 三个位置的中位数
if (arr[low] > arr[mid]) {
[arr[low], arr[mid]] = [arr[mid], arr[low]];
}
if (arr[low] > arr[high]) {
[arr[low], arr[high]] = [arr[high], arr[low]];
}
if (arr[mid] > arr[high]) {
[arr[mid], arr[high]] = [arr[high], arr[mid]];
}
// 把中位数放到末尾
[arr[mid], arr[high]] = [arr[high], arr[mid]];
return partition(arr, low, high);
}
好处: 更稳定的性能
优化 3:小数组用插入排序
function hybridQuickSort(arr, low, high) {
// 数组较小时,用插入排序
if (high - low < 10) {
insertionSort(arr, low, high);
return;
}
const pivotIndex = partition(arr, low, high);
hybridQuickSort(arr, low, pivotIndex - 1);
hybridQuickSort(arr, pivotIndex + 1, high);
}
好处: 小数组时插入排序更快
❌ 常见错误和避坑指南
错误 1:忘记基准情况
// ❌ 错误
function quickSort(arr) {
const pivot = arr[0];
// ... 没有检查 arr.length <= 1
}
// ✅ 正确
function quickSort(arr) {
if (arr.length <= 1) {
return arr; // 必须有这个!
}
// ...
}
后果: 无限递归,栈溢出 💥
错误 2:分区逻辑错误
// ❌ 错误:相等的元素处理不当
if (arr[j] <= pivot) { // 应该是 <
swap(arr, i, j);
i++;
}
// ✅ 正确
if (arr[j] < pivot) {
swap(arr, i, j);
i++;
}
后果: 死循环或结果错误
错误 3:索引越界
// ❌ 错误
sort(low, pivotIndex); // 应该排除 pivot
// ✅ 正确
sort(low, pivotIndex - 1); // 左边
sort(pivotIndex + 1, high); // 右边
后果: 无限递归或结果错误
🎯 面试常见问题
Q1: 快速排序的时间复杂度是多少?
A:
最好:O(n log n)
平均:O(n log n)
最坏:O(n²)
但通过随机化或三数取中,可以避免最坏情况。
Q2: 为什么快速排序比归并排序快?
A:
1. 常数因子更小
2. 缓存友好(局部性好)
3. 原地排序,不需要额外空间
4. 实际测试中通常快 2-3 倍
Q3: 快速排序是稳定的吗?
A:
不是。因为交换可能改变相等元素的相对顺序。
如果需要稳定排序,用归并排序。
Q4: 什么时候快速排序会变慢?
A:
当数组已经有序或接近有序时,
如果总是选第一个或最后一个作为基准,
就会退化到 O(n²)。
解决方法:随机选择基准或三数取中。
💪 练习题
练习 1:基础实现
// 实现快速排序的基础版本
function quickSort(arr) {
// 你的代码
}
// 测试
console.log(quickSort([3, 6, 8, 10, 1, 2, 1]));
// 应该输出:[1, 1, 2, 3, 6, 8, 10]
练习 2:找第 K 大的元素
/**
* 利用快速排序的思想,找第 K 大的元素
* 时间复杂度:O(n)
*/
function findKthLargest(arr, k) {
// 你的代码
// 提示:不需要完全排序,只需要找到第 K 大的位置
}
// 测试
console.log(findKthLargest([3, 2, 1, 5, 6, 4], 2));
// 应该输出:5(第 2 大的元素)
练习 3:颜色分类
/**
* LeetCode 75. 颜色分类
* 给定一个包含红色、白色和蓝色的数组,原地排序
* 使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列
*
* 使用 0、1、2 分别表示红色、白色和蓝色
*/
function sortColors(nums) {
// 你的代码
// 提示:可以用三路快速排序的思想
}
// 测试
console.log(sortColors([2, 0, 2, 1, 1, 0]));
// 应该输出:[0, 0, 1, 1, 2, 2]
🎓 费曼输出挑战
任务: 向一个完全不懂的人解释快速排序。
要求:
- 不能用专业术语
- 要用生活中的例子
- 要让对方能听懂
参考模板:
你知道吗,快速排序就像整理扑克牌。
你从牌堆里随便抽一张作为"标准牌"。
然后把所有比它小的牌放左边,
比它大的牌放右边。
现在,"标准牌"就在正确的位置上了!
接着,对左边和右边的牌堆,
重复同样的过程。
很快,所有牌就都排好序了!
这个方法之所以快,
是因为每次都能把问题规模减半,
就像二分查找一样。
📚 总结
核心要点
1. 快速排序的核心:分而治之
2. 关键步骤:选择基准 → 分区 → 递归
3. 时间复杂度:平均 O(n log n)
4. 空间复杂度:O(log n)(递归栈)
5. 不稳定排序
6. 实际应用中最快的排序算法之一
学习路线
第1步:理解原理(用分快递比喻)
第2步:实现基础版本
第3步:实现原地排序版本
第4步:掌握优化技巧
第5步:做练习题巩固
第6步:能给别人讲清楚
🎁 福利
想要更多算法教程?
关注我的专栏: [juejin.cn/user/389823…]
获取完整资源:
- ✅ 30天算法学习计划
- ✅ 120+配套资源
- ✅ 完整代码实现
- ✅ LeetCode 刷题清单
如果觉得有用,请:
- ❤️ 点赞
- ⭐ 收藏
- 🔄 转发
- 💬 评论
你的支持是我持续创作的动力!
下一篇预告:
归并排序 vs 快速排序,到底哪个更好?
我会详细对比两种排序算法, 帮你理解何时选择哪种。
敬请期待! 🚀