上海交大ACM班C++算法与数据结构——C++算法初级2

上海交大ACM班C++算法与数据结构——C++算法初级2

大纲

1.排序

  • 交换a和bswap(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;
			}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿航626

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值