1.快速排序
快速排序总结
原理
二分 + 分治

目标:
- 选取基准值pivot
- 根据基准值将数组一分为二,左边
<= pivot
, 右边>= pivot
- 根据pivot位置,将数组一份为二,递归排序处理
代码
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int s[N];
void quickSort(int l, int r){
if(l >= r) return;
int pivot = s[(l + r) >> 1];
int i = l - 1, j = r + 1;
while(i < j){
do ++i; while(s[i] < pivot);
do --j; while(s[j] > pivot);
if(i < j) swap(s[i], s[j]);
}
quickSort(l, j);
quickSort(j + 1, r);
}
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i++) cin >> s[i];
quickSort(0, n - 1);
for(int i = 0; i < n; i++) cout << s[i] << " ";
return 0;
}
接收输入数据格式如下,第一行是输入数据长度,第二行是具体数据,以空格分隔
4
3 1 2 4
Q&A
1. 为什么是先do后while?
while(i < j){
do ++i; while(s[i] < pivot);
do --j; while(s[j] > pivot);
if(i < j) swap(s[i], s[j]);
}
能不能改成
while(i < j){
while(s[i] < pivot) ++i;
while(s[j] > pivot) --j;
if(i < j) swap(s[i], s[j]);
}
考虑如下情况
8 8 8
当值都相同时,i
和 j
会停止移动,导致死循环,先do
后while
能保证每轮循环指针都至少移动一格,从而跳出循环。
2. s[i]<pivot
可以换成s[i]<=pivot
吗?
不能,考虑如下输入
8 8 8
使用<=
,i会不停地增长,根本没有条件使其停下来。所以不能换。
2. 划分区间时,能将j换成i吗?
即,将
quickSort(l, j);
quickSort(j + 1, r);
改成
quickSort(l, i);
quickSort(i + 1, r);
答:不能
在快排中,每层递归,都需要将数据划分成的两个子数组,且左边<= pivot
, 右边 >= pivot
--条件1
在每一轮运行结束时,各位置有如下含义:
-
i >= j
-
s[i] >= pivot
,s[j] <= pivot
-
s[l..i-1] <= pivot
-
s[j+1...r] >= pivot
所以 s[l,j] <= pivot、s[j+1,r] >= pivot
,满足 条件1
,划分合理
但 s[i] >= pivot
,所以 s[l,i]
并不满足<= pivot
,虽然 s[i+1,r] >= pivot
成立,但还是不满足 条件1
,所以不能使用i进行划分
3. 那如何使用i进行区间划分?
可以划分为 [l, i-1]
和 [i, r]
,此时就满足条件1
了
同时需要将pivot
的求法改为
int pivot = s[(l+r+1)>>1];
为什么?先看下出错案例:
输入:
2
1 2
算法模拟:
# 第一轮
下标:[0,1]
pivot = 0
执行结束时:i = 0, j = 0
划分为[0,-1]和[0,1]
# 第二轮,左边子数组
[0,-1], 返回不执行
# 第二轮,右边子数组
[0,1]
下标和起始状态一样,显然之后会陷入死循环
死循环的原因:
当数组有序,且l和r
相差较小时,pivot=s[(l+r)>>1]=s[l]
(比如上面的例子)
于是起始状态为:i=l-1, j=r+1, pivot=s[l]
- 更新i:
i
移动一次等于l
,此时s[l]=pivot
,停止更新 - 更新j:因为有序,
j
会一直减到pivot
所在的位置s[l]
,此时j=l
- 此时
i = j
,退出循环,此时i=j=l
于是子数组 [l, i-1]
和 [i, r]
,就变成了 [l, l-1]
和 [l, r]
,从而陷入死循环
解决方法:保证i!=l
,即pivot不能等于l
而 (l+r+1)>>1
是向上取整,l和r
再接近, 求出来结果也等于r
,避免了pivot取到l
以r分割也是同理,当数组有序,且数组较小时,pivot=s[(l+r+1)>>1]=r
,此时i和j
会都移动到r
[l, j]
和 [j+1, r]
就等于 [l, r]
和 [r+1, r]
,从而陷入死循环
解决方法就是保证j!=r
,即pivot不能等于r
而 int pivot = (l+r)>>1
是向下取整,此时 pivot = l
,避免了pivot取到r
4.总结
在快排中,每层递归,都需要将数据划分成的两个子数组,且左边<= pivot
, 右边 >= pivot
--条件1
在每一轮运行结束时,各位置有如下含义:
-
j <= i
-
s[i] >= pivot
,s[j] <= pivot
-
s[l..i-1] <= pivot
-
s[j+1...r] >= pivot
为满足条件1
- 以
i
划分,区间可为[l,i-1]和[i,r]
- 以
j
划分,区间可为[l,j]和[j+1,r]
又为了防止下标停止更新,陷入死循环,
即[i,r]
不能取到[l,r]
,[l,j]
不能取到[l,r]
即以i
划分,pivot不能取到l
,以j
划分,pivot不能取到r
因此
-
以
i
划分,pivot
向上取整:int pivot = s[(l+r+1)>>1]
-
以
j
划分,pivot
向下取整:int pivot = s[(l+r)>>1]
注
二分算法也有同样的边界问题,解决思路也相同。