0
点赞
收藏
分享

微信扫一扫

自动化办公01 smtplib 邮件⾃动发送

快速排序算法:原理与最优实现

快速排序(Quick Sort)是一种高效的排序算法,它的时间复杂度为O(nlogn),在实际应用中表现优异。快速排序的思路是通过分治的方式,将待排序的序列分成两个子序列,其中一个子序列的所有元素都比另一个子序列的所有元素小(或大),然后再对这两个子序列分别进行排序,最后将排好序的子序列合并成最终的排序序列。

快速排序算法的原理

快速排序算法的基本步骤如下:

  1. 选择一个基准值(pivot),通常选择第一个元素或者最后一个元素。
  2. 将序列中的所有元素与基准值进行比较,小于基准值的元素放到基准值的左边,大于基准值的元素放到基准值的右边。这个过程称为分区(partition)。
  3. 对基准值左边的子序列和右边的子序列分别进行递归排序。

快速排序的关键在于分区操作,它决定了算法的效率。理想情况下,每次分区都能将序列分成两个相等长度的子序列,这样递归深度为logn,时间复杂度为O(nlogn)。然而,在最坏情况下,每次分区只能将序列分成一个元素和n-1个元素的两个子序列,此时递归深度为n,时间复杂度退化为O(n^2)。

为了避免最坏情况的发生,我们可以采用随机化策略,即随机选择基准值。这样,快速排序的平均时间复杂度仍为O(nlogn)。

快速排序算法的实现

以下是使用JavaScript实现的快速排序算法示例,采用了随机化策略和双指针法进行分区:

版本一:

// 快速排序主函数
function quickSort(arr, left = 0, right = arr.length - 1) {
  // 如果左边界大于等于右边界,说明数组已经排序完成
  if (left >= right) {
    return;
  }

  // 对数组进行分区操作,返回分区后基准值的索引
  const pivotIndex = partition(arr, left, right);
  // 对基准值左侧的子数组进行递归排序
  quickSort(arr, left, pivotIndex - 1);
  // 对基准值右侧的子数组进行递归排序
  quickSort(arr, pivotIndex + 1, right);
}

// 分区函数
function partition(arr, left, right) {
  // 随机选择一个基准值索引,这里使用随机化策略
  const pivotIndex = Math.floor(Math.random() * (right - left + 1)) + left;
  // 获取基准值
  const pivot = arr[pivotIndex];
  // 初始化左右指针
  let i = left,
    j = right;

  // 当左指针小于等于右指针时,继续循环
  while (i <= j) {
    // 如果左指针指向的元素小于基准值,左指针向右移动
    while (arr[i]< pivot) {
      i++;
    }
    // 如果右指针指向的元素大于基准值,右指针向左移动
    while (arr[j] > pivot) {
      j--;
    }
    // 如果左指针小于等于右指针,说明找到了需要交换的元素
    if (i <= j) {
      // 交换左右指针指向的元素
      [arr[i], arr[j]] = [arr[j], arr[i]];
      // 交换后,左指针向右移动,右指针向左移动
      i++;
      j--;
    }
  }

  // 返回分区后基准值的索引
  return i;
}
// 示例用法
const arr = [64, 34, 25, 12, 22, 11, 90];
quickSort(arr);
console.log(arr); // 输出:[11, 12, 22, 25, 34, 64, 90]

这段代码实现了一个基本的快速排序算法,使用了随机化策略来选择基准值,以提高算法的平均时间复杂度。通过递归调用quickSort函数,对基准值左右两侧的子数组进行排序。partition函数负责将数组分为两个子数组,其中一个子数组的所有元素都小于基准值,另一个子数组的所有元素都大于基准值。这样,通过不断递归调用,最终可以得到一个完全有序的数组。

当然还有其他很多实现方式,例如以下版本二:

版本二:

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 = [64, 34, 25, 12, 22, 11, 90];
const sortedArr = quickSort(arr);
console.log(sortedArr); // 输出:[11, 12, 22, 25, 34, 64, 90]

方法二这种实现是递归排序,它通过递归地将数组分为左右两部分,然后将左右两部分的结果合并。这种方法的优点是代码简洁易懂,但缺点是空间复杂度较高,为O(n),因为每次递归都会创建新的数组。

结论

从性能角度来看,第一种实现通常更优,因为它的空间复杂度较低。然而,第二种实现(递归排序)的代码更简洁,易于理解。在实际应用中,可以根据项目需求和团队偏好来选择合适的实现方式。如果内存资源充足,第二种实现也是一个不错的选择。如果对性能有较高要求,建议使用第一种实现。

举报

相关推荐

0 条评论