JavaScript快速排序

1,537 阅读3分钟

目标

从0开始一步一步地实现一个快速排序。并逐步优化成一个通用的快速排序。 (欢迎大家提供建议和帮助)

快速排序的原理

对数组的左右两端用指针标记,取出数组最左侧的元素作为参照数,将数组中小于参照数的置于左侧、大于参照数的置于右侧,此时数组被分为左右两个子数组,再分别对两个子数组进行上述排序,直到子数组长度为1或0。

代码实现

下面的排序函数只是按照最初的思路进行实现,后面会进行优化。

function quickSort(arr) {
  if (arr < 2) {
    return arr;
  }
  const length = arr.length;
  let left = 0;
  let right = length - 1;
  const reference = arr[0];
  let slotInLeft = true;

  // 假设排序结果为递增数组
  // 5. 最后的情况是,两个指针距离为1,一个指向待比较元素,一个指向空槽。
  // 5.1 待比较元素较大,放入空槽,原始空槽指针移动到原始待比较元素位置(新空槽),两指针相遇。
  // 5.2 待比较元素较小,它的指针移动到空槽,两指针相遇。
  // 所以一轮排序的终止条件为,两指针相遇(即left===right)(排序中的状态:left的值应该小于right)
  while (left !== right) {
    // 6. 对空槽位置不同情况的判断
    if (slotInLeft) {
      // 1. 比较参考数和待排序元素
      if (reference > arr[right]) {
        // 2. 对待排序元素排序,放入空槽
        arr[left] = arr[right];
        // 3. 更新空槽标记,空槽从左侧变为右侧 (一开始默认空槽在左侧,但是漏掉了对空槽位置不同情况的判断[6])
        slotInLeft = false;
        // 4. 开始下一次比较,左侧索引left已经排好序,继续从左向右扫描(此时发现需要添加**扫描的终止条件**)
        left++;
      } else {
        // 右侧值大,保持不动,继续从右向左扫描
        right--;
      }
    } else {
      // 7. 当前空槽在右侧,待排序元素则在左侧。此时需要特别注意,在空槽在左侧的情况没有对“与参考数相等”的情况做处理(相当于默认相等相等时,右侧元素保持不动),所以在此必须处理这种情况,左侧元素相等时将移动到右侧
      if (reference <= arr[left]) {
        arr[right] = arr[left];
        slotInLeft = true;
        right--;
      } else {
        left++;
      }
    }
  }
  // 8. 一轮排序结束后,left和right相遇。当前left指针指向空槽,将参照数放入空槽。
  arr[left] = reference;
  // 9. 以left为界限,划分两个子数组。特别注意,右子数组可能存在数组越界的问题(当left指向arr最右侧时,left+1会越界,越界长度为1)
  const leftChildArr = arr.slice(0, left);
  const rightChildArr = left === length - 1 ? [] : arr.slice(left + 1, length);
  // 10. 分别对左子数组和右子数组排序,然后组合起来,整个数组就排好序了
  return [...quickSort(leftChildArr), reference, ...quickSort(rightChildArr)];
}

优化:不再分割数组

function quickSort(arr, leftBounds = 0, rightBounds = arr.length - 1) {
  // 当排序区间长度不大于1时,停止排序
  if (leftBounds >= rightBounds) {
    return arr;
  }
  const length = rightBounds - leftBounds + 1;
  let left = leftBounds;
  let right = rightBounds;
  const reference = arr[leftBounds];
  let slotInLeft = true;

  while (left !== right) {
    if (slotInLeft) {
      if (reference > arr[right]) {
        arr[left] = arr[right];
        slotInLeft = false;
        left++;
      } else {
        right--;
      }
    } else {
      if (reference <= arr[left]) {
        arr[right] = arr[left];
        slotInLeft = true;
        right--;
      } else {
        left++;
      }
    }
  }
  arr[left] = reference;
  // 排序子数组,参考数不参与排序。数组越界问题在函数调用开头处理。
  quickSort(arr, leftBounds, left - 1);
  quickSort(arr, left + 1, rightBounds);
  return arr;
}

优化:排序模式(递增、递减)

