使用C++更适合参加这类比赛,我们也不用担心要学一门新语言,因为只是学算法的话用的也就是C和C++的STL,而这些STL我们只需要一个一个积累就行了,就是用到一个就学习一个,而且也可以自己用数组实现,所以算法学习应该是不需要太多基础的,但是,请一定要熟悉递归、递推等基本内容,这对于理解算法是十分重要的。
引入:
排序是我们很常用的算法,在初学C阶段,我们都会写冒泡排序,但是毫无疑问O(n2)的时间复杂度并不是我们能接受的,因此我们将介绍一些更优的排序方法,他们都有各自的应用场景,熟悉他们的原理是学会应用的基础。
顾名思义,就是很快的排序方法,快排的实现有很多种方法,我在这里就挑一种我认为比较简单的方法来讲解。
我们采取的是一种类似分治的思想,把一个区间分为两个区间,想办法让第一个区间内的数都小于等于第二个区间内的数,再把这两个区间各分为两个区间且满足前一个区间的数都小于等于后一个区间内的数,这样不断分下去,最后整个数组就是有序的了。想法是有了,但我们怎么实现呢?
其实也不难,我们只需先选定一个界定值,然后把大于等于这个值的数都放在第二个区间,小于等于这个值的数都放在第一个区间,就完成了第一步,后续只要不断递归操作每个区间,便能完成排序了。
话不多说,直接上代码+样例详解,这样更有利于理解。
/* 样例 5(代表要排序的数字个数) 3 1 2 4 5(要排序的数) */ #include<iostream> using namespace std; const int N=1e5+10;//养成写算法的好习惯 int n; int arr[N]; void myqsort(int l,int r){ if(l>=r) return ;//如果当前区间内只有一个数,那么就不用继续了,直接返回 //因为下面我们写成++i,--j,因此i要初始化为l-1,j要初始化为r+1 int i=l-1,j=r+1,mid=arr[l+r>>1];//mid为界定值,>>运算符优先级较低,不用加括号,相当于(l+r)/2,但位运算更快 while(i<j){ //这里不必担心i,j的越界问题,因为他们至少会遇到这个界定值的数 while(arr[++i]<mid);//从左边开始搜索大于等于mid的数 while(arr[--j]>mid);//从右边开始搜索小于等于mid的数 if(i<j) swap(arr[i],arr[j]);//如果i<j,则交换两数 } //再分别对两个区间排序 myqsort(l,j); myqsort(j+1,r); } int main(){ cin>>n; for(int i=0;i<n;i++){ cin>>arr[i]; } myqsort(0,n-1); for(int i=0;i<n;i++){ cout<<arr[i]<<" "; } return 0; }
我们来看一下对于这个样例,我们的程序是如何计算的:
图中i,j表示的是下标,mid表示的是界定值,l,r表示区间左、右端点
其实,mid是可以选任意一个数的,但选择中间的情况最好,因为可能会有比较特殊的情况,当你选择最左边或最右边的数作为mid时,可能会花费很多时间,导致超时,感兴趣的读者可以自行思考。
其实,真正写算法题时我们一般不用自己写快排,c++头文件<algorithm>
中为我们提供了快排函数。
第一个参数是要排序的起始位置,第二个参数是终止位置的后一个位置,我们还可以自定义一个函数,并作为最后一个参数来定义各种类型的排序
sort(arr,arr+n,cmp);
这样一想,自己手写快排好像没什么用了?
并不一定,当我们遇到下面这个问题:
你用sort函数的话就有可能超时,不过c++还是提供了一个强大的函数——nth_element(),想了解的自行百度,这里我们用自己手写的快排进行优化来求解
其实思想很简单,如果第k小的数在左边,我们便只对左边排序,否则就对右边排序,这样就能节省很多时间。
#include<iostream> using namespace std; const int N=1e5+10; int n,k; int arr[N]; void myqsort(int l,int r){ if(l>=r) return ; int i=l-1,j=r+1,mid=arr[l+r>>1]; while(i<j){ while(arr[++i]<mid); while(arr[--j]>mid); if(i<j) swap(arr[i],arr[j]); } if(k-1<=j) myqsort(l,j); else myqsort(j+1,r); } int main(){ ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n>>k; for(int i=0;i<n;i++){ cin>>arr[i]; } myqsort(0,n-1); cout<<arr[k-1]; return 0; }
归并排序采用的也是分治思想,你可能会想,我都会快排了,还学这个干嘛?但是当你遇到下面这个问题:
暴力求解是一种方法,但会超时,要知道这道题的一种O(nlogn)解法,我们需要先了解归并排序。
#include<iostream> using namespace std; const int N=1e5+10; int n; int arr[N],tmp[N]; void merge_sort(int l, int r){ if(l>=r) return ; int mid=l+r>>1; merge_sort(l,mid); merge_sort(mid+1,r); int i=l,j=mid+1,s=0; while(i<=mid&&j<=r){ if(arr[i]<arr[j]) tmp[s++]=arr[i++]; else tmp[s++]=arr[j++]; } while(i<=mid) tmp[s++]=arr[i++]; while(j<=r) tmp[s++]=arr[j++]; for(int i=l;i<=r;i++){ arr[i]=tmp[i-l]; } } int main(){ ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=0;i<n;i++){ cin>>arr[i]; } merge_sort(0,n-1); for(int i=0;i<n;i++){ printf("%d ",arr[i]); } return 0; }
上面的代码有什么效果呢,我们用一个样例来理解
5
3 1 2 4 5
其实我们相当于把整个数组均分为两份,再分别对这两份进行排序,最后把整个数组排序,这样得到的整个数组就有序了。这里我们额外开了一个临时数组,更方便操作。
整个排序流程是这样的:
但这中排序和逆序对数量有什么联系呢?
我们会发现,当我们合并数组时,如果左区间的数还没合并完,右区间的数就合并上了,此时左区间剩余未合并的数(数的数目为mid-i+1)均大于该数,则我们可以在递归排序的过程中顺势把逆序对数量求出来。
#include<iostream> using namespace std; const int N=1e5+10; int n; int arr[N],tmp[N]; unsigned long long ans=0; void merge_sort(int l, int r){ if(l>=r) return ; int mid=l+r>>1; merge_sort(l,mid); merge_sort(mid+1,r); int i=l,j=mid+1,s=0; while(i<=mid&&j<=r){ if(arr[i]<=arr[j]) tmp[s++]=arr[i++]; else { tmp[s++]=arr[j++]; ans+=mid-i+1; } } while(i<=mid) tmp[s++]=arr[i++]; while(j<=r) tmp[s++]=arr[j++]; for(int i=l;i<=r;i++){ arr[i]=tmp[i-l]; } } int main(){ ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n; for(int i=0;i<n;i++){ cin>>arr[i]; } merge_sort(0,n-1); cout<<ans; return 0; }
你已经学会了两种算法,你可能会问,为什么还要学桶排序?其实也是应用场景的原因。
桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。
桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效。
下面来介绍一个桶排序的典型例题:
这题中前面干草堆的处理用到了前缀和,这个我们后面会讲,我们现在只需关注后面的排序部分,因为K<=25000,干草堆高度的数据范围较小,因此我们可以开辟一个25001大小的数组,其中b[i]就代表高度为i的干草堆的数量,这样的时间复杂度是O(n)级别的,详细的看代码
#include<bits/stdc++.h>//万能头文件 using namespace std; int s[1000010],b[25010]; int main(){ int n,m; cin>>n>>m; for(int i = 1;i<=m;++i){ int a,b; cin>>a>>b; s[a]++; s[b+1]--; } for(int i = 1;i<=n;++i)s[i]+=s[i-1];//前缀和处理,可忽略,只需知道最终s[i]中存储的是第i个干草堆的高度即可 for(int i = 1;i<=n;++i)b[s[i]]++; //s[i]<=25000 int k = (n+1)/2;//中间 for(int i = 0;i<=25000;++i){ k-=b[i]; if(k<=0){ cout<<i; return 0; } } return 0; }
这样看来,桶排序也是有它的妙用的。
总结:各种排序方式各有特点,我们应当熟悉他们的特点,才能在做题时找到合适的解法!
这一节的博客就到这里了,下节预告:二分查找与二分答案