快速排序和递归之间的关系

158 阅读3分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。

背景介绍

先别标题党,什么快速排序是分治算法什么的先别管,里面是不是有递归?有递归就能跟树扯上一点关系, 而 labuladong 的手把手带你刷二叉树(第一期), 这篇文章里面就说过快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历

所以本篇文章会将快速排序结合的性质讲一下

注:快速排序分为两个部分, 一个是排序逻辑, 一个是递归调用

  1. 排序逻辑:会简单复习一下
  2. 递归调用(这里与 labuladong 的算法小抄相关)

排序逻辑

已经对快排逻辑了如指掌的可以直接跳到递归调用部分

详细逻辑请看,快速排序 - 秒懂算法

// 具体代码
// nums 是待排序数组
let left = 0;
let right = nums.length - 1;
const pivot = nums[0];
while (left < right) {
  while (left < right && nums[right] >= pivot) {
    right = right - 1;
  }
  nums[left] = nums[right];
  while (left < right && nums[left] <= pivot) {
    left = left + 1;
  }
  nums[right] = nums[left];
}

看来上面的视频你应该知道,排序逻辑主要是双指针,这里面双指针的难点在于,怎样移动的这个过程在两个指针中切换(我之前就是不会这个)

其实这个双指针的移动不要纠结与在一个循环中完成,而是想成拆分,每一次排序中,涉及两个操作,右指针移动到与左指针相同,左指针移动,右指针移动,而这里就是三个循环

快速排序逻辑1.jpg

上面这个一般被放到 partition 里面(也就是分治), partition 一般有很多细节, 像上面这个其实考虑了很多边界情况, 但是这里就不讲了, 因为 partition 虽然理念大概都是相同的, 但是实现上其实还是有很多差别的, 轮子肯定有差有好吗, 推荐去看 labuladong 大佬推荐的, 《算法4》的快速排序版本, 很细, 我只能这么说

细节没得说

递归调用

前序位置

二叉树有一个遍历的框架,像下面这个样子

const traverse = (root) => {
   // 前序位置
   traverse(root.left);
   // 中序位置
   traverse(root.right);
   // 后序位置
}

注意,注意,小细节,回忆一下快速排序的代码

const sort = (nums,left,right) => {
  if(left >= right) return;
  let p = partition(num,left,right);
  sort(nums,left,p-1);
  sort(nums,p+1,right);
}

是不是似曾相识, 快速排序就是在前序位置上做了分治,然后使用了经典的二叉树遍历,二叉树恐成最大赢家快速排序竟和二叉树打成闭环

这个时候完全可以用二叉树来模拟一遍快速排序的过程

首先,快速排序的递归是要传递左右两部分排过序的值,而函数传递进来的参数是上一层未经排过序的值,所以我们需要在前序位置处理参数,然后在进行递归调用

[19,97,09,17,01,08] 为例

快速排序递归逻辑1.jpg

相对于一般快速排序讲解以数组横列式排序的方法,二叉树描述是不是又有另一种感觉?不要小看上面那个 labuladong 的二叉树遍历框架,非常多的二叉树题目都可以用这个框架解决问题,虽然快速排序并不是一个二叉树问题,而是被归纳到快速排序,但是它的递归也是类似树这种数据结构

代码

最后小小的复习一下快速排序,我之前写的(并不是 leetcode 上的那种最优解)一个解法,后面发现内含动态规划思想,算不算是动态规划已经深入我心?这个算法从代码看从每个子问题的解得到答案然后得到最优解,但实际写出来是由自顶向下然后转变成自底向上。但是也存在缺点,就是很浪费内存,因为每次递归都要消耗创建新的数组,后面优化后,直接原地修改数组或者拷贝一个新的数组原地修改

var sortArray = function (nums) {
  if (nums.length <= 1) {
    return nums;
  }
  let left = 0;
  let right = nums.length - 1;
  const pivot = nums[0];
  while (left < right) {
    while (left < right && nums[right] > pivot) {
      right = right - 1;
    }
    nums[left] = nums[right];
    while (left < right && nums[left] < pivot) {
      left = left + 1;
    }
    nums[right] = nums[left];
  }
  nums[left] = pivot;
  return [
    ...sortArray(nums.slice(0, left)),
    pivot,
    ...sortArray(nums.slice(left + 1)),
  ];
};

快速排序用回溯的思想去递归比较好,可能是因为数组式的递归要想和二叉树链表类型式的递归相比,数组式的递归必须同时拿捏数组本身,左指针和右指针

总结

可能有人看了想骂我,你根本没有把快速排序的精髓分治讲出来,快速排序为什么要这么分治为什么要这么做你都没有详细讲

其实是因为文章是想讲快排和递归的关系,对于快速排序的内在逻辑,我也不想过多赘述,网上一大把,而且递归是很符合计算机的思考方式,写好逻辑然后将不可预知的次数交给计算机处理