function quickSort(
  arr,
  leftBounds = 0,
  rightBounds = arr.length - 1,
  incremental = true // 是否递增
) {
  if (leftBounds >= rightBounds) {
    return arr;
  }
  const length = rightBounds - leftBounds + 1;
  let left = leftBounds;
  let right = rightBounds;
  const reference = arr[leftBounds];
  let slotInLeft = true;

  while (left !== right) {
    if (slotInLeft) {
      // 递增 右小则交换;递减 右大则交换
      if (
        (incremental && reference > arr[right]) ||
        (!incremental && reference < arr[right])
      ) {
        arr[left] = arr[right];
        slotInLeft = false;
        left++;
      } else {
        right--;
      }
    } else {
      // 递增 左大则交换;递减 左小则交换
      if (
        (incremental && reference <= arr[left]) ||
        (!incremental && reference >= arr[left])
      ) {
        arr[right] = arr[left];
        slotInLeft = true;
        right--;
      } else {
        left++;
      }
    }
  }
  arr[left] = reference;
  quickSort(arr, leftBounds, left - 1);
  quickSort(arr, left + 1, rightBounds);
  return arr;
}

优化:支持对象数组类型

function quickSort(
  arr,
  leftBounds = 0,
  rightBounds = arr.length - 1,
  incremental = true,
  getSortableValue = quickSort.getSortableValue //自定义取值方法,以支持Array<Object>
) {
  if (leftBounds >= rightBounds) {
    return arr;
  }
  let left = leftBounds;
  let right = rightBounds;
  // 保存参考值的引用,一轮排序结束需要将参考值放入空槽。
  const referenceEl = arr[leftBounds];
  const reference = getSortableValue(referenceEl);
  let slotInLeft = true;

  while (left !== right) {
    if (slotInLeft) {
      if (
        (incremental && reference > getSortableValue(arr[right])) ||
        (!incremental && reference < getSortableValue(arr[right]))
      ) {
        arr[left] = arr[right];
        slotInLeft = false;
        left++;
      } else {
        right--;
      }
    } else {
      if (
        (incremental && reference <= getSortableValue(arr[left])) ||
        (!incremental && reference >= getSortableValue(arr[left]))
      ) {
        arr[right] = arr[left];
        slotInLeft = true;
        right--;
      } else {
        left++;
      }
    }
  }
  arr[left] = referenceEl;
  quickSort(arr, leftBounds, left - 1);
  quickSort(arr, left + 1, rightBounds);
  return arr;
}
// 增加静态方法作为默认参数
quickSort.getSortableValue = function (arrEl) {
  return arrEl;
};

优化:支持稳定排序

快速排序法不是稳定排序算法。 因为快排会打乱相同的值的原始顺序。

例如

// 期望结果为递增的例子
const arrOfWaitSort1 = [
  { value: 2 },
  { value: 1, position: 1 },
  { value: 1, position: 2 },
  { value: 3 },
];
/*

 step 1:
[
->{ value: 2 },
  { value: 1, position: 1 },
  { value: 1, position: 2 },
  { value: 3 }<-
]

 step 2:
 { value: 2 }
[
-> slot,
  { value: 1, position: 1 },
  { value: 1, position: 2 },
  { value: 3 }<-
]

 step 3:
 由于 2<3 ,右指针向左移动1格
 { value: 2 }
[
-> slot,
  { value: 1, position: 1 },
  { value: 1, position: 2 },<-
  { value: 3 }
]

 step 4:
 由于 2>1 且slot(空槽)在左,将较小的元素放入slot(空槽)中并产生新的slot,左指针向右移动1格
 { value: 2 }
[
  { value: 1, position: 2 },
->{ value: 1, position: 1 },
  slot,<-
  { value: 3 }
]


 step 5:
 由于 2>1 且slot(空槽)在右,将较小的元素保持不动,左指针向右移动1格
 { value: 2 }
[
  { value: 1, position: 2 },
->{ value: 1, position: 1 },
  slot,<-
  { value: 3 }
]

 step 6:
 此时左右指针相遇,本轮排序结束
 { value: 2 }
[
  { value: 1, position: 2 },
  { value: 1, position: 1 },
->slot,<-
  { value: 3 }
]
*/

// 期望结果为递减的例子,同理
const arrOfWaitSort2 = [
  { value: 2 },
  { value: 3, position: 1 },
  { value: 3, position: 2 },
  { value: 1 },
];