快速排序本质上可以看作二叉树的前序遍历
快速排序是先将一个元素排好序,然后再将剩下的元素排好序
核心思路依然是分治
快排整体思路
准确的可以说是治分 => 先治 得到分界点后 再分
治:双指针技巧(左右指针或者快慢指针,我会将两种都展示)将一个元素放到正确的位置
分:根据上面“治”返回的分界点进行拆分 => 得到新个较小规模问题后再治理
你甚至可以这样理解:快速排序的过程是一个构造二叉搜索树的过程
但谈到二叉搜索树的构造,那就不得不说二叉搜索树不平衡的极端情况,极端情况下二叉搜索树会退化成一个链表,导致操作效率大幅降低
=> 为了避免这种问题 => 我们将引入洗牌算法 (别害怕就是随机数问题,没难度)
void shuffle(vector<int>& nums){
int n = nums.size();
srand((unsigned)time(NULL));
for(int i=0;i<n;i++){
int r = i + rand()%(n-i);
swap(nums[i],nums[r]);
}
}
partition写法
其中最难的就是partition的写法 => 也是本文的核心内容
=> 我将从多角度分析双指针技巧
好啦,下面就来介绍partition写法
首先你要确定以谁为基准(也就是pivot或者说分界点),这很重要 => 直接影响到下一步选择
原则:升序情况下,以最右边为基准时,最后拿较大的和基准元素交换
注意我下面所说的较大较小都是和基本比较而言的哦!
左右指针
升序:左指针找较大的数字,右指针找较小的数字,然后交换彼此
降序:左指针找较小的数字,右指针找较大的数字,然后交换彼此
升序降序分别两种写法
辅助理解
这里是升序情况,right拿到的是较小的 => 适合基准在最左边
int Partition(vector<int>& nums, int lo, int hi) {
int pivot = nums[lo];
int left = lo + 1, right = hi;
while(left <= right) {
while(left < hi && nums[left] <= pivot) left++;
while(right > lo && nums[right] > pivot) right--;
if(left >= right) break;
swap(nums[left], nums[right]);
}
swap(nums[lo], nums[right]);
return right;
}
还有一种升序情况演示:
int Partition(vector<int>& nums, int lo, int hi) {
int pivot = nums[hi];
int left = lo, right = hi - 1;
while(left <= right) {
while(left < hi && nums[left] <= pivot) left++;
while(right > lo && nums[right] > pivot) right--;
if(left >= right) break;
swap(nums[left], nums[right]);
}
swap(nums[hi], nums[left]);
return left;
}
这里是降序情况,left拿到的是较大的 => 适合基准在最左边 (注意降序要倒一下)
int Partition(vector<int>& nums, int lo, int hi) {
int pivot = nums[hi];
int left = lo, right = hi - 1;
while(left < right) {
while(left < hi && nums[left] > pivot) left++;
while(right > lo && nums[right] <= pivot) right--;
if(left >= right) break;
swap(nums[left], nums[right]);
}
swap(nums[hi], nums[left]);
return left;
}
快慢指针
(这里只考虑升序情况)
不同于上面左右指针两个都可以和基准做交换,
这里fast是探路指针,只有slow可以和基准做交换
但是方向不同,在升序情况下,亦有两种写法
辅助理解
探路fast指针遇到较大的才交换时:
=> 将所有较大的数值放到走过的背后
=> slow占的位置是较小的数字 => 适合基准在最左边
int partition(vector<int>& nums,int lo,int hi){
int pivot = nums[lo];
int slow = hi, fast =hi;
while(fast > lo){
// 探路fast指针遇到较大的才交换
// => 将所有大的数值放到走过的背后
// => slow占的位置是较小的数字
if(nums[fast] > pivot){
swap(nums[slow],nums[fast]);
slow--;
}
fast--;
}
swap(nums[slow],nums[lo]);
return slow;
}
探路fast指针遇到较小的才交换时:
=> 将所有较小的数值放到走过的背后
=> slow占的位置是较大的数字 => 适合基准在最右边
int partition(vector<int>& nums, int lo, int hi) {
int pivot = nums[hi];
int fast = lo, slow = lo;
while(fast < hi) {
if(nums[fast] < pivot) swap(nums[slow++], nums[fast]);
fast++;
}
swap(nums[hi], nums[slow]);
return slow;
}
最后给出完整代码(以类形式给出)
class Quick {
public:
void sort(vector<int>& nums) {
// 为了避免极端情况,先打乱
shuffle(nums);
sort(nums, 0, nums.size() - 1);
}
private:
void sort(vector<int>& nums, int lo, int hi) {
if(lo >= hi) return;
int p = partition(nums, lo, hi);
sort(nums, lo, p - 1);
sort(nums, p + 1, hi);
}
int partition(vector<int>& nums, int lo, int hi) {
int pivot = nums[hi];
int fast = lo, slow = lo;
while(fast < hi) {
if(nums[fast] < pivot) swap(nums[slow++], nums[fast]);
fast++;
}
swap(nums[hi], nums[slow]);
return slow;
}
void shuffle(vector<int>& nums) {
int n = nums.size();
srand((unsigned )time(NULL));
for(int i = 0; i < n; i++) {
int r = i + rand() % (n - i);
swap(nums[i], nums[r]);
}
}
};
快排变体 => 快速选择算法
215. 数组中的第K个最大元素 - 力扣(LeetCode)
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
shuffle(nums);
int n = nums.size();
// 将第k大数字 转化为升序下的第k个数字
k = n - k;
int left = 0, right = n - 1;
while(left <= right) {
int p = partition(nums, left, right);
// p排位计较小 => 到分界点右边去找找第k个
if(k > p) left = p + 1;
// p排位计较大 => 到分界点左边去找找第k个
else if(k < p) right = p - 1;
else return nums[p];
}
return -1;
}
private:
void shuffle(vector<int>& nums) {
int n = nums.size();
srand((unsigned)time(NULL));
for(int i = 0; i < n; i++) {
int r = i + rand() % (n - i);
swap(nums[i], nums[r]);
}
}
int partition(vector<int>& nums, int lo, int hi) {
int pivot = nums[hi];
int fast = lo, slow = lo;
while(fast < hi) {
if(nums[fast] < pivot) swap(nums[slow++], nums[fast]);
fast++;
}
swap(nums[hi], nums[slow]);
return slow;
}
};