电子版教材链接
:
我通过百度网盘分享的文件:深入浅出程序设计…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 数列排序
解法一:选择排序
原理
选择排序是一种原地比较排序算法,其基本思想是:
在未排序的序列中,每次选择最小(或最大)元素,将其放到已排序序列的末尾
实现过程
- 从第一个元素开始,向后扫描整个数组,找出最小值;
- 将这个最小值和当前位置的值交换;
- 继续处理后续的未排序部分,重复上述操作
算法复杂度
最优: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; // 提前结束
}
解法三:插入排序
原理
将一个元素插入到已排好序的序列中的合适位置
实现过程
- 从第一个元素开始,它默认是有序的;
- 取下一个元素,将它和前面排好序的序列从后往前比较;
- 如果发现比它大的元素,就将这些元素都向后移动;
- 找到合适位置插入当前元素;
- 重复步骤 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)策略实现的排序算法
实现过程
- 选取一个“基准值”(pivot),通常是数组中的一个元素;
- 将数组划分为两部分:
- 小于等于 pivot 的元素放左边;
- 大于 pivot 的元素放右边;
- 递归地对左右两个子数组继续排序;
- 最终整个数组就是有序的。
算法复杂度推导
总复杂度可以抽象成:
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(nlogn) T(n) = 2T(n/2) + cn \Rightarrow T(n) = O(n \log n) T(n)=2T(n/2)+cn⇒T(n)=O(nlogn)
也可以用递归树直观看出:
层数 | 子问题个数 | 每层总大小 | 复杂度 |
---|---|---|---|
0 | 1 | nnn | cncncn |
1 | 2 | nnn | cncncn |
2 | 4 | nnn | cncncn |
… | … | nnn | cncncn |
log2n\log_2 nlog2n | nnn个长度为1的子问题 | 总共 nnn | cncncn |
所以总共复杂度:
T(n)=cn⋅log2n=O(nlogn)
T(n) = cn \cdot \log_2 n = O(n \log n)
T(n)=cn⋅log2n=O(nlogn)
最坏情况分析:每次只分出一个元素
最糟糕的情况是每次分区时,pivot 是最大或最小值,一边是 n−1n-1n−1,另一边是空集:
T(n)=T(n−1)+O(n) T(n) = T(n - 1) + O(n) T(n)=T(n−1)+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(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)
=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_iai 和 aja_jaj 被比较的概率。
关键观察:
- aia_iai 和 aja_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 比较)=∣j−i∣+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)=1≤i<j≤n∑j−i+12
这个和可以近似为:
C(n)≤2n⋅∑k=1n1k=O(nlogn) C(n) \le 2n \cdot \sum_{k=1}^{n} \frac{1}{k} = O(n \log n) C(n)≤2n⋅k=1∑nk1=O(nlogn)
这就是平均时间复杂度为 O(nlogn)O(n \log n)O(nlogn) 的数学推导。
情况 | 分析思路 | 复杂度 |
---|---|---|
最佳情况 | 每次划分成相等子区间 | O(nlogn)O(n \log n)O(nlogn) |
最坏情况 | 每次只去掉一个元素 | O(n2)O(n^2)O(n2) |
平均情况 | 所有 pivot 情况的概率期望 | O(nlogn)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;
}