快排

快速排序

快速排序背后采用的也是分治的思想。

核心思想

  1. 选择基准元素
    从数组中 随机选择 一个元素作为基准值(pivot)。
  2. 分区操作
    将数组中所有小于基准的元素移动到基准的左侧,大于基准的元素移动到右侧。这样,基准就处于它排序后应在的位置上。整个序列就变成了 [比基准小的值] 基准值 [比基准大的值]
  3. 递归排序
    对基准左右两边的子数组分别重复上述步骤,直到所有子数组都有序。

举个例子:

假设我们有 [3, 5, 8, 1, 2, 9, 4, 7] 这个序列,这里选择 4 来作为基准值

image-20250305160714044

接下来从头到尾,把小于 4 的放到左边,大于 4 的放到右边,如下图所示:

image-20250305161013505

接下来对基准值左边和右边进行相同的操作即可。

好了,这是关于快速排序的最基本的思想。下面是一个快速排序整体框架的示例代码:

// 分治函数
function partition(array, left, right){
  // todo
}

// 这是入口函数
function quickSort(array){
 	function QuickSort(array, left, right){
    if(left < right){
      // 该方法内部,会选择一个元素作为基准值
      // 然后将所有小于基准值的元素,放到基准值左边,所有大于基准值的元素,放到基准值右边
      // 最后会返回基准值的索引
      let index = partition(array, left, right);
      // 拿到基准值之后,再对数组进行切割,对左右两边的子数组做相同的操作
      QuickSort(array, left, index - 1);
      QuickSort(array, index + 1, right);
    }
  }
  QuickSort(array, 0, array.length - 1);
}

const arr = [3, 5, 8, 1, 2, 9, 4, 7];
quickSort(arr);

快速排序的 partition 方法的实现有多种方式,例如:

  1. 左右指针法
  2. 挖坑法

这里我们就介绍一个最常用的左右指针法。

左右指针法

使用左右指针法实现 partition 方法的步骤如下:

  1. 选取某个元素作为 pivot 基准值,一般取当前数组的第一个元素或者最后一个元素,这里我们采用 最后一个元素
  2. 从 left 一直 向后寻找,直到找到一个大于 pivot 的值,然后 right 从后往前找,找到一个小于 pivot 的值,然后交换这两个元素的位置
  3. 重复第 2 步,直到 left 和 right 相遇,这时将 pivot 放置在 left 的位置即可。

下面是一个具体的图解:

(1)原始数组,left 一开始指向第一个元素,right 一开始指向倒数第二个元素,最后一个元素是基准值

image-20250312075121706

(2)left 从左往右走,走到 7 的位置就停下了,接下来 right 从右往左走,一开始在 3 的位置就停下了。然后两者进行交换

image-20250312075228279

(3)left 继续往右走,走到 6 的位置停下,right 走到 0 的位置,然后停下,两者进行交换

image-20250312075446279

(4)left 继续走,走到 9 停下来,right 继续走,走到 2 停下来,进行交换

image-20250312100358576

(5)接下来 left 继续往右走,此时 left >= right 了,说明第一轮就走完了,将基准值和 array[left]进行交换

image-20250312100808750

然后走完一轮之后,基准值左边的元素都是比基准值小的,基准值右边的元素都是比基准值大的。

代码实现

// 分治函数
function partition(array, left, right) {
  // 首先是找一个基准值,我们取最后一个元素作为基准值
  let pivot = array[right];
  let pivotIndex = right; // 基准值对应的下标
  while (left < right) {
    // 左边的left一直往右走
    while (left < right && array[left] < pivot) {
      left++;
    }
    // 上面跳出 while 说明找到了一个比基准值大的
    // 右边的right一直往左边走
    while (left < right && array[right] >= pivot) {
      right--;
    }
    // 上面跳出 while 说明找到了一个比基准值小的
    [array[left], array[right]] = [array[right], array[left]];
  }
  // 当跳出上面的while 的时候,需要将基准值和left指向的值进行交换
  [array[left], array[pivotIndex]] = [array[pivotIndex], array[left]];
  return left;
}

// 这是入口函数
function quickSort(array) {
  function QuickSort(array, left, right) {
    // 注意下面的条件判断,是判断子数组是否还能够继续拆分
    if (left < right) {
      // 该方法内部,会选择一个元素作为基准值
      // 然后将所有小于基准值的元素,放到基准值左边,所有大于基准值的元素,放到基准值右边
      // 最后会返回基准值的索引
      let index = partition(array, left, right);
      // 拿到基准值之后,再对数组进行切割,对左右两边的子数组做相同的操作
      QuickSort(array, left, index - 1);
      QuickSort(array, index + 1, right);
    }
  }
  QuickSort(array, 0, array.length - 1);
}

