电子版教材链接
:
我通过百度网盘分享的文件:深入浅出程序设计…pdf
链接:https://pan.baidu.com/s/1kmF8wZLnK3Zci7s1ffjRzw
提取码:Ra3Q
复制这段内容打开「百度网盘APP即可获取」
// 法一: 计数排序
#include
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
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;
}
原理
选择排序是一种原地比较排序算法,其基本思想是:
在未排序的序列中,每次选择最小(或最大)元素,将其放到已排序序列的末尾
实现过程
算法复杂度
最优:O(n),最坏:O(n²),平均:O(n²) 有两轮for循环,所以复杂度是O(n* n)
#include
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
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; // 提前结束
}
原理
将一个元素插入到已排好序的序列中的合适位置
实现过程
算法复杂度
最坏:O(n^2)
最优:O(n)
平均:O(n^2)
#include
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;
}
原理
快速排序是一种分治(Divide and Conquer)策略实现的排序算法
实现过程
算法复杂度推导
总复杂度可以抽象成:
T ( n ) = T ( 左区间 ) + T ( 右区间 ) + O ( n ) T(n) = T(\text{左区间}) + T(\text{右区间}) + O(n) T(n)=T(左区间)+T(右区间)+O(n)
假设每次都能把数组正好分成左右两半,则:
T ( n ) = 2 T ( n 2 ) + O ( n ) T(n) = 2T\left(\frac{n}{2}\right) + O(n) T(n)=2T(2n)+O(n)
这是经典的**主定理(Master Theorem)**形式:
T ( n ) = 2 T ( n / 2 ) + c n ⇒ T ( n ) = O ( n log n ) 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 | n n n | c n cn cn |
1 | 2 | n n n | c n cn cn |
2 | 4 | n n n | c n cn cn |
… | … | n n n | c n cn cn |
log 2 n \log_2 n log2n | n n n个长度为1的子问题 | 总共 n n n | c n cn cn |
所以总共复杂度:
T ( n ) = c n ⋅ log 2 n = O ( n log n ) T(n) = cn \cdot \log_2 n = O(n \log n) T(n)=cn⋅log2n=O(nlogn)
最糟糕的情况是每次分区时,pivot 是最大或最小值,一边是 n − 1 n-1 n−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 ) + c n = T ( n − 2 ) + c ( n − 1 ) + c n = T ( n − 3 ) + c ( n − 2 ) + c ( n − 1 ) + c n = ⋯ = 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 ( n 2 ) = O(n^2) =O(n2)
所以最坏复杂度为 O ( n 2 ) O(n^2) O(n2),这就是为什么快速排序在有序/逆序数组上可能退化的原因
我们考虑 任意排列下的期望比较次数。假设排序数组为 a 1 , a 2 , … , a n a_1, a_2, \dots, a_n a1,a2,…,an,我们来考虑任意两个元素 a i a_i ai 和 a j a_j aj 被比较的概率。
关键观察:
所以我们可以证明:
P ( a i 与 a j 比较 ) = 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 ≤ n 2 j − 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 ) ≤ 2 n ⋅ ∑ k = 1 n 1 k = O ( n log n ) 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 ( n log n ) O(n \log n) O(nlogn) 的数学推导。
情况 | 分析思路 | 复杂度 |
---|---|---|
最佳情况 | 每次划分成相等子区间 | O ( n log n ) O(n \log n) O(nlogn) |
最坏情况 | 每次只去掉一个元素 | O ( n 2 ) O(n^2) O(n2) |
平均情况 | 所有 pivot 情况的概率期望 | O ( n log n ) O(n \log n) O(nlogn) |
#include
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;
}
通过使用快速排序的思想,我们实现一下快速选择
快排的底层原理是的快速选择的实现是线性复杂度(因为每次只需要对一个子区间进行划分(第k个元素所在的子区间),相较于快速排序的两个子区间划分要省去很多的复杂度)\
tips
本题一定要使用scanf和printf的输入输出形式,不然会超时(或者使用cin时加上取消限制流的代码也行),建议以后读者多用scanf,少用cin,scanf的读取速度一般来说是比cin快的多的,后续遇见超时的情况不妨也检查一下是否是读入过慢导致的问题
#include
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
}
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;
}
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
#include // 使用算法库函数调用函数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> a[i];
sort(a,a+n);
for(int i = 0;i
#include
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
}
#include
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;
}
#include
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;
}
#include
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;
}
#include
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;
}
#include
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;
}
#include
#include
#include
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;
}
#include
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;
}
其中有一个自定义排序规则的谓词函数,用于比较两个字符串 a
和 b
:
比如 a = "9", b = "34"
:
a + b = "934"
大于 b + a = "349"
,那么我们就认为 a
应该排在 b
前。这个排序规则的核心思想是:两个字符串按不同顺序拼接后,哪种拼接结果更大,就按那种顺序来排序。
所以这个 cmp
保证了:最终拼接起来的字符串整体尽可能大,所以这也算是一种贪心的思想。
#include
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;
}