深入浅出程序设计竞赛(洛谷基础篇) 第九章 排序

电子版教材链接
我通过百度网盘分享的文件:深入浅出程序设计…pdf
链接:https://pan.baidu.com/s/1kmF8wZLnK3Zci7s1ffjRzw
提取码:Ra3Q
复制这段内容打开「百度网盘APP即可获取」

例9-1 选举学生会
// 法一: 计数排序

#include <bits/stdc++.h>
using namespace std;
int n,m,arr[1005] = {0},tmp;

int main()
{
	cin >> n >> m;
	for(int i = 0;i<m;i++)
	{
		cin >> tmp;
		arr[tmp]++;
	}	
	for(int i = 1;i<=n;i++)
	{
		for(int j = 0;j<a[i];j++) // 把桶内对应的编号全部输出出来
		{
			cout << i << " ";
		}
	}
	cout << endl;
	return 0;
}

// 法二 使用库函数

#include <bits/stdc++.h>
using namespace std;
int n,m,arr[2000010];
int main()
{
	cin >> n >> m;
	for(int i = 0;i<m;i++)
	{
		cin >> arr[i];
	}
	sort(arr,arr+m);
	for(int i = 0;i<m;i++)
	{
		cout << arr[i] << " ";
	}
	cout << endl;
	return 0;
}
例9-2 数列排序
解法一:选择排序

原理 选择排序是一种原地比较排序算法,其基本思想是:
在未排序的序列中,每次选择最小(或最大)元素,将其放到已排序序列的末尾
实现过程

  1. 从第一个元素开始,向后扫描整个数组,找出最小值;
  2. 将这个最小值和当前位置的值交换;
  3. 继续处理后续的未排序部分,重复上述操作

算法复杂度
最优:O(n),最坏:O(n²),平均:O(n²) 有两轮for循环,所以复杂度是O(n* n)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int n,a[N];

int main()
{
	cin >> n;
	for(int i = 0;i<n;i++)
	{
		cin >> a[i];
	}
	for(int i = 0;i<n-1;i++)
	{
		for(int j = i+1;j<n;j++)
		{
			if(a[j]<a[i]){
				int p = a[i];
				a[i] = a[j];
				a[j] = p;
			}
		}
	}
	return 0;
}
解法二:冒泡排序

原理:每一轮通过相邻两元素比较并交换,把当前未排好序中最大(或最小)的数“冒”到末尾,就像气泡一样“冒上来”

核心机制

  • 比较相邻元素 arr[j] 和 arr[j+1]
  • 若顺序不对(比如升序中前一个更大),就交换
  • 每一轮确定一个最大值位置,放在末尾

算法复杂度
最坏:O(n^2)
最优:O(n)
平均:O(n^2)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int n,arr[N];
int main()
{
	cin >> n;
	for(int i = 0;i<n;i++)
	{
		cin >> arr[i];
	}
	for(int i = 0;i<n-1;i++)
	{
		for(int j = 0;j<n-i-1;j++)
		{
			if(a[j]>a[j+1]){
				int p = a[j];
				a[j] = a[j+1];
				a[j+1] = p;
			}
		}
	}
	return 0;
}

优化版本:当某一轮没有发生交换,说明已经有序,直接终止

bool swapped = false;
for(int i = 0; i < n - 1; ++i) {
    swapped = false;
    for(int j = 0; j < n - 1 - i; ++j) {
        if(arr[j] > arr[j+1]) {
            swap(arr[j], arr[j+1]);
            swapped = true;
        }
    }
    if(!swapped) break; // 提前结束
}

解法三:插入排序

原理 将一个元素插入到已排好序的序列中的合适位置

实现过程

  1. 从第一个元素开始,它默认是有序的;
  2. 取下一个元素,将它和前面排好序的序列从后往前比较;
  3. 如果发现比它大的元素,就将这些元素都向后移动;
  4. 找到合适位置插入当前元素;
  5. 重复步骤 2~4,直到所有元素处理完毕。

