信息奥赛|快速排序
知识点概览 🚀
- 快速排序の简单介绍
- 快速排序の编程框架
- 快速排序の代码模版
知识点详解 🎥
简单介绍 ☘️
快速排序 是分治算法的应用之一,其时间复杂度可以到达 $O(nlogn)$
关于快速排序的具体思想,网上已经有许多优秀的博客,这里不再赘述,有需要的读者可以查看下面几篇推文:
快速排序详解:http://t.csdn.cn/v8Zul
摊主将在本文中阐述一些快速排序的学习心得与注意事项,供日后复习使用。
编程框架 🧑🏻💻
首先来看一下 快速排序的编程框架:
quicksort(int l,int r){
if(l>=r) return; //返回条件
//快速排序每一趟流程
1.选定当前序列的基准数(pivot)
2.哨兵出动,交换序列值,直到找到基准点(两哨兵交汇处)
3.将基准数归位至基准点(pivot_position)
//二分,递归调用
quicksort(l,pivot_position);
quicksort(pivot_position+1,r);
}
对比 归并排序的编程框架:
mergesort(int l,int r){
if(l>=r) return; //返回条件
//二分,递归调用,先把两个子序列分别排好
int mid = (l+r)/2;
mergesort(l,mid);
mergesort(mid+1,r);
//将两个有序的子序列合并
1.设定哨兵p1,p2,两个子序列元素不断比较,放入临时数组t中
2.检查哨兵p1是否到末尾,将剩余元素依次放入t中
3.检查哨兵p2是否到末尾,将剩余元素依次放入t中
4.将临时数组t中的元素全部放入原序列中
}
可以看到,两者的共同点是 都有递归调用的过程,不同点是 递归调用的位置不同。快速排序 是先完成一趟排序,然后将序列二分;归并排序 则是先将序列二分,再将排序后的两个子序列合并。
虽然两者的编程框架看起来有些相似,但 实际的编程难度,快速排序会更难一些。
快速排序的难度提升,并不是指思想上的难度,而是快速排序在编程时,需要考虑的编程细节,如「等号条件」等较为繁琐。在快速排序的编程过程中,很容易出现「漏掉一个等号,排序就失败」的情况。
代码模版 🧑🏻💻
模版 1
该模版容易理解,但是非常容易超时。❌
void qsort(int a[], int l, int r) {
if (l >= r) return;
int pivot = a[l]; //基准数
int i = l; //选定左哨兵
int j = r; //选定右哨兵
while (i < j) {
while (a[j] >= pivot && i < j) j--;
while (a[i] <= pivot && i < j) i++;
if (i != j) swap(a[i], a[j]); //交换当前不相同的哨兵值
}
swap(a[i], a[l]); //循环结束后,哨兵值 i = j,为基准数a[l]最后的位置
qsort(a, l, i - 1);
qsort(a, i + 1, r);
}
模版 2(AcWing 版)
该模版相对不容易理解,但是不会超时。✅
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); //经过实测,该模版中的j不能替换为i
}
边界值思考 🧐
结合快速排序的 C++ 源代码,思考下面的问题:
- 返回条件:
if(l > r) return;
和if(l >= r) return;
对程序造成的影响。 - 左哨兵的选择:
p1 = a[l]
🆚p1 = a[l+1]
对程序造成的影响。 - 在第一层
while
循环中,while(p1<p2)
🆚while(p1<=p2)
对程序造成的影响。 - 在第二层
while
循环中,while (a[p2] >= pivot && p2 > p1)
🆚while (a[p2] > pivot && p2 > p1)
对程序造成的影响。 - 在第二层
while
循环中,while (a[p2] >= pivot && p2 > p1)
🆚while (a[p2] >= pivot && p2 >= p1)
对程序造成的影响。
C++ 源代码(基于模版1)
#include<bits/stdc++.h>
using namespace std;
int a[10005];
int n;
void quicksort(int l, int r);
void print();
int main() {
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> a[i];
cout<<"before quick sort:";
print();
quicksort(1, n);
cout<<"after quick sort: ";
print();
return 0;
}
void print() {
for (int i = 1; i <= n; ++i) {
cout<<a[i]<<" ";
}
cout<<endl;
}
void quicksort(int l, int r) {
if (l > r) return; //思考此处的条件,是否需要添加等号,如果不加,会有什么影响
int tmp;
int pivot = a[l]; //设定基准数
int p1 = l; //左哨兵初始位置在哪里,是l还是l+1
int p2 = r;
while (p1 < p2) { //思考此处条件,是否需要添加 =,如果不添加,会有什么后果
while (a[p2] >= pivot && p2 > p1) p2--; //思考此处条件,两者是否需要添加等号
while (a[p1] <= pivot && p2 > p1) p1++;
if (p1 != p2) {
tmp = a[p2];
a[p2] = a[p1];
a[p1] = tmp;
}
}
a[l] = a[p1];
a[p1] = pivot;
quicksort(l, p1 - 1);
quicksort(p1 + 1, r);
}
分析:
- 返回条件:
if(l > r) return;
和if(l >= r) return;
对程序 没有影响。 - 左哨兵必须选择
p1 = a[l]
✅ ,不能选择p1=a[l+1]
❌ 。比如序列 6 7 10 9,如果左哨兵选择 7 ,那么两个哨兵在 7 相遇,7 和 6 换了位置,这是不对的。而左哨兵选择 6 ,两个哨兵会在 6 相遇,6 和自己换了位置(等于没换),才是正确的。 - 在第一层 while 循环中,
while(p1<p2)
✅while(p1<=p2)
❌ 。当两个哨兵相遇时,后者可能陷入死循环。 - 在第二层 while 循环中,
while (a[p2] >= pivot && p2 > p1)
✅while (a[p2] > pivot && p2 > p1)
❌ 。比如序列 6 2 1 7 5 6,如果不添加等号,右哨兵就会停留在最后一个 6 上,左哨兵一直停留在第一个 6 上,程序陷入死循环。 - 在第二层 while 循环中,
while (a[p2] >= pivot && p2 > p1)
✅while (a[p2] >= pivot && p2 >= p1)
❌ 。比如序列 6 7 8 9,添加等号后,右哨兵在回到第 1 个 6 的位置时,仍会执行p1--
,导致下标越界。
C++ 源代码(基于模版2)
Acwing 的 模版2 ,没有那么多等号条件要考虑,较为推荐。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
//快速排序
void qsort(int l,int r){
//返回条件
if(l >= r) return ;
//选定基准数,定义双指针
int temp = a[ (l + r) >> 1 ];
int i = l - 1, j = r + 1;
//基本流程
while( i < j ){
do i++; while(a[i] < temp);
do j--; while(a[j] > temp);
if(i < j) swap(a[i],a[j]);
}
//递归
qsort(l,j);
qsort(j+1,r);
}
int main(){
//读入数据
cin>>n;
for(int i = 1; i <= n; i++) {
cin>>a[i];
}
//快速排序
qsort(1,n);
//输出结果
for ( int i = 1; i <= n; i++){
cout<<a[i]<<" ";
}
cout<<endl;
return 0;
}
🧑🏻💻 快速排序OI案例
OI 案例1
【题目描述】
利用快速排序算法将读入的 $N$ 个数从小到大排序后输出。
快速排序是信息学竞赛的必备算法之一。对于快速排序不是很了解的同学可以自行上网查询相关资料,掌握后独立完成。(C++ 选手请不要试图使用 STL
,虽然你可以使用 sort
一遍过,但是你并没有掌握快速排序算法的精髓。)
【输入】
第 $1$ 行为一个正整数 $N$,第 $2$ 行包含 $N$ 个空格隔开的正整数 $a_i$,为你需要进行排序的数,数据保证了 $A_i$ 不超过 $10^9$。
【输出】
将给定的 $N$ 个数从小到大输出,数之间空格隔开,行末换行且无空格。
【输入样例】
5
4 2 4 5 1
【输出样例】
1 2 4 4 5
【数据范围】
对于 $20%$ 的数据,有 $N\leq 10^3$;
对于 $100%$ 的数据,有 $N\leq 10^5$。
【原题链接】
【题目解析】
本题如果直接提交 模版1 程序,会有 3 个超时点 ❌
因此需要优化。
先来看一些不需要对代码算法做改进的优化。
优化方案1:将 输入输出流 改为
scanf/printf
,实测该方法 失败,仍有 3 个超时点。❌优化方案2:添加优化语句,实测该方法 失败,仍有 3 个超时点(关于该优化的 相关技术博客)❌
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
...
}
- 优化方案3:手动开启O2优化,实测该方法 失败,编译错误。(洛谷不支持手动开启 O2优化)❌
#pragma GCC optimize(2)
- 优化方法4:在洛谷 OJ 上,勾选 开启O2优化,实测该方法生效,通过所有测试点。✅
再来看一些 基于快速排序本身算法思想的优化:
首先分析 快速排序的时间复杂度:
简单来说,快速排序是一种基于二分思想的算法,时间复杂度为 $O( nlogn )$
但是,当需要排序的数组,本身就有序,或者趋近于有序时,快速排序的时间复杂度会退化成 $O(n^2)$
具体数学推导可以看下面这一篇 技术博客的分析:
基于以上对时间复杂度的分析,最终优化方案 如下:
- 优化方案1:使用
random_shuffle(a+1,a+n+1)
将原数组打乱,该方案 部分奏效,但是仍有 1 个超时点。(PS:调用 1 次和调用 n 次 该代码进行打乱,最后 1 个超时点均存在)❌
- 优化方案2:使用
sort(a+1,a+n+1)
,直接 AC(不得不说,STL 真香)✅
#include<bits/stdc++.h>
using namespace std;
int a[1000005];
int n;
int main() {
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
sort(a+1,a+n+1); //STL大法真香
for (int i = 1; i <= n; ++i) cout << a[i] << " ";
cout << endl;
return 0;
}
- 优化方案3:在传统快速排序中,我们的基准点总是选择左边界,这使得像
6 5 4 3 2 1
这类数据,在递归时,quicksort(p1+1,r)
失效,程序并没有「二分」,速度大大降低。
所以我们 修改基准点的选择,改为 tmp = a[(l + r) / 2]
(即 模版2),成功 AC ✅
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n;
int a[N];
//快速排序
void qsort(int l, int r) {
//返回条件
if (l >= r) return;
//选定基准数,定义双指针
int temp = a[(l + r) >> 1];
int i = l - 1, j = r + 1;
//基本流程
while (i < j) {
do i++; while (a[i] < temp);
do j--; while (a[j] > temp);
if (i < j) swap(a[i], a[j]);
}
//递归
qsort(l, j);
qsort(j + 1, r);
}
int main() {
ios::sync_with_stdio(false); //cin读入优化
cin.tie(0);
//读入数据
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
//快速排序
qsort(1, n);
//输出结果
for (int i = 1; i <= n; i++) {
cout << a[i] << " ";
}
cout << endl;
return 0;
}
刷题题单 🧑🏻💻
刷题题单与相应题解请见博客:信息奥赛题单|快速排序🔗 / (备用)信息奥赛题单|归并排序🔗
如链接失效,可以在文章末尾选择上一篇博客 或 在搜索界面搜索关键词「快速排序」即可。
参考资料 📚
- AcWing算法基础课 - https://www.acwing.com/activity/content/11/
- AcWing代码模版 - https://www.acwing.com/blog/content/277/