const arr = [3, 5, 8, 1, 2, 9, 4, 7];
quickSort(arr);
console.log(arr); 

复杂度

  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(logn)

稳定性

快排是一种 不稳定 排序,因为在分区的过程中,元素的交换可能影响相等元素的原始顺序。

我的实现与问题纠正

  function change(arr, left, right) {
      let tar = arr[right]
      let tarIndex = right
      let start = left
      let end = right - 1

      while (start < end) {
          while (arr[start] > tar && start < end) {
              start ++
          }
          while (arr[end] <= tar && start < end) {
              end --
          }
          [arr[start], arr[end]] = [arr[end], arr[start]]
      }
      [arr[start], arr[tarIndex]] = [arr[tarIndex], arr[start]]
      return start
  }

  function sort(arr) {
      function helper(arr, left, right) {
          if (left < right) {
              let index = change(arr, left, right)

              helper(arr, left, index - 1)
              helper(arr, index + 1, right)
          }
      }
      helper(arr, 0, arr.length - 1)

      return arr
  }

  let arr = [1,4,7,3,6,9,3,2,8]

  console.log(sort(arr))

这样实现的结果是错误的,原因如下

你的代码实现的是快速排序的 分区(partition) 过程,问题出在 end = right - 1 这个初始值上。我们来逐步分析:


问题分析

  1. 你的逻辑

    • 分区时以 arr[right] 为基准值(pivot)。
    • start 从左向右找 大于 pivot 的值,end 从右向左找 小于等于 pivot 的值。
    • 交换 startend,最后将 pivot 放到正确的位置。
  2. 关键问题

    • end = right - 1 时,end 的初始位置是基准值的前一个元素,这会导致:
      • 如果 arr[end] 本身 就是正确的(即 arr[end] <= pivot),end-- 会跳过它,导致分区点计算错误。
      • 最终 startend 的相遇点可能不满足 arr[start] <= pivot,导致基准值被交换到错误的位置。
  3. 为什么 end = right 能修复问题?

    • endright 开始(即包含基准值本身),可以保证:
      • 如果 arr[right] 是最大值,end 会一直减到 start,此时 start 指向的位置就是基准值的正确位置。
      • 分区逻辑更通用,不会漏掉边界情况。

修正后的代码

function change(arr, left, right) {
    let pivot = arr[right]; // 基准值
    let pivotIndex = right; // 基准值原始索引
    let start = left;
    let end = right; // 关键修改:end 从 right 开始,而非 right - 1

    while (start < end) {
        while (arr[start] <= pivot && start < end) start++; // 注意条件改为 <=
        while (arr[end] >= pivot && start < end) end--;     // 注意条件改为 >=
        [arr[start], arr[end]] = [arr[end], arr[start]];
    }
    // 将基准值放到正确位置
    [arr[start], arr[pivotIndex]] = [arr[pivotIndex], arr[start]];
    return start; // 返回分区点
}

function sort(arr) {
    function helper(arr, left, right) {
        if (left < right) {
            let index = change(arr, left, right);
            helper(arr, left, index - 1);
            helper(arr, index + 1, right);
        }
    }
    helper(arr, 0, arr.length - 1);
    return arr;
}

let arr = [1, 4, 7, 3, 6, 9, 3, 2, 8];
console.log(sort(arr)); // 输出正确排序结果

关键修改点

  1. end = right 而非 end = right - 1
    • 确保分区过程能正确处理基准值本身的位置。
  2. 调整比较条件
    • arr[start] <= pivotarr[end] >= pivot:避免重复元素导致死循环。
  3. 分区逻辑的对称性
    • 从两端向中间扫描时,startend 的移动条件需要对称,否则可能导致分区点偏移。

快速排序的分区正确写法(经典Hoare分区)

更常见的分区实现如下(无需单独处理 right - 1):

function partition(arr, left, right) {
    let pivot = arr[right];
    let i = left;
    for (let j = left; j < right; j++) {
        if (arr[j] <= pivot) {
            [arr[i], arr[j]] = [arr[j], arr[i]];
            i++;
        }
    }
    [arr[i], arr[right]] = [arr[right], arr[i]];
    return i;
}

总结

  • 你的原始代码因 end 初始值少了一位,导致分区点计算不准确。
  • 快速排序的分区需要保证 基准值最终位置 的正确性,因此 end 应从 right 开始扫描。
  • 建议使用经典的分区写法(如上),更简洁且不易出错。

-EOF-


快排
http://localhost:8090/archives/kuai-pai
作者
Administrator
发布于
2026年01月30日
许可协议