快速排序详解及应用
什么是快速排序?
1.快速排序明明就是一个数组算法,和二叉树有什么关系?
首先我们要知道的是所有递归的算法,你甭管它是干什么的,本质上都是在遍历一棵(递归)树,然后在节点(前中后序位置)上执行代码,你要写递归算法,本质上就是要告诉每个节点需要做什么。
然后看看归并排序的代码框架:
void sort(int[] nums, int low, int high) {
if (low >= high) {
return;
}
// 对 nums[low..high] 进行切分
// 使得 nums[low..p-1] <= nums[p] < nums[p+1..high]
int p = partition(nums, low, hi);
// 去左右子数组进行切分
sort(nums, low, p - 1);
sort(nums, p + 1, high);
}
显然快速排序就是先对整个数组进行切分,再分别对左右子数组进行切分。也就是快速排序是先将一个元素排好序然后再将剩下的元素排好序。
快速排序的核心是partition函数,partition函数的作用是在nums[low…high]中寻找一个分界点p,通过交换元素使得nums[low…p-10]都小于等于nums[p],且nums[p+1…high]都大于nums[p]
一个元素左边的元素都比它小,右边的元素都比它大,是什么意思?
也就是说在经过一轮的partition之后,就会让nums[p]放在正确的位置上。(即nums[p]已经被排好序了)
那接下来就是将剩下的元素排好序
那剩下的元素有哪些?很明显,元素都分布在左边和右边,所以我们可以对子数组进行递归,用partition函数把剩下的元素也排好序
但这里我们可以想到二叉树的前序遍历
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
if (root == null) {
return;
}
/****** 前序位置 ******/
print(root.val);
/*********************/
traverse(root.left);
traverse(root.right);
}
因此得出结论:快速排序的过程可以抽象成一棵二叉树,把子数组 nums[lo..hi]
理解成二叉树节点上的值,sort
函数理解成二叉树的遍历函数。
参照二叉树的前序遍历顺序,快速排序可以如图所示,第二个数组原本是空的,在经过多次partition之后按颜色顺序被填入到数组中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nUVlk44p-1680504623264)(D:\Development\Typora\img\image-20230403094217056.png)]
可以注意到最后形成的这棵二叉树是一棵二叉搜索树。
为什么最后得到的是一棵二叉搜索树呢?是巧合吗?
不是巧合,这是因为 partition
函数每次都将数组切分成左小右大两部分,恰好和二叉搜索树左小右大的特性吻合。
因此我们也可以把快速排序的过程理解成是一个构造二叉搜索树的过程
但是谈到二叉搜索树的构造,那就不得不说二叉搜索树不平衡的极端情况,极端情况下二叉搜索树会退化成一个链表,导致操作效率大幅降低。
快速排序的过程中也有类似的情况,比如我画的图中每次
partition
函数选出的分界点都能把nums[low..high]
平分成两半,但现实中你不见得运气这么好。如果你每次运气都特别背,有一边的元素特别少的话,这样会导致二叉树生长不平衡:
这样的话,时间复杂度会大幅上升
解决办法:
我们为了避免出现这种极端情况,需要引入随机性。
常见的方式是在进行排序之前对整个数组执行
洗牌算法
进行打乱,或者在partition
函数中随机选择数组元素作为分界点,本文会使用前者。
2.快速排序代码实现
class Quick {
public static void sort(int[] nums) {
// 为了避免出现耗时的极端情况,先随机打乱
shuffle(nums);
// 排序整个数组(原地修改)
sort(nums, 0, nums.length - 1);
}
private static void