排序效率低?5分钟吃透快速排序,性能飙升至O(nlogn)

日常开发中,你是不是还在纠结数组排序的效率问题?冒泡、选择、插入排序动辄O(n²)的时间复杂度,处理稍大数据量就卡顿;想上手快排又被「分治」「双指针」这些概念绕晕,看完理论还是写不出能跑的代码?

这篇文章,我会用最通俗的语言拆解快速排序核心逻辑,从实战场景出发,带你手写两种可直接运行的快排代码,避开新手常踩的坑,彻底搞懂为什么快排能做到平均O(nlogn)的高效排序!

一、先搞懂:快速排序到底快在哪?

在讲具体代码前,先明确快排的核心优势,这也是它能成为开发中最常用排序算法的原因:

  • 分治策略:把一个大数组排序问题,拆成「基准值左边」「基准值右边」两个小数组的排序问题,递归解决,大幅缩小问题规模;
  • 原地交换:无需额外开辟大量数组空间(仅递归栈占用O(logn)空间),双指针交换元素,减少内存开销;
  • 时间效率:平均时间复杂度O(nlogn),远优于冒泡/选择/插入排序的O(n²),处理10万级数据也不慌。

快速排序核心思想(一句话讲透)

选一个「基准值(pivot)」,把数组分成两部分:比基准值小的放左边,比基准值大的放右边,然后递归对左右两部分重复这个操作,直到整个数组有序。

二、实战场景:给无序数组高效排序

假设我们有一个电商订单金额数组 [5, 3, 8, 1, 9, 2, 7, 4, 6],需要快速按金额从小到大排序,这就是快排最典型的实战场景。

下面给出两种常用的快排实现方式,可直接复制运行!

实现方式1:简洁版(易理解,新手首选)

这种方式用额外数组存储左右部分,代码逻辑直观,适合入门理解核心思想。

javascript 复制代码
/**
 * 快速排序(简洁版)
 * 核心:选基准值,拆分数组,递归合并
 * 时间复杂度:平均 O(nlogn),最坏 O(n²)
 * 空间复杂度:O(n)(额外数组)
 */
function quickSort(arr) {
  // 递归终止条件:数组长度≤1时直接返回
  if (arr.length <= 1) return arr;

  const pivot = arr[0]; // 取第一个元素作为基准值
  const left = [];      // 存比基准值小的元素
  const right = [];     // 存比基准值大的元素

  // 遍历数组,拆分左右
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < pivot) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }

  // 递归排序左右数组,再合并结果
  return [...quickSort(left), pivot, ...quickSort(right)];
}

// 测试代码(直接运行)
const arr = [5, 3, 8, 1, 9, 2, 7, 4, 6];
console.log('排序前:', arr);
console.log('排序后:', quickSort(arr)); // 输出:[1,2,3,4,5,6,7,8,9]

实现方式2:原地交换版(性能更优,生产常用)

简洁版需要额外数组,原地交换版通过双指针操作,无需额外空间,性能更优,是实际开发中更推荐的写法。

javascript 复制代码
/**
 * 快速排序(原地交换版)
 * 核心:双指针+原地交换,减少内存开销
 * 时间复杂度:平均 O(nlogn),最坏 O(n²)
 * 空间复杂度:O(logn)(仅递归栈)
 */
// 分区函数:找到基准值最终位置,返回其索引
function partition(arr, left, right) {
  const pivot = arr[left]; // 取左边界为基准值
  let i = left, j = right; // 左右双指针

  // 双指针未相遇时循环
  while (i < j) {
    // 右指针向左找:第一个比基准值小的元素
    while (i < j && arr[j] >= pivot) {
      j--;
    }
    // 找到后,把该元素放到左指针位置,左指针右移
    if (i < j) {
      arr[i] = arr[j];
      i++;
    }

    // 左指针向右找:第一个比基准值大的元素
    while (i < j && arr[i] <= pivot) {
      i++;
    }
    // 找到后,把该元素放到右指针位置,右指针左移
    if (i < j) {
      arr[j] = arr[i];
      j--;
    }
  }

  // 基准值放到最终位置
  arr[i] = pivot;
  return i; // 返回基准值索引
}

// 递归排序函数
function quickSort(arr, left, right) {
  // 递归终止条件:左右边界重合/交叉
  if (left >= right) return;
  // 分区,得到基准值索引
  const pivotIndex = partition(arr, left, right);
  // 递归排序左半部分
  quickSort(arr, left, pivotIndex - 1);
  // 递归排序右半部分
  quickSort(arr, pivotIndex + 1, right);
}

// 测试代码(直接运行)
const arr2 = [5, 3, 8, 1, 9, 2, 7, 4, 6];
console.log('排序前:', arr2);
quickSort(arr2, 0, arr2.length - 1); // 注意:原地排序会修改原数组
console.log('排序后:', arr2); // 输出:[1,2,3,4,5,6,7,8,9]

三、踩坑提醒(新手必看)

  1. 基准值选择坑 :如果始终选数组第一个元素,当数组已经有序时,快排会退化成O(n²)! ✅ 解决方案:随机选基准值(比如 const pivot = arr[Math.floor(Math.random()*(right-left+1)) + left]),避免最坏情况。
  2. 相等元素处理坑 :快排是「不稳定排序」,相等元素的相对位置可能颠倒(比如数组 [2, 1, 2],排序后第二个2可能跑到第一个2前面)。 ✅ 解决方案:如果需要稳定排序,可改用归并排序;或在分区时把等于基准值的元素均匀分到左右两边。
  3. 递归栈溢出坑:处理超大数组时,递归深度过深可能导致栈溢出。 ✅ 解决方案:改用非递归实现(用栈模拟递归),或限制递归深度。
  4. 原地排序修改原数组 :原地交换版会直接修改原数组,若需保留原数组,排序前先复制(const newArr = [...arr])。

四、核心知识点总结

  1. 快排核心:分治思想(分:拆分数组;治:递归排序;合:无需合并,原地排序)+ 基准值 + 双指针;
  2. 效率关键:基准值选得好(随机选),能让数组均匀拆分,时间复杂度接近O(nlogn);
  3. 版本选择
    • 新手入门:先写简洁版,理解核心逻辑;
    • 生产环境:用原地交换版,减少内存开销;
  4. 特性:平均效率高、原地排序,但不稳定,适合对性能要求高、无需稳定排序的场景(如大数据量数组排序)。

最后

快速排序是前端/后端开发中高频考察、高频使用的排序算法,吃透它不仅能解决实际开发中的排序问题,也能加深对分治、递归思想的理解。

相关推荐
OpenTiny社区1 小时前
🎨 看完 GenUI SDK 源码我悟了!
前端·vue.js·github
叁两1 小时前
前端转型AI Agent该如何学习?(前置篇)
前端·人工智能·node.js
何时梦醒2 小时前
深入理解递归与快速排序 —— 从基础入门到手写实现
前端·javascript
爱勇宝2 小时前
淡泊名利之前,先承认我们都很焦虑
前端·后端·程序员
bonechips2 小时前
LLM 的无状态:从 HTTP 协议到对话上下文工程
前端·javascript
杨利杰YJlio2 小时前
Codex桌面客户端上手:项目、插件与自动化实战
前端·后端
胡志辉2 小时前
从 prototype 到 V8,看懂 JavaScript 原型链
前端·javascript
ricardo19732 小时前
React 渲染优化:memo / useMemo / useCallback 的正确姿势与并发模式实战
前端·面试
ClouGence2 小时前
零代码自动化测试:手把手教你录出一条能反复用的测试用例
前端·测试