「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。
背景介绍
先别标题党,什么快速排序是分治算法什么的先别管,里面是不是有递归?有递归就能跟树扯上一点关系, 而 labuladong 的手把手带你刷二叉树(第一期), 这篇文章里面就说过快速排序就是个二叉树的前序遍历,归并排序就是个二叉树的后序遍历
所以本篇文章会将快速排序结合树的性质讲一下
注:快速排序分为两个部分, 一个是排序逻辑, 一个是递归调用
- 排序逻辑:会简单复习一下
- 递归调用(这里与 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];
}
看来上面的视频你应该知道,排序逻辑主要是双指针,这里面双指针的难点在于,怎样移动的这个过程在两个指针中切换(我之前就是不会这个)
其实这个双指针的移动不要纠结与在一个循环中完成,而是想成拆分,每一次排序中,涉及两个操作,右指针移动到与左指针相同,左指针移动,右指针移动,而这里就是三个循环
上面这个一般被放到 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] 为例
相对于一般快速排序讲解以数组横列式排序的方法,二叉树描述是不是又有另一种感觉?不要小看上面那个 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)),
];
};
快速排序用回溯的思想去递归比较好,可能是因为数组式的递归要想和二叉树链表类型式的递归相比,数组式的递归必须同时拿捏数组本身,左指针和右指针
总结
可能有人看了想骂我,你根本没有把快速排序的精髓分治讲出来,快速排序为什么要这么分治为什么要这么做你都没有详细讲
其实是因为文章是想讲快排和递归的关系,对于快速排序的内在逻辑,我也不想过多赘述,网上一大把,而且递归是很符合计算机的思考方式,写好逻辑然后将不可预知的次数交给计算机处理