快速排序算法复习
作者:何贤
基础理论
快速排序算法是什么?
快速排序算法由 C. A. R. Hoare 在 1960 年提出。它的时间复杂度也是 O(nlogn)O(nlogn),但它在时间复杂度为 O(nlogn)O(nlogn) 级的几种排序算法中,大多数情况下效率更高,所以快速排序的应用非常广泛。再加上快速排序所采用的分治思想非常实用,使得快速排序深受面试官的青睐,所以掌握快速排序的思想尤为重要。
基本思想
从数组中取出一个数,称之为基数(pivot)
遍历数组,将比基数大的数字放到它的右边,比基数小的数字放到它的左边。遍历完成后,数组被分成了左右两个区域
将左右两个区域视为两个数组,重复前两个步骤,直到排序完成
事实上,快速排序的每一次遍历,都将基数摆到了最终位置上。第一轮遍历排好 1 个基数,第二轮遍历排好 2 个基数(每个区域一个基数,但如果某个区域为空,则此轮只能排好一个基数),第三轮遍历排好 4 个基数(同理,最差的情况下,只能排好一个基数),以此类推。总遍历次数为 logn~n 次,每轮遍历的时间复杂度为 O(n)O(n),所以很容易分析出快速排序的时间复杂度为 O(nlogn)O(nlogn) ~ O(n^2)O(n2),平均时间复杂度为 O(nlogn)O(nlogn)。
自用笔记
快速排序例题
快速排序
给你一个整数数组 nums
,请你将该数组升序排列。用快速排序
#include <vector>
#include <iostream>
using namespace std;
/*
* @lc app=leetcode.cn id=912 lang=cpp
*
* [912] 排序数组
*/
// @lc code=start
class Solution
{
private:
// 快速排序 非递归版
void quick_sort(vector<int> &nums, int left, int right)
{
// 边界条件判断
if (left >= right)
{
return;
}
// 设定新的基准 这里只是假定每次最左 实际上应该随机拟定新的基准
int mid = left + (right - left) / 2;
int flag = nums[mid], i = left - 1, j = right + 1;
while (i < j)
{
do
{
i++;
} while (nums[i] < flag);
do
{
j--;
} while (nums[j] > flag);
if (i < j)
{
swap(nums[i], nums[j]);
}
}
quick_sort(nums, left, j);
quick_sort(nums, j + 1, right);
}
public:
vector<int> sortArray(vector<int> &nums)
{
quick_sort(nums, 0, nums.size() - 1);
return nums;
}
};
// @lc code=end
多数元素
Category | Difficulty | Likes | Dislikes |
---|---|---|---|
algorithms | Easy (66.52%) | 1246 | - |
给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋
的元素。
你可以假设数组是非空的,并且给定的数组总是存在多数元素。
#include <vector>
#include <iostream>
using namespace std;
/*
* @lc app=leetcode.cn id=912 lang=cpp
*
* [912] 排序数组
*/
// @lc code=start
class Solution
{
private:
// 快速排序 非递归版
void quick_sort(vector<int> &nums, int left, int right)
{
// 边界条件判断
if (left >= right)
{
return;
}
// 设定新的基准 这里只是假定每次最左 实际上应该随机拟定新的基准
int mid = left + (right - left) / 2;
int flag = nums[mid], i = left - 1, j = right + 1;
while (i < j)
{
do
{
i++;
} while (nums[i] < flag);
do
{
j--;
} while (nums[j] > flag);
if (i < j)
{
swap(nums[i], nums[j]);
}
}
quick_sort(nums, left, j);
quick_sort(nums, j + 1, right);
}
public:
vector<int> sortArray(vector<int> &nums)
{
quick_sort(nums, 0, nums.size() - 1);
return nums;
}
};
// [2,2,1,3,1,1,4,1,1,5,1,1,6]
// @lc code=end
快速排序的递归框架
顺带一提用i做划分时的模板
void quick_sort(int q[], int l, int r)
{
if(l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r + 1 >> 1];//注意是向上取整,因为向下取整可能使得x取到q[l]
while(i < j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i], q[j]);
}
quick_sort(q, l, i - 1), quick_sort(q, i, r);//不用q[l..i],q[i+1..r]划分的道理和分析4中j的情况一样
}
还有从大到小排序的模板(仅仅改两个地方的判断符号)
void quick_sort(int q[], int l, int r)
{
if(l >= r) return;
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while(i < j)
{
do i++; while(q[i] > x); // 这里和下面
do j--; while(q[j] < x); // 这行的判断条件改一下
if(i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j), quick_sort(q, j + 1, r);
}
边界问题证明
边界情况分析
若x选为q[r],数组中q[l…r-1] < x,那么这一轮循环结束时i = r, j = r,显然会造成无限划分
快排属于分治算法,最怕的就是 n分成0和n,或 n分成n和0,这会造成无限划分
以j为划分时,x不能选q[r] (若以i为划分,则x不能选q[l])
假设 x = q[r]
关键句子**quick_sort(q, l, j)**, quick_sort(q, j + 1, r);
由于j的最小值是l,所以q[j+1..r]不会造成无限划分
但q[l..j](即quick_sort(q, l, j))却可能造成无限划分,因为j可能为r
do i++; while(q[i] < x)和do j–; while(q[j] > x)不能用q[i] <= x 和 q[j] >= x
假设q[l..r]全相等
则执行完do i++; while(q[i] <= x);之后,i会自增到r+1
然后继续执行q[i] <= x 判断条件,造成数组下标越界(但这貌似不会报错)
并且如果之后的q[i] <= x (此时i > r) 条件也不幸成立,
就会造成一直循环下去(亲身实验),造成内存超限(Memory Limit Exceeded)
if(i < j) swap(q[i], q[j])能否使用 i <= j
可以使用if(i <= j) swap(q[i], q[j]);
因为 i = j 时,交换一下q[i],q[j] 无影响,因为马上就会跳出循环了
最后一句能否改用quick_sort(q, l, j-1), quick_sort(q, j, r)作为划分(用i做划分时也是同样的道理,)
不能
根据之前的证明,最后一轮循环可以得到这些结论
j <= i 和 q[l..i-1] <= x, q[i] >= x 和 q[j+1..r] >= x, q[j] <= x
所以,q[l..j-1] <= x 是显然成立的,
但quick_sort(q, j, r)中的q[j] 却是 q[j] <= x,这不符合快排的要求
另外一点,注意quick_sort(q, l, j-1), quick_sort(q, j, r)可能会造成无线划分
当x选为q[l]时会造成无限划分,报错为(MLE),
如果手动改为 x = q[r],可以避免无限划分
但是上面所说的q[j] <= x 的问题依然不能解决,这会造成 WA (Wrong Answer)
j的取值范围为[l..r-1]
证明:
假设 j 最终的值为 r ,说明只有一轮循环(两轮的话 j 至少会自减两次)
说明q[r] <= x (因为要跳出do-while循环)
说明 i >= r(while循环的结束条件), i 为 r 或 r + 1(必不可能成立)
说明 i 自增到了 r , 说明 q[r] >= x 和 q[l..r-1] < x,
得出 q[r] = x 和 q[l..r-1] < x 的结论,但这与 x = q[l + r >> 1]矛盾
反证法得出 j < r
假设 j 可能小于 l 说明 q[l..r] > x ,矛盾
反证法得出 j >= l
所以 j的取值范围为[l..r-1],不会造成无限划分和数组越界
请注意 —— 边界证明非原创
作者:醉生梦死
链接:https://www.acwing.com/solution/content/16777/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。