我们通常会用到以下这些排序算法:
1.选择排序,插入排序,冒泡排序
2.堆排序,归并排序,快速排序
3.计数排序,基数排序,桶排序
前两类是基于比较的排序算法,对n个元素进行排序时,若元素比较大小的时间复杂度为0(1),则第一类排序算法的时间复杂度为0(n^2),第二类排序算法的时间复杂度为0(nlogn)。实际上,基于比较的排序算法的时间复杂度下界为0(nlogn),因此堆排序,归并排序与快速排序已经是时间复杂度最优的基于比较的排序算法。
第三类算法换了一种思路,它们不直接比较大小,而是对排序的数值采取按位划分、分类映射等处理,其时间复杂度不仅与n有关,还与数值的大小范围m有关。
离散化
通俗的说,“离散化”就是把无穷大集合中的若干个元素映射为有限集合以便于统计的方法。例如在很多情况下,问题的范围虽然定义在整数集合z,但是只涉及其中m个有限数值,并且与数值的绝对大小无关(只把这些数值作为代表,或只与它们的相对顺序有关)此时,我们就可以把整数集合z中的这m个整数与1~m建立映射关系。如果有一个时间、空间复杂度与数值范围z的大小有关的算法,在离散化后,该算法的时间、空间复杂度就降低为与m相关。
具体的说,假设问题涉及int范围内的n个整数a[1]~a[n],这n个整数可能有重复,去重以后共有m个整数。我们要把每个整数a[i]用一个1~m之间的整数代替,并且保持大小顺序不变,即如果a[i]小于(或等于、大于)a[j],那么代替a[i]的整数也小于(或等于、大于)代替a[j]的整数。
很简单,我们可以把a数组排序并去掉重复的值,得到有序数组b[1]~b[m],在b数组的下标i与数值b[i]之间建立映射关系。若要查询整数i(1<=i<=m)代替的数值,只需直接返回b[i];若要查询整数a[j](1<=j<=n)被哪个1~m之间的整数代替,只需要数组b中二分查找a[j]的位置即可。
void discrete(){
sort(a+1,a+1+n);
for(int i=1;i<=n;i++){ //也可以STL的unique函数
if(i==1||a[i]!=a[i-1])
b[++m] = a[i];
}
}
int query(int x){ //查询x映射为1~m之间的那个数
return lower_bound(b+1,b+1+m,x)-b;
}
题目零:区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是0。
现在,我们首先进行 n 次操作,每次操作将某一位置x上的数加c。
接下来,进行 m 次询问,每个询问包含两个整数l和r,你需要求出在区间[l, r]之间的所有数的和。
输入格式
第一行包含两个整数n和m。
接下来 n 行,每行包含两个整数x和c。
再接下里 m 行,每行包含两个整数l和r。
输出格式
共m行,每行输出一个询问中所求的区间内数字和。
数据范围
−10^9≤x≤10^9,
1≤n,m≤10^5,
−10^9≤l≤r≤10^9,
−10000≤c≤10000
输入样例:
3 3
1 2
3 6
7 5
1 3
4 6
7 8
输出样例:
8
0
5
题目思想:由于数轴大太,不可能初始化数轴那么大的一个数组,所以只能用离散化的思想,用alls数组存放要用到的x,l,r然后排序去重,再映射到a数组中,再求a数组的前缀和。
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef pair<int,int> PII;
const int N = 300010;
int n,m;
int a[N],s[N];
vector<int>alls;
vector<PII> add,query;
int find(int x){
int l = 0;
int r = alls.size()-1;
while(l<r){
int mid = l+r >> 1;
if(alls[mid]>=x) r = mid;
else l = mid+1;
}
return l+1; //前缀和从1开始
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++){
int x,c;
cin>>x>>c;
add.push_back({x,c});
alls.push_back(x);
}
for(int i=0;i<m;i++){
int l,r;
cin>>l>>r;
query.push_back({l,r});
alls.push_back(l);
alls.push_back(r);
}
sort(alls.begin(),alls.end());
alls.erase(unique(alls.begin(),alls.end()), alls.end()); //去重
for(auto temp:add){
int x = find(temp.first);
a[x] += temp.second;
}
for(int i=1;i<=alls.size();i++) s[i] = s[i-1] + a[i] ;
for(auto temp:query){
int l = find(temp.first);
int r = find(temp.second);
cout<<s[r] - s[l-1]<<endl;
}
return 0;
}
题目一:电影
有m部正在上映的电影,每部电影的语音和字幕都采用不同的语言,用一个int范围内的整数来表示语言,有n个人相约一起去看其中一部电影,每个人只会一种语言,如果一个人能听懂电影的语音,他会很高兴,如果能看懂字幕,他会比较高兴,如果语音和字幕都不懂,他会不开心,请你选择一部电影让这n个人一起看, 使很高兴的人数最多,若答案不唯一,则在此前提下再让比较高兴的人最多,n,m<=200000
题目思路:虽然说题目涉及到int范围的语言数,但是实际上n个人,m部电影,最多涉及到2m+n种语言。我们可以把所有电影和人涉及的语言放入一个数组,排序并离散化。用一个1~2m+n之间的整数代替每种语言。此时我们就可以用数组直接统计会上述每种语言的人的数量。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 200010;
int a[N],b[N],c[N]; // 数组a表示科学家能懂的语言,数组b表示每个电影的语音所用的语言,数组c表示每个电影字幕所用的语言
int n,m; //n表示科学家的数量 ,m表示电影的数量
int sum[3*N],sumi,ans[3*N],cnt; //sum数组表示所有电影和人涉及的语言放入一个数组,然后排序离散化。
//ans表示统计开心度的一个数组,cnt表示排序离散化后的数组长度
int lBound(int x){
int l= 0,r=cnt-1;
while(l<r){
int mid=(l+r)>>1;
if(sum[mid]>=x) r=mid;
else l=mid+1;
}
return l;
}
int main(){
cin>>n;
for(int i=0;i<n;i++)
{//输入数组a
cin>>a[i];sum[sumi++] = a[i];
}
cin>>m;
for(int i=0;i<m;i++)
{//输入数组b
cin>>b[i];sum[sumi++]=b[i];
}
for(int i=0;i<m;i++)
{//输入数组c
cin>>c[i];sum[sumi++]=c[i];
}
sort(sum,sum+sumi);
cnt = unique(sum,sum+sumi)-sum; //排序去重
for(int i=0;i<n;i++){
ans[lBound(a[i])]++; //统计每个人会的语言的人数。
}
int res =0;
int resx=0,resy=0;
for(int i=0;i<m;i++){ //遍历m部电影,获取每部电影让观众很开心和比较开心的人数
int h1 = ans[lBound(b[i])];//第i+1部电影,最开心的人数
int h2 = ans[lBound(c[i])]; //第i+1部电影,比较开心的人数
if( h1>resx || (h1==resx&&h2>resy)){ //要么就是很开心的人数最多,要么就是存在多个很开心人数相等的情况 ,再比较 比较开心的人数
res=i+1; //获得电影的编号,因为遍历是从0开始,所以要加1
resx=h1;
resy=h2;
}
}
cout<<res<<endl;
return 0;
}
中位数:在有序序列中,中位数具有很好性质,
题目二:货仓选址
在一条数轴上有 N 家商店,它们的坐标分别为 A1~AN。
现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。
为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
输入格式
第一行输入整数N。
第二行N个整数A1~AN。
输出格式
输出一个整数,表示距离之和的最小值。
数据范围
1≤N≤100000
输入样例:
4
6 2 9 1
输出样例:
12
题目思想:把A[1]到A[N]进行排序,设货仓建在x坐标处,x左侧的商家有P家,右侧有Q家,若P<Q,则每把货仓的选址向右移动1单位距离,距离之和就会变小Q-P。同理,若P>Q,则货仓的选址向左移动会使距离之和变小,当P=Q是为最优解,因此货仓应该建在中位数上。即把A排序后,当N是奇数时,货仓建在A[(N+1)/2]处最优,当N为偶数时,货仓建在A[N/2~N/2+1]之间的任何位置都是最优解。
#include<iostream>
#include<algorithm>
using namespace std;
const int N =100010;
int a[N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
}
sort(a+1,a+n+1);
int mid = (n+1)/2;
int sum=0;
for(int i=1;i<=n;i++){
sum+=abs(a[mid]-a[i]);
}
cout<<sum<<endl;
。 return 0;
}
题目三:动态中位数
动态维护中位数问题:依次读入一个整数序列,每当已经读入的整数个数为奇数时,输出已读入的整数构成的序列的中位数。
输入格式
第一行输入一个整数P,代表后面数据集的个数,接下来若干行输入各个数据集。
每个数据集的第一行首先输入一个代表数据集的编号的整数。
然后输入一个整数M,代表数据集中包含数据的个数,M一定为奇数,数据之间用空格隔开。
数据集的剩余行由数据集的数据构成,每行包含10个数据,最后一行数据量可能少于10个,数据之间用空格隔开。
输出格式
对于每个数据集,第一行输出两个整数,分别代表数据集的编号以及输出中位数的个数(应为数据个数加一的二分之一),数据之间用空格隔开。
数据集的剩余行由输出的中位数构成,每行包含10个数据,最后一行数据量可能少于10个,数据之间用空格隔开。
输出中不应该存在空行。
数据范围
1≤P≤1000,
1≤M≤9999
输入样例:
3
1 9
1 2 3 4 5 6 7 8 9
2 9
9 8 7 6 5 4 3 2 1
3 23
23 41 13 22 -3 24 -31 -11 -8 -7
3 5 103 211 -311 -45 -67 -73 -81 -99
-33 24 56
输出样例:
1 5
1 2 3 4 5
2 5
9 8 7 6 5
3 12
23 23 22 22 13 3 5 5 3 -3
-7 -3
题目思路:为了动态维护中位数,我们可以建立两个二叉堆:一个小根堆,一个大根堆。在依次读入这个整数序列的过程中,设当前序列长度为M,我们始终保持:
1.序列中从小到大排名1~M/2的整数存储在大根堆中
2.序列中从小到大排名为M/2+1~M的整数存储在小根堆中。
任何时候,如果某一个堆中元素个数过多,打破了这个性质,就取出该堆的堆顶插入另一个堆。这样一来,序列的中位数就是小根堆的堆顶。
每次新读入一个数值X后,若X比中位数小,则插入大根堆,否则插入小根堆,在插入之后检查并维护上述性质即可。这就是“对顶堆”算法。
#include<iostream>
#include<algorithm>
#include<queue>
#include <cstring>
using namespace std;
int main(){
int t;
cin>>t;
while(t--){
int n,m;
cin>>m>>n;
cout<<m<<" "<<(n+1)/2<<endl;
int cnt=0;
priority_queue<int> max_heap; //大根堆
priority_queue<int, vector<int>, greater<int>> min_heap;//小根堆
for(int i=0;i<n;i++){
int x;
cin>>x;
max_heap.push(x);
if(min_heap.size()&&min_heap.top()<max_heap.top()){ //当小根堆不空,且小堆垠的堆顶小于大根堆的堆顶时,就互相交换堆顶
auto a = max_heap.top();
auto b = min_heap.top();
max_heap.pop();min_heap.pop();
max_heap.push(b);min_heap.push(a);
}
if(max_heap.size()>min_heap.size()+1){ //由于最后输入的中位数是大根堆的堆顶,
//所以当大根堆中结点数为小根堆中结点数多1时,正好中位数在大根堆的堆顶,
//1 2 3 4 5在大根堆,6 7 8 9在小根堆,而5正好是中位数。
min_heap.push(max_heap.top());
max_heap.pop();
}
if((i+1)%2==1){
cout<<max_heap.top()<<" ";
if(++cnt%10==0) cout<<endl;
}
}
if (cnt % 10) puts("");
}
return 0;
}
题目五:超快速排序
在这个问题中,您必须分析特定的排序算法----超快速排序。
该算法通过交换两个相邻的序列元素来处理n个不同整数的序列,直到序列按升序排序。
对于输入序列9 1 0 5 4,超快速排序生成输出0 1 4 5 9。
您的任务是确定超快速排序需要执行多少交换操作才能对给定的输入序列进行排序。
输入格式
输入包括一些测试用例。
每个测试用例的第一行输入整数n,代表该用例中输入序列的长度。
接下来n行每行输入一个整数ai,代表用例中输入序列的具体数据,第i行的数据代表序列中第i个数。
当输入用例中包含的输入序列长度为0时,输入终止,该序列无需处理。
输出格式
对于每个需要处理的输入序列,输出一个整数op,代表对给定输入序列进行排序所需的最小交换操作数,每个整数占一行。
数据范围
0≤N<500000,
0≤ai≤999999999
输入样例:
5
9
1
0
5
4
3
1
2
3
0
输出样例:
6
0
题目思想:只通过比较和交换相邻两个数值的排序方法,实际就是冒泡排序。在排序过程中每找到一对大小颠倒的相邻数值,把它们交换,就会使整个序列的逆序对个数减少1。最终排好序后逆序对个数为0,所以对a进行冒泡排序需要的最少交换次数就是序列a中逆序对的个数,我们直接使用归并排序求出a的逆序对就是本题的答案。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 500010;
int a[N],t[N];
long long merge_sort(int l,int r){
if(l==r) return 0;
int mid = l+r>>1;
long long res = merge_sort(l,mid)+merge_sort(mid+1,r);
int i=l,j=mid+1,k=0;
while(i<=mid&&j<=r){
if(a[i]<=a[j]) t[k++] = a[i++];
else{
res+=mid-i+1; //注意这个地方
t[k++] = a[j++];
}
}
while(i<=mid) t[k++] = a[i++];
while(j<=r) t[k++] = a[j++];
for(int i=l,j=0;i<=r;i++,j++) a[i] = t[j];
return res;
}
int main(){
int n;
while(cin>>n && n){ //这个输入方式也很有意思
for(int i=0;i<n;i++){
cin>>a[i];
}
cout<<merge_sort(0,n-1)<<endl;
}
return 0;
}
题目六:快速排序和第k大数
题目思路:无
#include<iostream>
#include<algorithm>
using namespace std;
const int N =100010;
int a[N];
void quick_sort(int l,int r){
if(l>=r) return;
int i=l-1,j = r+1;
int x = a[l+r>>1];
while(i<j){
do i++; while(a[i]<x);
do j--; while(a[j]>x);
if(i<j) swap(a[i],a[j]);
}
quick_sort(l,j);
quick_sort(j+1,r);
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
}
quick_sort(0,n-1);
for(int i=0;i<n;i++){
cout<<a[i]<<" ";
}
return 0;
}
#include<iostream>
#include<algorithm>
using namespace std;
const int N =100010;
int a[N];
int quick_sort(int l,int r,int k){
if(l>=r) return a[l];
int i=l-1,j=r+1;
int x = a[l+r>>1];
while(i<j){
do i++; while(a[i]<x);
do j--; while(a[j]>x);
if(i<j) swap(a[i],a[j]);
}
if(j-l+1>=k) return quick_sort(l,j,k); //注意这个地方
else return quick_sort(j+1,r,k-(j-l+1));
}
int main(){
int n,k;
cin>>n>>k;
for(int i=0;i<n;i++){
cin>>a[i];
}
cout<<quick_sort(0,n-1,k);
return 0;
}
题目七:归并排序和逆序对
题目思路:无
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N],t[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,k=0;
while(i<=mid&&j<=r){
if(a[i]<=a[j]) t[k++] = a[i++];
else t[k++] = a[j++];
}
while(i<=mid) t[k++] = a[i++];
while(j<=r) t[k++] = a[j++];
for(i=l,j=0;i<=r;i++,j++) a[i] = t[j];
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
merge_sort(0,n-1);
for(int i=0;i<n;i++){
cout<<a[i]<<" ";
}
return 0;
}
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
int a[N],t[N];
long long merge_sort(int l,int r){
if(l>=r) return 0;
int mid = l+r>>1;
long long res = merge_sort(l,mid)+merge_sort(mid+1,r);
int i=l,j=mid+1,k=0;
while(i<=mid&&j<=r){
if(a[i]<=a[j]) t[k++] = a[i++];
else {
res+=mid-i+1; //注意这里
t[k++] = a[j++];
}
}
while(i<=mid) t[k++] = a[i++];
while(j<=r) t[k++] = a[j++];
for(i=l,j=0;i<=r;i++,j++) a[i] = t[j];
return res;
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
cout<<merge_sort(0,n-1);
return 0;
}
题目八:七夕祭和奇数码