快速排序
1. 写在之前
快速排序还有不会的?
虽然快速排序的想法深入各个程序员的心中,但在面试高压情况下,想要一次流畅正确地写出快排或者用到快排的题目并非容易的事情,经常会出现一些这样那样的小bug,给面试官留下不好的映象。
本文将解决:
- 快速排序的一般框架
- 两种划分方法以及可能出错的点
- 循环不变量验证
2. 快排框架
快速排序的框架非常简单,与归并排序一样,是分治法的体现,思路为:
假设为升序排序,快排将数组划分为两个部分,左边的部分小于某数,后边的部分大于某数,然后在再对数组的左右两部分各自再进行排序。
class Solution{
public int[] sort(int[] nums){
sort(nums, 0. nums.length - 1); // 1
return nums;
}
public void sort(int[] nums, int start, int end){
if(start >= end){
return; // 2
}
int mid = partition(nums, start, end);
sort(nums, start, mid-1); //3
sort(nums, mid+1, end); //3
}
}
框架中的易错点。
- 这里为了划分方便,用的是左右闭区间(也可以使用开区间,但划分步骤得修改)。
- 递归的终止条件,若无就会导致栈溢出
- 新的排序应该去掉返回的mid值,增加效率
3. Partition
划分是快速排序的重头戏,也是bug的高发区域,容易出错的地方是:
- 划分的方法
- 划分的左右边界,的初始值设置和结束条件
- 划分的返回值
3.1 双指针法
双指针法,顾名思义就是左右两个指针,
- 首先选择第一个为标的,
- 左指针指向第一个比标的大的数,右指针指向第一个比标的小的数,然后交换左右指针;
- 不断重复,直到左指针大于右指针;
- 将右指针和第一个位置替换。
private int partition(int[] nums, int start, int end){
int left = start + 1;
int right = end;
int target = nums[start];
while(left <= right){
// search left
while(left <= right && nums[left] <= target ){ //1
left++;
}
while(left <= right &&nums[right] > target){
right--;
}
if(left<=right){
swap(nums, left, right);
}
// search right
}
swap(nums, start, right);
return right;
}
易错的地方:
- 终止条件,
这里选择的是当left不大于right的时候,说明最后跳出循环的时候是left > right,且大于1;
可以推导出一个结论,right最终指向部分肯定有nums[right] >= target,所以跳出循环后用right交换start,而不是left。(留作思考,关注语句1最后两句的执行情况)
3.2 三指针法
三指针法相较于双指针法,多了一个遍历指针,此时左右指针的含义有所不同。
指针 | 开始 | 结束 |
---|---|---|
左指针 | 指向第一个仅可能大于等于target数,左端 | 指向第一个等于target的数,其左边都会小于target |
右指针 | 指向第一个小于等于target的数,右端 | 指向最后一个等于target的数,其右边会大于target |
遍历指针 | 左端+1 | 最终右指针+1 |
private int partition(int[] nums, int start, int end){
int left = start;
int right = end;
int i = left + 1;
int target = nums[start];
while(i <= right){
if(nums[i] < target){
// nums[i]小于target, 应该在左侧,与左侧交换, 两个指针都增长,因为i指针遍历过的位置,都表示左边已经
swap(nums, i, left);
left++;
i++;
}
else if(nums[i] > target){
swap(nums, i, right);
right--;
}
else{
i++;
}
}
return right;
}