引言
今天登了一下几乎搁置了一两年的洛谷账号,发现自己有一个80分的提交记录,是当年一开始学快排的时候交的模板题,卡在最后一个TLE的点。估计当时自己也忘了去优化AC了(也可能是因为懒 )
今天看到了,就想着去优化一下那份代码,正好重温一下这个经典算法~~~本文主要写了一些实现时候的细节优化以及关于快排思路在其他情景上的应用。
先放一个检验快排真假的提交地址:快速排序模板
快速排序核心思想
快排的核心思想就是每一次选取一个主元 p i v o t pivot pivot,然后每一轮将比 p i v o t pivot pivot小的数放在它左边,比 p i v o t pivot pivot大的数放在它右边,然后递归的对 p i v o t pivot pivot左右两边的数组部分进行以上操作。由于每一轮对于 p i v o t pivot pivot的左右重排是 O ( n ) O(n) O(n)的,在每次 p i v o t pivot pivot划分的时候两边的大小相当的情况下,这个递归树的深度就是 l o g ( n ) log(n) log(n)的,所以快速排序的平均时间复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))。根据一些数据表明,快速排序在实际应用场景下,是目前平均效率最高的一种内部排序算法。
顺便一提, C + + C++ C++的 S T L STL STL中的 s o r t sort sort远不是一个快速排序那么简单,其中结合了插入排序、快速排序、堆排序等方法并且优化了非常多的细节。
快速排序实现
快速排序的思想很简单,但是实现起来却有各式各样的版本,实现起来也有一些细节。这里主要考虑以下两个细节:
- 主元 p i v o t pivot pivot的选取(传统选法/三轴取中/随机算法)
- 在双指针指向元素与主元进行比较时是否取等号
(1) 主元 p i v o t pivot pivot的选取
在清华大学严蔚敏教授的《数据结构》教材中,是直接将待划分数组部分的第一个元素作为 p i v o t pivot pivot的。这样做有一个很明显也很致命的缺点:假设一个数组已经基本有序,那么每一次选取第一个主元作为划分依据将会导致每一次的左右划分非常不均衡,这样会使得递归树的深度急剧增加,快速排序的时间复杂度将退化至 O ( n 2 ) O(n^2) O(n2)。
因此,在具体实现时,我们一般用三轴取中或者是随机选取主元的方式来避免这种极端的数据。
- 三轴取中源代码
int selectPivot(int nums[], int left, int right){
if(right - left < 3) return left;
int mid = (left + right) / 2;
if(nums[left] > nums[right]) swap(left, right);
if(nums[right] < nums[mid]) return right;
if(nums[mid] < nums[left]) swap(left, mid);
return mid;
}
- 随机选取主元的cpp源代码
int selectPivot(int nums[], int left, int right){
//随机选取主元
srand(int(time(0)));
int i = rand() % (right - left + 1);
return i + left;
}
(2) 实现主体的细节
在实现的主体代码中,主要需要留意一下双指针 i i i和 j j j的一些细节。在清华大学严蔚敏教授的《数据结构》教材中,在双指针指向元素与 p i v o t pivot pivot进行比较时,是取了等号的。这样做也有一个很明显的问题,我们考虑如下数据: n = 100000 , a [ ] = { 1 , 1 , 1 , 1 , 1 , 1 , . . . , 1 } n=100000,a[]=\{ {1,1,1,1,1,1,...,1}\} n=100000,a[]={ 1,1,1,1,1,1,...,1}。在取了等号的情况下,会使得每一次对于该数组的划分也是极其不均衡的,因为前面那个指针总是会从一端移动到另一端。所以这个地方不取等号会有一定程度的优化。
- 完整版的手写快排源代码(在洛谷上已AC)
#include<bits/stdc++.h>
using namespace std;
class Solution {
public:
int n;
int selectPivot(int nums[], int left, int right){
srand(int(time(0)));
int i = rand() % (right - left + 1);
return i + left;
}
int partition(int nums[], int low, int high){
int i, j;
i = low + 1;j = high;
int p = selectPivot(nums, low, high);
swap(nums[low], nums[p]);//第0位置存放主元
while(i <= j){
//一定要有等号,比如只有两个数的边界情况[4,5]就会出错
while(nums[j] > nums[low]) --j;
while(i <= high && nums[i] < nums[low]) ++i;
if(i <= j){
//要有等号
swap(nums[i], nums[j]);
++i;--j;
}
}
swap(nums[low], nums[j]);
return j;
}
void quickSort(int nums[], int left, int right) {
if(left < right){
int pos = partition(nums, left, right);
quickSort(nums, left, pos - 1);
quickSort(nums, pos + 1, right);
}
}
}s;
int main(){
int n, m;
int nums[101000];
ios::sync_with_stdio(false);
cin>>n;
s.n = n;
for(int i=0;i<n;++i){
cin>>nums[i];
}
s.quickSort(nums, 0, n - 1);
cout<<nums[0];
for(int i=1;i<n;++i){
cout<<' '<<nums[i];
}
return 0;
}
- 经典的不加优化的快排 p a r t i t i o n partition partition(从网上找的代码)
public static int partition(int a[], int low, int high){
int x = a[low]; //将该数组第一个元素设置为比较元素
int i=low;
int j=high;
while(i < j){
while(i<j && a[j] >= x)
j--; //从右至左找到第一个小于比较元素的数
while(i<j && a[i] <= x)
i++; //从左至右找到第一个大于比较元素的数
/*需要注意的是,这里的j--与i++的顺序不可以调换!
*如果调换了顺序,i会走过头,以至于将后面较大的元素交换到数组开头*/
swap(a, i, j);
}
swap(a, i, low); //将比较元素交换到期望位置
return i; //返回比较元素的位置
}
关于快排思想的迁移情景
- 求第 k k k小数的问题
这个问题可以利用快排的思想,每一次划分可以用两边的划分规模和 k k k进行比较,从而得到解决。