面试必考:快速排序详解,我用费曼学习法讲透了(含完整代码)

37 阅读9分钟

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 快速排序,到底哪个更好?

我会详细对比两种排序算法, 帮你理解何时选择哪种。

敬请期待!  🚀