算法1
快速排序算法可以分为三个步骤:
- 选取
pivot
- 划分,根据选取的
pivot
将数组划分成小于pivot
的部分和大于pivot
的部分 - 递归求解小于
pivot
和大于pivot
的部分
一种比较简单的划分方式为 Lomuto Partition Scheme,这种划分方式是单向划分,我们选取数组最左边的元素q[l]
作为pivot
,之后从左到右扫描数组,将数组划分为小于pivot
和大于等于pivot
两部分,之后需要将pivot
交换到它对应的位置,注意这一步不能忽略,不然q[l]
永远都不会被移动。最后我们对划分好的子数组进行递归求解。这种划分方式保证了划分完后q[j]
就是第j - l + 1
大的数。
时间复杂度
这种划分方式在平均情况下时间复杂度为 O(nlogn),空间复杂度为 O(logn)。最坏情况为数组已排好序或者数组中的数都相等,此时每次划分只会将数组的长度减少1,会递归 n 次,导致时间复杂度为 O(n2),空间复杂度为 O(n)。
C++代码
void quick_sort(int q[], int l, int r) {
if (l >= r) return;
int x = q[l], j = l;
for (int i = l + 1; i <= r; i ++ )
if (q[i] < x) swap(q[ ++ j], q[i]);
swap(q[l], q[j]);
quick_sort(q, l, j - 1);
quick_sort(q, j + 1, r);
}
思考题
这里能否对代码进行改动使得它将数组划分成小于等于pivot
和大于pivot
的两部分,这样划分是否会影响正确性?
算法2
y总的模板,这种方式会将数组划分为小于等于和大于等于pivot
的左右两部分,并且每一部分都不会为空,因为每次递归的时候数组的长度都会变小从而确保不会死循环。如果每次都选取数组最左边的元素来作为pivot
,当数组已经是有序的时候每次递归数组的长度只会减少一,导致时间复杂度变为 O(n2),这可以通过选中间元素作为pivot
或者每次随机选取数组中一个元素与最左边的元素交换来解决。注意这里不能用最右边的元素作为pivot
,这样如果数组最右边是最大元素的话会导致划分完[l, j]
不变导致死循环。
由于划分完成后pivot
不一定在这两部分的分界线上,所以在做比如得到第k
大的数这种题目时不能用j - l + 1 == k
来判断q[j]
为第k大的数,因为左半区间只保证了所有数小于等于 pivot
,而不一定都小于等于q[j]
。
另外值得一提的是这种双向划分方式就是 Hoare 在最开始的论文中提出的方式 – Hoare Partition Scheme。
时间复杂度
平均时间复杂度 O(nlogn),最坏情况下 O(n2),在数组已排好序的情况下出现,可以通过随机化或者取中点来避免最差情况。
C++ 代码
void quick_sort(int q[], int l, int r) {
if (l >= r) return;
int x = q[l + r >> 1], i = l - 1, j = 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);
}
思考题
能否对代码进行改动使得它不使用do while
循环与++
操作符并保证其正确性?
算法3
将算法改成尾递归,可以使空间严格小于 O(logn),具体思想是把第一次递归调用改成永远先选长度较小的那一半,第二次递归调用改成循环(即尾递归)。这样由于每次都选较小的那一半,所以每次递归长度至少变小一半,递归栈不会超过 O(logn)。
C++ 代码
void quick_sort(int q[], int l, int r)
{
// 将第二次递归改为循环
while (l < r) {
int p = partition(q, l, r);
// 先递归较短的部分,可以把空间优化到O(logn)
if (p - l < r - p) {
quick_sort(q, l, p - 1);
l = p + 1;
} else {
quick_sort(q, p + 1, r);
r = p - 1;
}
}
}
附:思考题解答
- 可以,将判断条件由
q[i] < x
改成q[i] <= x
即可。 - 可以,这里附上一份参考代码:
void quick_sort(int q[], int l, int r)
{
if (l >= r) return;
int x = q[l + r >> 1], i = l, j = r;
while (1) {
while (q[i] < x) i = i + 1;
while (q[j] > x) j = j - 1;
if (i >= j) break;
swap(q[i], q[j]);
i = i + 1;
j = j - 1;
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
#include[HTML_REMOVED]
using namespace std;
const int N=1e7+5;
int q[N];
void quick_sort(int q[],int l,int r){
if(l>=r)
return ;
int x=q[l],i=l-1,j=r+1;
while(i[HTML_REMOVED]x);
if(i<j)
swap(q[i],q[j]);
}
quick_sort(q,l,j);
quick_sort(q,j+1,r);
}
int main(){
int n,i;
scanf(“%d”,&n);
for(i=0;i<n;i)
scanf(“%d”,&q[i]);
quick_sort(q,0,n-1);
for(i=0;i<n;i)
printf(“%d “,q[i]);
return 0;
}请问为什么提交会超时呢?
因为数据更新过不能选左右端点做x,选中间端点和随机数就行
请问楼主算法2:随机选pivot时要加代码判断避开最右边元素?
个人感觉不用。随机数(哪怕是碰巧为最右那个)交换后,i还是左边第一个,数组还会减小
实际上取区间中点仍然可以卡。。。
模板1一直TLE 好痛苦
我也是害
if (l >= r) return; 请问为什么去掉这句,会出现内存超限?
去掉这一句的话,函数的递归就不会结束了,他就会无限的继续......
递归出口啊
大佬,算法二的思考题解答,为什么把while(1)改成while(i<j)就不对呢?
循环结束后
i
和j
有可能相等,若此时j
位置的元素大于pivot
则j
应该在j-1
的位置上,而如果while循环的条件是i < j
就会导致不进入循环,从而破坏了[l, j]
都小于等于pivot
的循环不变式,可以将while (1)
改为while (i <= j)
第一种时间超了
因为算法一会在数组已排好序或者数组中的数都相等的情况下时间复杂度退化为O(n2),导致超时
这种超时怎么办,可不可以每次选中点,跟最左边或者最右边的交换一下
这样在数组中的数都相等的情况下依然会退化为 O(n2),需要用双向划分解决
双向划分(即y总模板)也是最坏n²。感觉是swap次数导致的差异,如果不对请指正。
第一种模板,假如是452632,这样排这个4怎么交换到中间来。可能是我没推好
如果小于4就与4的下一个数交换,感觉4是不是永远在前面
swap(q[l], q[j]);
最后会将pivot
放到对应的位置,如果想不清楚可以把算法中的每一步交换给输出出来,这样比较直观。第一种模板的思想是维护[l + 1, j]
这一段的数都小于pivot
,最后我们将pivot
放到位置j
上然后递归处理。谢谢 之前没看到swap(q[l],q[j])
y总那个面试时候想不起来,太巧了
大佬,算法2while循环里的if(i<j)是不是可以去掉
不行,因为当
i == j
的时候同样会进入while
循环,导致在swap
时会出现i > j
的情况。这个应该是总结最好的,收藏了,感谢!
请问在acwing如何收藏文章呀?
我明白了
模板4和1应该是一个意思,模板一可以用while (q[++i] < x) ;来代替do while
模板4可以用在没有
do while
循环和++
操作符的地方,比如JavaScript用快排做topk的时候想到了这个问题,看了一下,你是解释的最好的。y总的模板最巧但可修改性也最低