算法复杂度
最坏:O(n^2)
最优:O(n)
平均:O(n^2)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int n,a[N];
int main()
{
	cin >> n;
	for(int i = 0;i<n;i++)
	{
		cin >> arr[i];
	}
	for(int i = 1;i<n;i++)
	{
		int now = a[i],j; // 记录一下待插牌
		for(j = i-1;j>=0;j--)
			if(a[j]>now)
				a[j+1] = a[j];
			else break;
		a[j+1] = now;
		
	}
	return 0;
}
例9-3 快速排序

原理 快速排序是一种分治(Divide and Conquer)策略实现的排序算法

实现过程

  1. 选取一个“基准值”(pivot),通常是数组中的一个元素;
  2. 将数组划分为两部分
    • 小于等于 pivot 的元素放左边;
    • 大于 pivot 的元素放右边;
  3. 递归地对左右两个子数组继续排序
  4. 最终整个数组就是有序的。

算法复杂度推导

总复杂度可以抽象成:

T(n)=T(左区间)+T(右区间)+O(n) T(n) = T(\text{左区间}) + T(\text{右区间}) + O(n) T(n)=T(左区间)+T(右区间)+O(n)


最佳情况分析:每次均匀划分

假设每次都能把数组正好分成左右两半,则:

T(n)=2T(n2)+O(n) T(n) = 2T\left(\frac{n}{2}\right) + O(n) T(n)=2T(2n)+O(n)

这是经典的**主定理(Master Theorem)**形式:

T(n)=2T(n/2)+cn⇒T(n)=O(nlog⁡n) T(n) = 2T(n/2) + cn \Rightarrow T(n) = O(n \log n) T(n)=2T(n/2)+cnT(n)=O(nlogn)

也可以用递归树直观看出:

层数子问题个数每层总大小复杂度
01nnncncncn
12nnncncncn
24nnncncncn
nnncncncn
log⁡2n\log_2 nlog2nnnn个长度为1的子问题总共 nnncncncn

所以总共复杂度:
T(n)=cn⋅log⁡2n=O(nlog⁡n) T(n) = cn \cdot \log_2 n = O(n \log n) T(n)=cnlog2n=O(nlogn)


最坏情况分析:每次只分出一个元素

最糟糕的情况是每次分区时,pivot 是最大或最小值,一边是 n−1n-1n1,另一边是空集:

T(n)=T(n−1)+O(n) T(n) = T(n - 1) + O(n) T(n)=T(n1)+O(n)

递推展开:

T(n)=T(n−1)+cn=T(n−2)+c(n−1)+cn=T(n−3)+c(n−2)+c(n−1)+cn=⋯=T(1)+c(2+3+⋯+n) T(n) = T(n-1) + cn \\ = T(n-2) + c(n-1) + cn \\ = T(n-3) + c(n-2) + c(n-1) + cn \\ = \dots = T(1) + c(2 + 3 + \dots + n) T(n)=T(n1)+cn=T(n2)+c(n1)+cn=T(n3)+c(n2)+c(n1)+cn==T(1)+c(2+3++n)
=O(n2) = O(n^2) =O(n2)

所以最坏复杂度为 O(n2)O(n^2)O(n2),这就是为什么快速排序在有序/逆序数组上可能退化的原因


平均情况复杂度分析

我们考虑 任意排列下的期望比较次数。假设排序数组为 a1,a2,…,ana_1, a_2, \dots, a_na1,a2,,an,我们来考虑任意两个元素 aia_iaiaja_jaj 被比较的概率

关键观察:

  • aia_iaiaja_jaj 只有在某一次递归中作为同一子数组的成员并且其中一个是 pivot 时才会比较;
  • 一旦有一个中间的元素 aka_kak 先成为 pivot,把两者分开了,它们就永远不会比较了

所以我们可以证明:

P(ai 与 aj 比较)=2∣j−i∣+1 P(\text{$a_i$ 与 $a_j$ 比较}) = \frac{2}{|j - i| + 1} P(ai  aj 比较)=ji+12

于是总期望比较次数是:

C(n)=∑1≤i<j≤n2j−i+1 C(n) = \sum_{1 \le i < j \le n} \frac{2}{j - i + 1} C(n)=1i<jnji+12

这个和可以近似为:

