上海交大ACM班C++算法与数据结构——C++算法初级2
1.排序
- 交换a和b:
swap(a,b);
- 稳定:在排序前的序列中,相同的元素排序前后相对位置不改变。(跳着排序的算法一般不稳定)
- O:不超过,Ω:不少于
- 选择排序
- 选出最小的元素放在首位,再选出第二小的元素放在第二位……
- 时间复杂度O(n^2)
- 不稳定
- 示例代码:
#include <bits/stdc++.h>
using namespace std;
int a[1010];
int n;
int main() {
// 输入
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
// 选择排序过程
for (int i = 1; i < n; ++i) { // 枚举应该归位的第i个元素,这里因为前n-1位归为以后,
// 第n位也会归位,所以我们只枚举到n-1。
int min_pos = i; // 将最小值位置设置为当前范围i~n的首位
for (int j = i + 1; j <= n; ++j) { // 将第i个元素和剩下的元素相比较
if (a[j] < a[min_pos]) { // 如果当前元素小于之前维护的最小值
min_pos = j; // 更改最小值出现的位置
}
}
swap(a[i], a[min_pos]); // 将最小值与第i个位置交换
}
// 输出
for (int i = 1; i <= n; ++i)
cout << a[i] << ' ';
return 0;
}
- 冒泡排序
- 相邻元素两两比较,大的后移,小的前移,直到最大的移到了末尾,再从头开始新的一轮比较
- 时间复杂度O(n^2)
- 稳定
- 示例代码:
#include <bits/stdc++.h>
#define N 1010
using namespace std;
int n, a[N];
int main() {
// 输入
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
// 冒泡排序
for (int i = 1; i < n; ++i) { // 一共n-1个阶段,在第i个阶段,未排序序列长度从n-i+1到n-i。
for (int j = 1; j <= n - i; ++j) // 将序列从1到n-i+1的最大值,移到n-i+1的位置
if (a[j] > a[j + 1]) // 其中j枚举的是前后交换元素的前一个元素序号
swap(a[j], a[j + 1]);
}
// 输出
for (int i = 1; i <= n; ++i) cout << a[i] << ' ';
cout << endl;
return 0;
}
- 插入排序
- 将第i个元素插入到前i-1个元素的有序序列中,形成长度为i的有序序列。
- 时间复杂度O(n^2)
- 稳定
- 代码示例:
#include <bits/stdc++.h>
#define N 1550
using namespace std;
int a[N], n;
int main() {
// 输入
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
// 插入排序
for (int i = 2; i <= n; ++i) { // 按照第2个到第n个的顺序依次插入
int j, x = a[i]; // 先将i号元素用临时变量保存防止被修改。
// 插入过程,目的是空出分界线位置j,使得所有<j的部分<=x,所有>j的部分>x。
// 循环维持条件,j>1,并且j前面的元素>x。
for (j = i; j > 1 && a[j - 1] >= x; --j) {
// 满足循环条件,相当于分界线应向前移,
// 分界线向前移,就等于将分界线前面>x的元素向后移
a[j] = a[j - 1];
}
// 找到分界线位置,插入待插入元素x
a[j] = x;
}
// 输出
for (int i = 1; i <= n; ++i) cout << a[i] << ' ';
cout << endl;
return 0;
}
- 快速排序
- 分治策略:不断分成更短的序列,对短序列进行排序,最后合并短的有序序列。一个头指针一个尾指针一个分界值,头尾轮换与分界值比较
- 原地排序:元素的移动都在原始数组中进行
- 时间复杂度:最好O(nlogn),最坏O(n^2)
- 不稳定
- 代码示例:
// 该代码参考 https://www.geeksforgeeks.org/quick-sort/
#include <bits/stdc++.h>
#define N 100010
using namespace std;
int n;
int a[N];
void quick_sort(int l, int r) {
// 设置最右边的数为分界线
int pivot = a[r];
// 元素移动
int k = l - 1;
for (int j = l; j < r; ++j)
if (a[j] < pivot) swap(a[j], a[++k]);
swap(a[r], a[++k]);
if (l < k - 1) quick_sort(l, k - 1); // 如果序列的分界线左边的子段长度>1,排序
if (k + 1 < r) quick_sort(k + 1, r); // 如果序列的分界线右边的子段长度>1,排序
// 上面的过程结束后,到这里左子段和右子段已经分别排好序。又因为确定分界线以后的移动操作
// 保证了左子段中的元素都小于等于分界线,左子段中的元素都大于分界线。所以整个序列也是有序的。
}
int main() {
// 输入
scanf("%d", &n);
for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
// 快速排序
quick_sort(1, n);
// 输出
for (int i = 1; i <= n; ++i) printf("%d ", a[i]);
return 0;
}
- sort比较函数:STL实现的快速排序。头指针、尾指针和比较函数,其中如果排序对象定义了小于号的话,比较函数可省略。
- 正常是从小到大排序
- 从大到小排序实现:
#include <bits/stdc++.h>
using namespace std;
int a[10] = {2, 3, 1, 5, 4};
int n = 5;
bool cmp(int x, int y) { // 比较函数,函数的参数是当前比较的两个数组中的元素
return x > y; // x和y分别为排序数组中的两个元素。
} // 当函数返回值为true时,x应该排在y的前面。
int main() {
sort(a, a + n, cmp); // 比较函数作为第三个参数传入sort函数
for (int i = 0; i < n; ++i) cout << a[i] << ' ';
cout << endl;
}
- 归并排序
- 分治,分成相等的两部分,各自排序,再合并成一个大的有序序列
- 稳定排序
- 时间复杂度:任何情况下都是O(nlogn);空间复杂度:O(n),借助了和a等长的辅助数组,不算原地排序。
- 代码示例:
#include <bits/stdc++.h>
#define N 100010
using namespace std;
int n = 5;
int a[6] = {0, 2, 4, 1, 6, 3};
int b[6] = {0, 0, 0, 0, 0, 0};
// 合并操作
void merge(int l, int r) {
for (int i = l; i <= r; ++i) b[i] = a[i]; // 将a数组对应位置复制进辅助数组
int mid = l + r >> 1; // 计算两个子段的分界线
// l + r 的值右移1位,相当 l + r 的值除以2取整。
int i = l, j = mid + 1; // 初始化i和j两个指针分别指向两个子段的首位
for (int k = l; k <= r; ++k) { // 枚举原数组的对应位置
// 如果:
// 1. ``j``已经移出子段的末尾;
// 2. 或者``i``和``j``都仍然指向子段中的元素,但``i``指向的元素比``j``指向的元素小;
if (j > r || i <= mid && b[i] < b[j]) a[k] = b[i++];
else a[k] = b[j++];
}
}
void merge_sort(int l, int r) { // l和r分别代表当前排序子段在原序列中左右端点的位置
// TODO 请补全下述代码,完成归并操作
if (l >= r) return; // 当子段为空或者长度为1,说明它已经有序,所以退出该函数
int mid = l + r >> 1; // 取序列的中间位置,并将序列分成两部分(左右长度相差最多为1)
merge_sort(l, mid); // 对``l``到``mid``第一个子段进行归并操作
merge_sort(mid + 1, r); // 对``mid+1``到``r``第二个子段子段进行归并操作
merge(l, r); // 调用合并操作函数,将l..mid和mid+1..r两个子段合并成完整的l..r的有序序列
}
int main() {
// 归并排序
merge_sort(1, n);
// 输出
for (int i = 1; i <= n; ++i) printf("%d ", a[i]);
return 0;
}
- stable_sort:STL实现的归并排序
#include <bits/stdc++.h>
using namespace std;
int a[10] = {0, 2, 3, 1, 5, 4}; // 1-base,0号元素无意义
int n = 5;
bool cmp(int x, int y) { // 比较函数,函数的参数是当前比较的两个数组中的元素
return x > y; // x和y分别为排序数组中的两个元素。
} // 当函数返回值为true时,x应该排在y的前面。
int main() {
stable_sort(a + 1, a + n + 1, cmp); // 比较函数作为第三个参数传入sort函数
for (int i = 1; i <= n; ++i) cout << a[i] << ' ';
cout << endl;
}
- 计数排序
- 元素的范围都是[0…K]中的整数,并且K的范围比较小(例如106,开长度为106 左右的int类型数组所占用的内存空间只有不到4M),可以统计每个数字出现的个数,据此重新构造出含有相同元素的有序数组
- 可以通过一定的放缩变化使其满足上述条件
- 时间和空间复杂度都是O(n+K)
- 基于比较的方法时间复杂度最好是O(nlogn),但基数排序不是基于比较的方法
- 可以实现稳定
- 代码示例
#include <bits/stdc++.h>
#define N 1000005
#define K 1000001 // 假设非负整数最大元素范围为1000000
using namespace std;
int a[N], n, b[N];
int cnt[K];
int main() {
// 输入
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
++cnt[a[i]]; // 这里通过计数数组cnt来维护每一种值出现的次数
}
// 维护最终有序序列
for (int i = 0, j = 0; i < K; ++i) // 枚举每一种值i,指针j用来枚举填充答案数组中的位置
for (int k = 1; k <= cnt[i]; ++k) // 根据该值出现的次数
b[++j] = i; // 添加对应个数的i到答案序列
// 输出
for (int i = 1; i <= n; ++i)
cout << b[i] << ' ';
cout << endl;
return 0;
}
2.二分查找
- 针对顺序存储有序序列,不断用中点去排除错误信息,缩小查找范围。
- 15个数,二分查找次数不超过4次(log15<4)
- 防止左后L,R不更新了陷入死循环,用向上向下取整多走一点:如果代码中是用的L = M,把L不断往右push,那么M向上取整(M = L + (R - L + 1)/2)
- 年月日的二分:要求19701212和20200817的中点,我们可以直接求(19701212 + 20200827) / 2 = 19951019,这就是这两个日期的近似中点。如果我们得到了类似于19971805这样不合法的日期(没有18月),我们只需要把18月向下取整到合法的日期(12月),变为19971205即可
- lower_bound:在指定的升序排序的数组中,找到第一个大于等于x的数字
upper_bound:在指定的升序排序的数组中,找到第一个大于x的数字。
这两个函数会返回对应数字的指针(或者是迭代器)。
int a[100000], n;
cin >> n;
for( int i = 0; i < n; ++i )
cin >> a[i];
sort(a, a + n);
int *p = lower_bound(a, a + n, 13); // 第一个大于等于13的数字
int *q = upper_bound(a, a + n, 13); // 第一个大于13的数字
- 二分法求方程的解时,double本身存在不小的精度误差,通过R - L >= 1e-10这种方式来控制二分的终止条件,会带来非常大的精度问题,可以采用固定次数二分的方法,达到一定次数就终止算法
3.对998244353取模
对一个很大的质数取模,是结果落在Int范围内,避免选手使用高精度整数计算。
4.递推与动态规划
- 递推公式如下:f(n)=f(n/2)+nf(n)=f(n/2)+n,则该算法的时间复杂度为:f(n) = f(1)+2n = O(n)
- 动态规划:
-
状态、转移方程(递推公式)、转移方向(递推进行的方向,也是递归是如何不断加深的,如j++ )、初始状态
-
用空间换时间的算法,占用存储空间大,优化办法有:滚动数组(由转移方程,当前状态只与前几个状态,其他无关的可以用后就删除,于是可以用更小的数组来存储相关计算信息)、一维数组存储(例如,注意到[i,j]状态需要[i-1,j]和[i-1,j-2],那么可以j从大到小遍历,将[i,j]存到[i-1,j]的位置,于是仅仅使用一维数组便可以完成动态规划)
-
用规模更小的子问题最优解来求解原问题最优解
-
需要问题无后效性,即前面状态的决策不会限制到后面的决策
-
典型应用:背包问题
-
例子:
给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。
输入描述:第一行是一个整数,表示序列的长度 n。第二行有n个整数,第 i个整数表示序列的第 i 个数字ai
输出描述:输出一行一个整数表示答案
示例 1:
输入:
7
2 -4 3 -1 2 -4 3
输出:
4
-
#include <bits/stdc++.h>
#define N 100000
using namespace std;
int n;
int a[N];
int b[N]; // 用于存储当前位置元素的最大字段和
int main() {
// 输入
cin >> n;
for (int i = 0; i < n; ++i)
cin >> a[i];
b[0] = a[0];
// 动态规划过程,寻找序列开头位置
for (int i = 1; i <= n; ++i)
if (b[i - 1] < 0) {
b[i] = a[i];
} else {
b[i] = b[i-1] + a[i];
}
// 输出,寻找序列结尾位置
int ans = 0;
for (int i = 0; i < n; ++i)
ans = max(ans, b[i]); // 求第n行的最大值
cout << ans << endl;
return 0;
}