快速排序(朴素)
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
/*实际过程见算法笔记p142、p143*/
int partition(int a[],int left,int right)
{
int temp=a[left];
while(left<right)
{
while(left<right&&a[right]>temp) right--;//如果right所指的元素大于主元temp,则指针不断左移至找到一个小于等于主元temp的
a[left]=a[right];//当前a[right]小于主元temp,于是将right上的元素挪到left上
while(left<right&&a[left]<=temp) left++;//如果left所指的元素小于等于主元temp,则指针不断右移至找到一个大于主元temp的
a[right]=a[left];
}
a[left]=temp;//现在已经满足left左边全部小于等于temp,right右边全部大于temp,于是把主元temp放在正确的位置,即left==right(两个pointer相遇的地方)
return left;//注意本行与上行中left==right,本行代表返回主元经一轮交换后的位置
}
void quick_sort(int a[],int left,int right)//left、right初值分别为待排序序列首尾下标
{
if(left<right)//当前待排序的子区间长度不超过1
{
int pos=partition(a,left,right);
quick_sort(a,left,pos-1);//对左子区间快排
quick_sort(a,pos+1,right);//对右子区间快排
}
}
signed main(void)
{
int n;
int a[1000];
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
quick_sort(a,1,n);
for(int i=1;i<=n-1;i++)
cout<<a[i]<<" ";
cout<<a[n]<<endl;
return 0;
}
快速排序算法针对完全无序时效率最高,但当接近有序时,效率会达到最坏的O(n²),产生这种情况的主要原因是主元没有把当前区间划分为两个长度接近的子区间,解决的办法就是随机选择主元,即对于a[left...right],不总是以a[left]作为主元,而是从a[left]、a[left+1]...a[right]中随机挑选一个,这样的话尽管最坏复杂度还是O(n²)(每次都选择a[left]为主元),但总体期望复杂度都能达到O(nlogn)。(详细证明见算法导论)
关于随机数的生成
1.main函数开头加
srand((unsigned)time(NULL));//初始化随机数种子
然后即可在需要使用随机数的地方使用rand();
2.rand()函数只能是[0,RAND_MAX]范围内的整数
因此如果想要实现[a,b]的整数的话,则需要使用rand()%(b-a+1)+a
生成大范围随机数则可多个rand()相乘例如,生成[10000,60000]则可以rand()*and()%(60000-10000+1)+10000
快速排序(随机主元)
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
/*实际过程见算法笔记p142、p143*/
int rand_partition(int a[],int left,int right)
{
int random_num=(rand()*rand())%(right-left+1)+left;
std::swap(a[random_num],a[left]);//就比朴素多了两行
int temp=a[left];
while(left<right)
{
while(left<right&&a[right]>temp) right--;//如果right所指的元素大于主元temp,则指针不断左移至找到一个小于等于主元temp的
a[left]=a[right];//当前a[right]小于主元temp,于是将right上的元素挪到left上
while(left<right&&a[left]<=temp) left++;//如果left所指的元素小于等于主元temp,则指针不断右移至找到一个大于主元temp的
a[right]=a[left];
}
a[left]=temp;//现在已经满足left左边全部小于等于temp,right右边全部大于temp,于是把主元temp放在正确的位置,即left==right(两个pointer相遇的地方)
return left;//注意本行与上行中left==right,本行代表返回主元经一轮交换后的位置
}
void quick_sort(int a[],int left,int right)//left、right初值分别为待排序序列首尾下标
{
if(left<right)//当前待排序的子区间长度不超过1
{
int pos=rand_partition(a,left,right);
quick_sort(a,left,pos-1);//对左子区间快排
quick_sort(a,pos+1,right);//对右子区间快排
}
}
signed main(void)
{
int n;
int a[1000];
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
quick_sort(a,1,n);
for(int i=1;i<=n-1;i++)
cout<<a[i]<<" ";
cout<<a[n]<<endl;
return 0;
}
同理,借助rand_parition的特性,我们还可以实现
随机选择算法
现在有一个问题:求序列中第k小的值,当然我们有最直接的想法即直接排序,然后取第k个元素,但时间复杂度最优也是O(nlogn),我们借助rand_partition实现一个最差情况是O(n²)(但不存在数据使其最坏情况),通常复杂度为O(n)的算法:随机选择算法
(注意算法笔记上有笔误,把第k小全部写成了第k大,本文按照第k小的思路叙述)
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
/*实际过程见算法笔记p142、p143*/
int rand_partition(int a[],int left,int right)
{
int p=(rand()*rand())%(right-left+1)+left;
std::swap(a[p],a[left]);
int temp=a[left];
while(left<right)
{
while(left<right&&a[right]>temp) right--;//如果right所指的元素大于主元temp,则指针不断左移至找到一个小于等于主元temp的
a[left]=a[right];//当前a[right]小于主元temp,于是将right上的元素挪到left上
while(left<right&&a[left]<=temp) left++;//如果left所指的元素小于等于主元temp,则指针不断右移至找到一个大于主元temp的
a[right]=a[left];
}
a[left]=temp;//现在已经满足left左边全部小于等于temp,right右边全部大于temp,于是把主元temp放在正确的位置,即left==right(两个pointer相遇的地方)
return left;//注意本行与上行中left==right,本行代表返回主元经一轮交换后的位置
}
int rand_select(int a[],int left,int right,int k)//注意书上表示有误,将第k小写成第k大,这里仍然按照书上“第k大”的代码来写,但须知实际上是求出的第k小
{
if(left==right) return a[left];//别忘了边界
int p=rand_partition(a,left,right);
int m=p-left+1;
if(m==k) return a[p];
else if(m>k) rand_select(a,left,p-1,k);
else if(m<k) rand_select(a,p+1,right,k-m); //1 2 3 4 5 6 7
}
signed main(void)
{
srand((unsigned)time(NULL));
int n,c;
int a[1000];
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
while(~scanf("%d",&c))
{
cout<<"第"<<c<<"小的数为"<<rand_select(a,1,n,c)<<endl;
}
return 0;
}
现在我们来看一个使用随机选择算法的实例
分析不难发现,元素个数之差最小就是元素平均分配到两个子集中,然后考虑元素之和差的绝对值最大,那么考虑让第1,2,...,n/2小的元素为一个集合,第n/2+1,n/2+2,...n小的元素为另一个集合,可以实现差绝对值最大,那么如何实现呢,最直接的想法是排序以后分配,但这里可以采用随机选择算法,找出第n/2小的元素并按照它为中心左右划分两个区间,完成划分以后,左边下标为1,2,......,n/2的全部小于第n/2个元素,右边下标为n/2+1,n/2+2,.....,n的全部大于第n/2个元素,那么直接将左右边区间元素加起来即是s1,s2
注意:下标为i和第i小不一样!
/*
题目见算法笔记p149
*/
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
/*实际过程见算法笔记p142、p143*/
int rand_partition(int a[],int left,int right)
{
int p=(rand()*rand())%(right-left+1)+left;
std::swap(a[p],a[left]);
int temp=a[left];
while(left<right)
{
while(left<right&&a[right]>temp) right--;//如果right所指的元素大于主元temp,则指针不断左移至找到一个小于等于主元temp的
a[left]=a[right];//当前a[right]小于主元temp,于是将right上的元素挪到left上
while(left<right&&a[left]<=temp) left++;//如果left所指的元素小于等于主元temp,则指针不断右移至找到一个大于主元temp的
a[right]=a[left];
}
a[left]=temp;//现在已经满足left左边全部小于等于temp,right右边全部大于temp,于是把主元temp放在正确的位置,即left==right(两个pointer相遇的地方)
return left;//注意本行与上行中left==right,本行代表返回主元经一轮交换后的位置
}
void rand_select(int a[],int left,int right,int k)//本题不需要知道第n/2大的数是多少,就不需要返回值了,另外如果题目保证数据分布比较随机,可以直接partition划分
{
if(left==right) return;
int p=rand_partition(a,left,right);
int m=p-left+1;
if(m==k) return;
else if(m>k) rand_select(a,left,p-1,k);
else if(m<k) rand_select(a,p+1,right,k-m);
}
signed main(void)
{
srand((unsigned)time(NULL));
int n,ans=0,sum=0;
int a[1000];
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
ans+=a[i];
}
rand_select(a,1,n,n/2);
for(int i=1;i<=n/2;i++)
sum+=a[i];
cout<<ans-2*sum<<endl;
return 0;
}
/*
13
1 6 33 18 4 0 10 5 12 7 2 9 3
*/