C(n)≤2n⋅∑k=1n1k=O(nlog⁡n) C(n) \le 2n \cdot \sum_{k=1}^{n} \frac{1}{k} = O(n \log n) C(n)2nk=1nk1=O(nlogn)

这就是平均时间复杂度为 O(nlog⁡n)O(n \log n)O(nlogn) 的数学推导。

情况分析思路复杂度
最佳情况每次划分成相等子区间O(nlog⁡n)O(n \log n)O(nlogn)
最坏情况每次只去掉一个元素O(n2)O(n^2)O(n2)
平均情况所有 pivot 情况的概率期望O(nlog⁡n)O(n \log n)O(nlogn)
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int n,a[N];

void qsort(int a[],int l,int r)
{
	int i = l,j = r,flag = a[(l+r)/2],tmp;
	do{
		while(a[i]<flag) i++; // 从左找比哨兵大的数
		while(a[j]>flag) j--; // 从右找比哨兵小的数
		if(i<=j){
			tmp = a[i];
			a[i] = a[j];
			a[j] = tmp;
			i++;
			j--;
		}
	}
	while(i<=j);
	if(l<j) qsort(a,l,j); // 处理左半边
	if(i<r) qsort(a,i,r); // 处理右半边
	
}

int main()
{
	scanf("%d",&n);
	for(int i = 0;i<n;i++)
		scanf("%d",&a[i]);
	qsort(a,0,n-1);
	for(int i = 0;i<n;i++)
		printf("%d ",a[i]);
	return 0;
}
例9-4 求第k小的数字

通过使用快速排序的思想,我们实现一下快速选择

快排的底层原理是的快速选择的实现是线性复杂度(因为每次只需要对一个子区间进行划分(第k个元素所在的子区间),相较于快速排序的两个子区间划分要省去很多的复杂度)\

tips 本题一定要使用scanf和printf的输入输出形式,不然会超时(或者使用cin时加上取消限制流的代码也行),建议以后读者多用scanf,少用cin,scanf的读取速度一般来说是比cin快的多的,后续遇见超时的情况不妨也检查一下是否是读入过慢导致的问题

#include <bits/stdc++.h>
using namespace std;
int ans = 0,k,a[5000005],n;
void findkth(int a[],int l,int r)
{
	if(l >= r){
		ans = a[l]; // 区间长度为1时,记录答案,说明此时已经找到了第k小的数字
		return;
	}
	int i = l,j = r,tmp;
	int flag = a[(l+r)/2]; 
	do{
		while (a[i] < flag) i++;// 从左找比哨兵大的数
		while (a[j] > flag) j--;// 从右找比哨兵小的数
		if(i<=j){ // 交换
			tmp = a[i];
			a[i] = a[j];
			a[j] = tmp;
			i++;
			j--;
		}
	}while(i <= j);

	if(k<=j) findkth(a,l,j);  // 第k小的数字在左区间
	else if(i<=k) findkth(a,i,r); // 第k小的数字在右区间
	else findkth(a,j+1,i-1);  // 第k小的数字既不在左区间也不在右区间,这里的前置条件是j<k<i,即右指针和左指针已经交叉,说明此时的答案就在两者之间
} 
int main()
{
	scanf("%d %d",&n,&k);
	for(int i = 0;i<n;i++)
	{
		scanf("%d",&a[i]);
	}
	findkth(a,0,n-1);
	printf("%d",ans);
	return 0;
}
例9-5 明明的随机数
前置知识

sort
形式为 sort(a,a+n,cmp),表示对数组a[0]到a[n-1]进行排序,其中的cmp表示自定义排序函数,默认是从小到大排序(实际上就是写一个谓词来改变排序的判断法则)
时间复杂度为nlogn(底层的实现就是一个快速排序)
如果要实现从大到小进行排序,自定义一个cmp函数并且作为参数传入函数中即可

bool cmp(int a,int b)
{
	return a>b;  
}

unique
形式为 unique(a,a+n) 表示对数组a[0]到a[n-1]进行去重,前提要求数组是有序的,返回去重后最后一个元素对应的指针(不理解这句话就认为:将它减去a的指针得到的值就是去重后的元素个数)

// 法一:自去重
#include <iostream>
#include <algorithm> // 使用算法库函数调用函数sort
using namespace std;
int const MAXN = 1010;

int a[MAXN],ans[MAXN],n,cnt = 0,tmp = -1;

int main()
{
	cin >> n;
	for(int i = 0;i<n;i++) cin >> a[i];
	sort(a,a+n);
	for(int i = 0;i<n;i++){  // 实现对于数组的去重
		if(a[i] != tmp) ans[cnt++] = a[i];
		tmp = a[i];
	}
	cout << cnt << endl; // 输出去重后的剩余数字的个数
	for(int i = 0;i<cnt;i++)
	{
		cout << ans[i] << " ";
	}
	return 0;
}

//法二:使用函数unique
sort(a,a+n);
cnt = unique(a,a+n) -a;
cout << cnt << endl;
for(int i = 1;i<cnt;i++) cout << a[i] << " ";

例9-6 奖学金
#include <bits/stdc++.h>
using namespace std;
int const MAXN = 310;
int n;
struct student{
	int id,chinese,total;
}a[MAXN];

int cmp(student a,student b)
{
	if(a.total!=b.total) retuan a.total>b.total; // 总分高者优先
	if(a.chinese != b.chinese) return a.chinses > b.chinese; // 语文高者优先
	return a.id < b.id; // 序号低者优先
}

int main()
{
	cin >> n;
	for(int i = 0;i<n;i++)
	{
		int math,english;
		cin >> a[i].chinese >> math >> english;
		a[i].total = a[i].chinese + math + english;
		a[i].id = i+1;
	}
	sort(a,a+n,cmp);
	for(int i = 0;i<5;i++)
	{
		cout << a[i].id << " " << a[i].total << endl;
	}
	return 0
}
例9-7 宇宙总统
#include <bits/stdc++.h>
using namespace std;
int n;

struct leader{
	string x; //票数
	int num_id;
}arr[30];

//自定义结构体排序
bool cmp(leader a,leader b)
{
	if(a.x.length()!=b.x.length())
	{
		return a.x.length() > b.x.length(); //a比b的位数多的时候,将a放在前面
	}
	return a.x > b.x; //正常就按照字典序进行排序
}

int main()
{
	cin >> n;
	for(int i = 0;i<n;i++)
	{
		cin >> arr[i].x ;
		arr[i].num_id = i+1;
	}
	sort(arr,arr+n,cmp);
	cout << arr[0].num_id << endl;
	cout << arr[0].x << endl;
	return 0;
}

习题9-1 超级书架
#include <bits/stdc++.h>
using namespace std;

// 比较函数,从大到小进行排序
bool cmp(int x,int y)
{
	return x>y;
}

int main()
{
	int N,B,sum = 0,ans = 0;
	int arr[20005];

	cin >> N >> B;
	for(int i = 0;i<N;i++) cin >> arr[i];
	
	// 从大到小进行排序
	sort(arr,arr+N,cmp);

	// 累加元素直至满足条件
	for(int i = 0;i<N;i++)
	{
		if(sum >= B) break;
		sum += arr[i];
		ans++;
	}
	cout << ans << endl;
	return 0;
}
习题9-2 车厢重组
#include <bits/stdc++.h>
using namespace std;

int N,arr[10005],ans = 0;

// 使用冒泡排序的方法,交换一次车厢我们就记录一下,最后返回所有的交换次数
int Bubblesort()
{
	for(int i = 0;i<N-1;i++)
	{
		for(int j = 0;j<N-i-1;j++)
		{
			if(arr[j]>arr[j+1])
			{
				swap(arr[j],arr[j+1]);
				ans++;
			}
		}
	}
	return ans;
}


int main()
{
	cin >> N;
	for(int i = 0;i<N;i++)
	{
		cin >> arr[i];
	}
	int a = Bubblesort();
	cout << a << endl;
	return 0;
}
习题9-3 欢乐的跳
#include <bits/stdc++.h>
using namespace std;

int arr[1010], n;
bool diff[1010] = {false}; // 记录差值是否出现过

int main()
{
    cin >> n;
    for(int i = 1; i <= n; ++i) {
        cin >> arr[i];
    }

    for(int i = 1; i < n; ++i) {
        int d = abs(arr[i] - arr[i+1]);
        if(d >= 1 && d < n) {
            diff[d] = true;
        }
    }

    for(int i = 1; i < n; ++i) {
        if(!diff[i]) {
            cout << "Not jolly" << endl;
            return 0;
        }
    }

    cout << "Jolly" << endl;
    return 0;
}
习题9-4 分数线划定
#include <bits/stdc++.h>
using namespace std;
int n,m,ans = 0;

struct student{
	int id,score;
}arr[5005],crr[5005];

bool cmp(student a,student b)
{
	if(a.score != b.score) return a.score > b.score; // 分数高者优先
	return a.id < b.id; // 序号小者优先
}

int main()
{
	cin >> n >> m;
	for(int i = 0;i<n;i++)
	{
		cin >> arr[i].id >> arr[i].score;
	}
	sort(arr,arr+n,cmp);
	int a = floor(m*1.5); // 向下取整
	int scores = arr[a].score; // 划定分数线
	for(int i = 0;i<n;i++)
	{
		if(arr[i].score >= scores)
			crr[ans++] = arr[i];  // 筛选出进入面试的选手
	}
	cout << score << " " << ans << endl;
	sort(crr+1,crr+ans,cmp); // 对这些选手再进行一下排序
	for(int i = 0;i<ans;i++)
		cout << crr[i].id << " " << crr[i].score << endl;
	
	return 0;
}
习题9-5 攀爬者
#include<cstdio>
#include<algorithm>
#include<cmath>

using namespace std;

int n;
double ans;
struct pt
{
	int x,y,z;
	// 定义重载等号运算符
	bool operator <(const pt &a) const
	{
		return z<a.z;
	}
	// 定义计算欧式距离的重载加减运算符
	double operator -(const pt&a) const
	{
		return sqrt((x-a.x)*(x-a.x)+(y-a.y)*(y-a.y)+(z-a.z)*(z-a.z));
	}
}a[50001];


int main()
{
	scanf("%d",&n);
	for(int i = 1;i<=n;i++) scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].z);
	sort(a+1,a+n+1);
	for(int i = 2;i<=n;i++) ans+=a[i] - a[i-1];
	printf("%.3lf",ans);
	return 0;
}
习题9-6 生日
#include <bits/stdc++.h>
using namespace std;
int n;
struct birthday
{
	string name;
	int y,m,d,id;
}arr[105];
  
bool cmp(birthday a,birthday b)
{
	if(a.y!=b.y) return a.y<b.y;
	else
	{
		if(a.m != b.m) return a.m < b.m;
		else
		{
			if(a.d != b.d) return a.d<b.d;
				else return a.id>b.id;
		}
	}
}

int main()
{
	cin >> n;
	for(int i = 0;i<n;i++)
	{
		arr[i].id = i+1;
		cin >> arr[i].name >> arr[i].y >> arr[i].m >> arr[i].d;
	}
	sort(arr,arr+n,cmp);
	for(int i = 0;i<n;i++)
	{
		cout << arr[i].name << endl;
	}
	return 0;
}
习题9-7 拼数

其中有一个自定义排序规则的谓词函数,用于比较两个字符串 a 和 b

  • 比如 a = "9", b = "34"

    • 如果 a + b = "934" 大于 b + a = "349",那么我们就认为 a 应该排在 b 前。
  • 这个排序规则的核心思想是:两个字符串按不同顺序拼接后,哪种拼接结果更大,就按那种顺序来排序

所以这个 cmp 保证了:最终拼接起来的字符串整体尽可能大,所以这也算是一种贪心的思想。

#include <bits/stdc++.h>
using namespace std;

//使用谓词,改变排序的规则,使得最高位数是最大的,向下进行迭代,依次让最高位数是最大的
bool cmp (string a,string b)
{
	return a+b>b+a;
}


int main()
{
	int n;
	cin >> n;
	string a[n];
	for(int i = 0;i<n;i++) cin >> a[i];
	sort(a,a+n,cmp);
	for(int i = 0;i<n;i++) cout << a[i];
	cout << endl;
	return 0;
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值