算法·入门篇『课堂随笔』
Zero·前言
本文章需结合 B站教学视频 共同学习与观看。
一、O(n)
1.1 分析模型基础
在分析算法复杂度之前,我们必须知道的概念:
- 时间复杂度:算法执行所需的时间与输入规模的关系
- 白话:大概执行了多少次宏观上、可概括的代码段
- 空间复杂度:算法执行所需的额外存储空间与输入规模的关系
- 白话:开辟了多大的空间,这个容量与输入规模的比例
常用表示法:
- 大O记号(Big-O):表示算法的最坏情况或渐进上界
- Ω记号(Omega):表示算法的最好情况或渐进下界
- Θ记号(Theta):表示算法的紧确界
1.2 冒泡排序分析
voidbubble_sort(int arr[],int n){for(int i =0; i < n-1; i++){bool swapped =false;for(int j =0; j < n-i-1; j++){if(arr[j]> arr[j+1]){swap(arr[j], arr[j+1]); swapped =true;}}if(!swapped)break;}}时间复杂度分析
- 最好情况(已有序数组):
- 第一轮遍历后立即退出(借助swapped标志)
- 比较次数 = n-1
- 时间复杂度:O(n)
- 最坏情况(逆序数组):
- 完全执行所有循环
- 比较次数 = (n-1)+(n-2)+…+1 = n(n-1)/2
- 时间复杂度:O(n²)
- 平均情况:
- 仍需进行大量比较和交换
- 时间复杂度:O(n²)
- 稳定性分析:
- 只有相邻元素比较交换
- 稳定排序
空间复杂度分析
- 仅使用常数级别的额外空间(i,j和swapped变量)
- 空间复杂度:O(1)(原地排序)
二、排序算法(入门级别)
排序是很实用的、常用的(几乎是必需品,只不过现在封装的很好,让你感觉不到)
排序的学习可以让你了解一些 算法的基本思想与操作技巧(暴力、比较、交换、分治、减治、空间换时间等)
- 排序算法也是 分析 时间/空间复杂度的理想模型
- 排序也是计算机科学早期研究的核心问题(如1956年希尔排序的提出)
2.1 选择排序
排序算法中最简单直观的。
思路:不断缩小范围确定当前范围内的最小/最大值,随后将最小/最大值与目标位置进行交换。那么每一趟筛选的行为就是选出第n个最小/最大值!
举例: 从小到大排序下方数字
- 18、1、5、27、12(下标范围 0 ~ 4,最小值 1 与 下标 0 的 18 交换)
- 1、18、5、27、12(下标范围 1 ~ 4,最小值 5 与 下标 1 的 18 交换)
- 1、5、18、27、12(下标范围 2 ~ 4,最小值 12 与 下标 2 的 18 交换)
- 1、5、12、27、18(下标范围 3 ~ 4,最小值 18 与 下标 3 的 27 交换)
- 1、5、12、27、18(下标范围 4 ~ 4,单个元素自然有序)
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];voidselect_sort(int*arr,int n){for(int i =0; i < n -1; i++){int pos = i;// 假设认为当前范围最小值 pos 就是 当前范围的第一个数for(int j = i +1; j < n; j++){if(arr[pos]> arr[j]){// 发现当前范围更小的数 pos = j;// 记录这个数的下标}}// 交换这俩数int t = arr[i]; arr[i]= arr[pos]; arr[pos]= t;}}intmain(void){int n ; cin >> n;for(int i =0; i < n; i++){scanf("%d",&arr[i]);}select_sort(arr, n);for(int i =0; i < n; i++){printf("%d ", arr[i]);}return0;}2.2 冒泡排序
见名知意,冒泡就是说将目前第n个最大/最小的数,冒泡到顶端。如果顶端有了k个泡,那么就要排在k个泡之后成为第k+1个泡。
思路:由于每次冒泡都需要排在之前泡泡的后面,并且之前冒泡的数值肯定是合理的。所以缩小冒泡范围并把当前范围最小/最大的值,通过相邻元素两两交换最后冒泡到范围顶端。
举例: 从小到大排序下方数字
- 18、1、5、27、12
(下标范围 0 ~ 4, 18 > 1 交换)1、18、5、27、12
(下标范围 0 ~ 4, 18 > 5 交换)1、5、18、27、12
(下标范围 0 ~ 4, 18 < 5 不交换)1、5、18、27、12
(下标范围 0 ~ 4, 27 > 12 交换)1、5、18、12、27(第一个泡泡)
- 1、5、18、12、27
(下标范围 0 ~ 3, 1 < 5 不交换)1、5、18、12、27
(下标范围 0 ~ 3, 5 < 18 不交换)1、5、18、12、27
(下标范围 0 ~ 3, 18 > 12 交换)1、5、12、18、27
- 1、5、12、18、27
(下标范围 0 ~ 2, 1 < 5 不交换)1、5、12、18、27
(下标范围 0 ~ 2, 5 < 12 不交换)1、5、12、18、27
- 1、5、12、27、18
(下标范围 0 ~ 1, 1 < 5 不交换)1、5、12、18、27
- 1、5、12、18、27(下标范围 0 ~ 0,单个元素自然有序)
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];voidbuttle_sort(int*arr,int n){for(int i =0; i < n -1; i++){for(int j =0; j < n - i -1; j++){if(arr[j]> arr[j +1]){int temp = arr[j]; arr[j]= arr[j +1]; arr[j +1]= temp;}}}}intmain(void){int n ; cin >> n;for(int i =0; i < n; i++){scanf("%d",&arr[i]);}buttle_sort(arr, n);for(int i =0; i < n; i++){printf("%d ", arr[i]);}return0;}2.2.1 稳定/不稳定排序的说法
如果一个排序算法在排序后能够保持相等元素的原始相对顺序,那么这个算法就是稳定的。
[("Alice",90),("Bob",85),("Chris",90),("Dave",80)]// 稳定排序的升序(Alice和Chris都是90分,Alice原本在Chris前面,排序后仍然在前面。)[("Dave",80),("Bob",85),("Alice",90),("Chris",90)].// 不稳定排序的升序(可能 Alice 就在 Chris 的后面)[("Dave",80),("Bob",85),("Chris",90),("Alice",90)]2.3 插入排序
插入排序类似于两堆扑克牌,从无序列扑克牌堆里面选牌插入到有序扑克牌堆。
思路 :将数组分为已排序和未排序两部分,每次从未排序部分取出第一个元素,在已排序部分找到合适的位置插入。
举例 :从小到大排序下方数字
- 18、1、5、27、12(初始状态,第一个元素18视为已排序)
- 1、18、5、27、12(将1插入到18前面)
- 1、5、18、27、12(将5插入到1和18之间)
- 1、5、18、27、12(27已经在正确位置)
- 1、5、12、18、27(将12插入到5和18之间)
#include<iostream>usingnamespace std;int arr[50];intmain(void){int n; cin >> n;for(int i =0; i < n;++i){ cin >> arr[i];}for(int i =1; i < n;++i){// 要插入的元素int t = arr[i];// 存起来int j = i -1;// 18 1 5 27 12// 1 5 18 27 12for(; j >=0;--j){if(arr[j]> t){ arr[j +1]= arr[j];}else{break;}} arr[j +1]= t;}for(int i =0; i < n;++i){ cout << arr[i]<<" ";}return0;}2.4 希尔排序
希尔排序是插入排序的改进版,通过分组(分治思想)让插入排序提高了效率。
思路 :
- 选择一个增量序列(通常为n/2, n/4, …, 1)
- 依据每个增量去分组,然后将每组都进行插入排序
- 到最后增量为1,也就是要进行一次完整的插入排序
希尔排序会有一种假象:让初学者误认为嵌套循环多就是速度慢的、进而忽略了有序序列堆插入排序具体实现过程的影响。
举例 :从小到大排序下方数字 [18, 1, 5, 27, 12, 9, 6, 15]
- 初始增量=4(数组长度8/2):
- 分组:[18,12], [1,9], [5,6], [27,15]
- 排序后:[12,18], [1,9], [5,6], [15,27]
- 数组变为:[12, 1, 5, 15, 18, 9, 6, 27]
- 增量=2:
- 分组:[12,5,18,6], [1,15,9,27]
- 排序后:[5,6,12,18], [1,9,15,27]
- 数组变为:[5, 1, 6, 9, 12, 15, 18, 27]
- 增量=1(标准插入排序):
- 最终排序结果:[1, 5, 6, 9, 12, 15, 18, 27]
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];voidshell_sort(int*arr,int n){for(int gap = n /2; gap >0; gap /=2){// 增量序列for(int i = gap; i < n; i++){// 对每个分组进行插入排序int temp = arr[i];int j;for(j = i; j >= gap && arr[j - gap]> temp; j -= gap){ arr[j]= arr[j - gap];} arr[j]= temp;}}}intmain(void){int n; cin >> n;for(int i =0; i < n; i++){scanf("%d",&arr[i]);}shell_sort(arr, n);for(int i =0; i < n; i++){printf("%d ", arr[i]);}return0;}2.5 计数排序
核心思想 :统计每个元素出现的次数,然后按顺序输出
特点 :
- 非比较排序,时间复杂度O(n+k),k为数据范围
- 适用于整数且范围不大的情况
- 稳定排序
voidcountingSort(int arr[],int n){// 假设数据范围在0到100之间int count[101]={0};// 统计每个元素出现的次数for(int i =0; i < n; i++) count[arr[i]]++;// 按顺序填充原数组int index =0;for(int i =0; i <=100; i++){while(count[i]--){ arr[index++]= i;}}}2.6 归并排序(前置知识:双指针)
归并排序:应用快慢指针与递归去解决排序问题的算法。
#include<iostream>usingnamespace std;// 5 4 3 2 1voidmerge_sort(int*arr,int l,int r){if(l >= r)return;int mid =(l + r)/2;merge_sort(arr, l, mid);merge_sort(arr, mid +1, r);int tmp[10];int i = l, j = mid +1, k =0;while(i <= mid && j <= r){if(arr[i]< arr[j]) tmp[k++]= arr[i++];else tmp[k++]= arr[j++];}while(i <= mid) tmp[k++]= arr[i++];while(j <= r) tmp[k++]= arr[j++];for(i = l, k =0; i <= r;++i,++k){ arr[i]= tmp[k];}}intmain(void){int n;int arr[10]; cin >> n;for(int i =0; i < n;++i){ cin >> arr[i];}merge_sort(arr,0, n -1);for(int i =0; i < n;++i){ cout << arr[i]<<" ";}return0;}2.7 快速排序(前置知识:双指针)
快速排序:应用左右指针与递归去解决排序问题的算法。
#include<iostream>usingnamespace std;// 5 4 3 2 1voidquick_sort(int*arr,int l,int r){if(l >= r)return;int x = arr[(l + r)/2];int i = l -1, j = r +1;while(i < j){do i++;while(arr[i]< x);do j--;while(arr[j]> x);if(i < j){int temp = arr[i]; arr[i]= arr[j]; arr[j]= temp;}}quick_sort(arr, l, j);quick_sort(arr, j +1, r);}intmain(void){int n;int arr[10]; cin >> n;for(int i =0; i < n;++i){ cin >> arr[i];}quick_sort(arr,0, n -1);for(int i =0; i < n;++i){ cout << arr[i]<<" ";}return0;}三、大整数四则运算
接下来,我带着大家继续锻炼模拟思维。
何为大整数:通常指的是非常大的整数,其值远超常规数据类型(如32位或64位整数)能直接表示的范围。
意义:
- 现代加密算法(如 RSA、ECC)依赖大质数或大整数运算,防止暴力破解。例如:RSA-2048 使用两个 1024 位的质数相乘,生成密钥。
- 精确计算极大数值(如天文数字、阶乘、斐波那契数列等)。
3.1 加法
大整数加法应该算是四个运算法则里,最好理解的了。
#include<iostream>#include<string>#include<vector>usingnamespace std;intmain(void){ string a,b; cin >> a >> b; vector<int> A; vector<int> B;// 做倒置存入到 vector 中(倒置是因为比较好处理,可以不倒置)for(int i = a.size()-1; i >=0;--i){ A.push_back(a[i]-'0');}for(int i = b.size()-1; i >=0;--i){ B.push_back(b[i]-'0');}/* 4321 765 */int t =0;// 维护进位值 vector<int> C;// 存储最后结果for(int i =0; i < A.size()|| i < B.size();++i){if(i < A.size()) t += A[i];// 有就加,该位没数就不加if(i < B.size()) t += B[i]; C.push_back(t %10);// 存储的是某个位的数 t /=10;// 更新进位值}if(t >0) C.push_back(t);// 若最后进位值还有,那么就属于进多了一位for(int i = C.size()-1; i >=0;--i){ cout << C[i];}return0;}3.2 减法
大整数减法会遇到负数问题的处理,还有前置0。关键点在于这里~
#include<iostream>#include<string>#include<vector>usingnamespace std;// 必须要判断出 A、B 两个数谁更大boolisMax(vector<int> A, vector<int> B){if(A.size()== B.size()){for(int i = A.size()-1; i >=0;--i){if(A[i]== B[i]){continue;}return A[i]> B[i];}returntrue;}else{return A.size()> B.size();}}intmain(void){ string a, b; cin >> a >> b; vector<int> A; vector<int> B;for(int i = a.size()-1; i >=0;--i){ A.push_back(a[i]-'0');}for(int i = b.size()-1; i >=0;--i){ B.push_back(b[i]-'0');}bool flag =isMax(A, B);if(!flag)// 如果 B 比 A 大则将他们交换,因为我们要默认 A - B 进行模拟运算{ vector<int> temp = A; A = B; B = temp;}int t =0;// 借位 vector<int> C;for(int i =0; i < A.size()|| i < B.size();++i){int num =-t;if(i < A.size()) num += A[i];if(i < B.size()) num -= B[i];if(num <0){ t =1; num +=10;}else{ t =0;} C.push_back(num %10);}while(C.size()>1&& C[C.size()-1]==0){ C.pop_back();}if(C.size()==1&& C[0]==0){ cout <<0<< endl;return0;}if(!flag) cout <<"-";for(int i = C.size()-1; i >=0;--i){ cout << C[i];}return0;}3.3 乘法
#include<iostream>#include<string>#include<vector>#include<algorithm>usingnamespace std;intmain(void){ string a, b; cin >> a >> b; vector<int> A; vector<int> B;for(int i = a.size()-1; i >=0;--i){ A.push_back(a[i]-'0');}for(int i = b.size()-1; i >=0;--i){ B.push_back(b[i]-'0');}/* 4321 765 */ vector<int>C(1000005);if(A.size()< B.size())swap(A, B);for(int i =0; i < B.size();++i){int j =0;int t =0;for(; j < A.size();++j){int num = A[j]* B[i]+ t;int ret = num %10; t = num /10; C[i + j]+= ret; C[i + j +1]+= C[i + j]/10; C[i + j]= C[i + j]%10;}if(t >0) C[i + j]+= t;}bool flag =true;for(int i = A.size()+ B.size(); i >=0;--i){if(i >0&& flag && C[i]==0){continue;}if(flag && C[i]!=0){ flag =false;} cout << C[i];}return0;}3.4 除法
除法这里用到了大整数减法。
#include<iostream>#include<vector>#include<string>#include<algorithm>usingnamespace std;// 比较两个大整数boolisGreaterOrEqual(vector<int> A, vector<int> B){if(A.size()!= B.size())return A.size()> B.size();for(int i = A.size()-1; i >=0;--i){if(A[i]!= B[i])return A[i]> B[i];}returntrue;}// 减法函数 vector<int>subtractBigInt(vector<int> A, vector<int> B){ vector<int> C;int t =0;for(int i =0; i < A.size()|| i < B.size();++i){int num =-t;if(i < A.size()) num += A[i];if(i < B.size()) num -= B[i];if(num <0){ t =1; num +=10;}else{ t =0;} C.push_back(num %10);}while(C.size()>1&& C.back()==0) C.pop_back();return C;}// 除法函数 pair<vector<int>, vector<int>>divideBigInt(vector<int> dividend, vector<int> divisor){ vector<int> quotient; vector<int> remainder = dividend;if(!isGreaterOrEqual(remainder, divisor)){return{ vector<int>{0}, remainder };} vector<int> current;for(int i = remainder.size()-1; i >=0;--i){ current.insert(current.begin(), remainder[i]);while(current.size()>1&& current.back()==0) current.pop_back();int q =0;while(isGreaterOrEqual(current, divisor)){ current =subtractBigInt(current, divisor); q++;} quotient.push_back(q);}reverse(quotient.begin(), quotient.end());while(quotient.size()>1&& quotient.back()==0) quotient.pop_back();return{ quotient, current };}intmain(){ string a, b; cin >> a >> b; vector<int> A, B;for(int i = a.size()-1; i >=0;--i) A.push_back(a[i]-'0');for(int i = b.size()-1; i >=0;--i) B.push_back(b[i]-'0');auto[quotient, remainder]=divideBigInt(A, B); cout <<"Quotient: ";for(int i = quotient.size()-1; i >=0;--i) cout << quotient[i]; cout <<"\nRemainder: ";for(int i = remainder.size()-1; i >=0;--i) cout << remainder[i];return0;}四、数组基本操作
4.1 增删改查
- 增
int arr[50];// 开辟足够大的数组int arr[2]=5;// 比如现在有两个元素了,我们增加一个元素。那就是 为下标2 赋值- 删
// 通过覆盖元素实现删除int arr[50]={1,2,3,4,5};// 起始位置为待删除元素位置 终止位置为最后元素位置for(int i =0; i < n;++i){ arr[i]= arr[i+1];}// 由于删掉一个元素 所以长度 从 n => n - 1for(int i =0; i < n -1;++i){ cout << arr[i]<<" ";}- 改
arr[5]=18;// 改就是重新赋值- 查
cout << arr[5]<< endl;// 直接访问即可4.2 数组元素交换与自身插入
- 数组元素交换
// 其实就是依赖于下标找到 A 和 B 然后进行 交换操作int temp = arr[2]; arr[2]= arr[3]; arr[3]= temp;- 自身插入:在一个数组中,A元素要插入到B元素的前面或后面
// 这个主要用的是 与删除操作 元素覆盖逆着来的写法。// 比如我们要把下标3的元素 插入到目前下标为1的前面int t = arr[3];for(int i =3-1; i >=1;--i){ arr[i +1]= arr[i];// 这个元素覆盖就是从左往右覆盖的(由于会覆盖掉待移动的元素,所以我们提前用 变量t 存储起来了)} arr[1]= t;// 从下标1 开始都整体往右移动了一位,所以我们只需要将 下标1位置重新赋值为t 即可完成需求// 比如我们要把下标3的元素 插入到目前下标为1的后面int t = arr[3];for(int i =3-1; i >1;--i){ arr[i +1]= arr[i];// 这个元素覆盖就是从左往右覆盖的(由于会覆盖掉待移动的元素,所以我们提前用 变量t 存储起来了)} arr[1+1]= t;4.3 实现动态数组
动态数组是普通数组的增强版,我记忆非常尤新。大一上学期的大作业,我写的就是动态数组的基本实现。
主要它是要实现以下几个扩展功能:
- 自动扩容
- 增删改查方法
- size方法
#include<iostream>#include<cstring>usingnamespace std;template<typenameT>classDynamicArray{private: T*data; size_t size; size_t capacity;// 扩容voidreserve(size_t new_capacity){if(new_capacity <= capacity)return; T* new_data =new T[new_capacity];// 分配新内存for(size_t i =0; i < size; i++){ new_data[i]= data[i];}delete[] data;// 释放旧内存 data = new_data; capacity = new_capacity;}public:DynamicArray(size_t init_capacity =4):size(0),capacity(init_capacity >0? init_capacity :4){ data =new T[capacity];// 分配初始内存}// 析构函数(释放内存)~DynamicArray(){delete[] data;}voidpush_back(const T& elem){if(size >= capacity){reserve(capacity *2);} data[size++]= elem;// size 就是下标 size++ 就是数量}voidremove(size_t index){if(index >= size){throwout_of_range("index out of bounds");return;}for(int i = index; i < size -1;++i){ data[i]= data[i +1];} data[size -1].~T();// 调用析构函数 size--;// 数量少了一个!} T&operator[](size_t index){if(index >= size){throwout_of_range("index out of bounds");}return data[index];} size_t get_size()const{return size;} size_t get_capacity()const{return capacity;}};intmain(void){ DynamicArray<int> arr; arr.push_back(12);for(int i=0;i<arr.get_size();i++){ cout << arr[i]<<" ";}return0;}五、双指针
双指针严格来说不能是算法,它更像是一种技巧或思想。到后面的学习里,我们还会讲到贪心,它也是一种思想。双指针常被用于线性有单调性趋势遍历的优化之中!使其从 O(n^2) => O(n)
5.1 快慢指针(龟兔指针)
概念:通过使用两个指针以不同的速度遍历数据结构(线性结构)。 —— 滑动窗口与快慢指针的思想就很类似
检测链表是否有环
找链表中间节点
删除倒数第N个节点
判断是否为回文链表
寻找数组中重复元素
- 快指针:先行一步满足前置条件(探索者)快速遍历并检测符合条件的数据
- 慢指针:配合快指针解决需求(建造者)维护已处理数据的正确边界
利用两者之间的速度差,可以在某个时刻根据特殊规则,获得或处理某些数据。
大白话:根据需求,当发现只需要用一个变量,先行一步拿到前置条件,再用另一个变量配合该前置条件,就可以完成需求时,那么你应该尝试下快慢指针。
5.1.1 有序数组去重
#include<iostream>usingnamespace std;int arr[50];intmain(void){int n; cin >> n;for(int i =0; i < n;++i){ cin >> arr[i];}if(n ==0)return0;// 边界检查int i =0;// 慢指针(为了配合快指针的)for(int j =1; j < n;++j){// 快指针的目的是找到非重复项if(arr[j]!= arr[i]){// 找到之后,用非重复项覆盖掉重复项 i++;// 慢指针前进 arr[i]= arr[j];// 覆盖重复项}}// 输出去重后的数组(前 i+1 个元素)for(int k =0; k <= i;++k){ cout << arr[k]<<" ";} cout << endl;return0;}5.1.2 数组的原地删除
原地删除某元素:比如数组 1,2,3,4,5,6,6,2,1,2,3,2 你就想把 里面的 2 全部删掉。逆向思维去看,其实就是只保留不是2的元素。
#include<iostream>usingnamespace std;voidremove_element(arr, n, val){int slow =0;for(int fast =0; fast < n;++fast){// 快指针去遍历if(arr[fast]!= val){// 进行筛选 arr[slow++]= arr[fast];// 慢指针给数组重新赋值符合条件的 arr[fast]}}}5.1.3 最长连续不重复子序列(经典例题)

#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];intmain(void){int n; cin >> n;for(int i =0; i < n;++i){ cin >> arr[i];}int maxLen =0;// 记录最长长度// n^3// n^2// 计数排序// nint cnt[maxN]={0};int i =0;// 慢指针for(int j =0; j < n;++j){ cnt[arr[j]]++;while(cnt[arr[j]]>1){ cnt[arr[i]]--; i++;}if(maxLen < j - i +1) maxLen = j - i +1;} cout << maxLen << endl;return0;}5.2 左右指针
左右指针是一种强大的算法技巧,其归属于双指针。特别适合处理数组和字符串问题。与快慢指针不同,左右指针同时从序列的两端向中间移动,形成 对称扫描!

左右指针最直接的应用就是(唐突):二分查找、二分法答案
5.2.1 基础模板
voidtwo_pointers(int* arr,int n){int l =0, r = n -1;// r 是最后元素的下标while(l < r){// 只要 l < r 就继续扫描if 左侧条件满足 { l++;}if 右侧条件满足 { r--;}// 当然可能同时满足(这个所谓的条件,都是人为定的)}}5.2.2 两数之和 II - 输入有序数组 (LeetCode 167)
题目:给定一个已按非递减顺序排列的整数数组 numbers,请你从数组中找出两个数,它们的和等于目标数 target。假设每个输入只对应唯一的答案,并且不能使用相同的元素两次。
那么这道题。如果暴力去做,怎么样也得 O(n^2)。但由于题目说提供的数据是有序的,就可以利用左右指针优化到 O(n)
voidtwo_sum(int*arr,int n,int target){// 若升序,肯定遵循左小右大int l =0, r = n -1;while(l < r){int total = arr[l]+ arr[r];if(total == target){ cout << l <<" "<< r << endl;break;}elseif(total < target){// 证明左边的太小了 l++;}else{// 证明右边的太大了 r--;}} couit <<""<< endl;}5.2.3 反转字符串 (洛谷 P5705 【深基2.例7】数字反转)
题目:编写一个函数,将输入的字符串反转过来。
#include<iostream>usingnamespace std;intmain(void){ string a; cin >> a;int i =0, j = a.size()-1;while(i < j){char t = a[i]; a[i]= a[j]; a[j]= t; i++; j--;} cout << a << endl;return0;}六、二分法
6.1 二分查找
- while(l < r)
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];intmain(void){int n,x;s cin >> n >> x;for(int i =0; i < n;++i){ cin >> arr[i];}int l =0, r = n -1;while(l < r){int mid =(l + r)/2;if(arr[mid]> x){ r = mid -1;}elseif(arr[mid]< x){ l = mid +1;}else{ cout << mid << endl;return0;}}if(arr[l]== x) cout << l << endl;else cout <<-1<< endl;return0;}- while(l <= r)
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];intmain(void){int n,x; cin >> n >> x;for(int i =0; i < n;++i){ cin >> arr[i];}int l =0, r = n -1;while(l <= r){int mid =(l + r)/2;if(arr[mid]> x){ r = mid -1;}elseif(arr[mid]< x){ l = mid +1;}else{ cout << mid << endl;return0;}} cout <<-1<< endl;return0;}6.2 左右边界
- while(l < r)
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];intmain(void){int n,x; cin >> n >> x;for(int i =0; i < n;++i){ cin >> arr[i];}int l =0, r = n -1;// 1 2 2 2 3 while(l < r){int mid =(l + r)/2;if(arr[mid]>= x){ r = mid;}else{ l = mid +1;}}if(arr[l]== x) cout << l << endl;else cout <<-1<< endl; l =0, r = n -1;// 1 2 2 2 3 while(l < r){int mid =(l + r +1)/2;if(arr[mid]<= x){ l = mid;}else{ r = mid -1;}}if(arr[r]== x) cout << r << endl;else cout <<-1<< endl;return0;}- while(l <= r)
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];intmain(void){int n,x; cin >> n >> x;for(int i =0; i < n;++i){ cin >> arr[i];}int l =0, r = n -1;// 1 2 2 2 3 while(l <= r){int mid =(l + r)/2;if(arr[mid]< x){ l = mid +1;}else{ r = mid -1;}}if(arr[l]== x) cout << l << endl;else cout <<-1<< endl; l =0, r = n -1;// 1 2 2 2 3 while(l <= r){int mid =(l + r)/2;if(arr[mid]> x){ r = mid -1;}else{ l = mid +1;}}if(arr[r]== x) cout << r << endl;else cout <<-1<< endl;return0;}七、筛质数
质数:只能被1 与 自身整除的数。
7.1 朴素法 O(nlogn)
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];boolisPrime(int x){if(x <=1)returnfalse;for(int i =2; i < x;++i){if( x % i ==0&& x !=2){returnfalse;}}returntrue;}intmain(void){int n; cin >> n;for(int i =2; i <= n;++i){if(isPrime(i)){ cout << i << endl;}}return0;}这里还会有个数学证明,优化了遍历条件。提高了效率,使其 i < x 化为 i * i <= x
- 首先我们要知道,x = a * b,x / a = b。那么此时比如 i = a,则 x / i = b,也就是说如果一旦它有个因子是a,肯定会有个b的,我们的i就是帮他试试有没有这样的因子a。这个是前提!那么问题了,如果 x 可以被平方根,是不是就直接能确定因子 sqrt(x),那么就不难发现,其它因子无非就是比 sqrt(x) 大或者小,如果 a是比 sqrt(x) 大,那么 b 就比 sqrt(x) 小,这是肯定的。此时我们其实就可以给 i 规定最大范围,即 i <= sqrt(x) 这是没毛病的,因为只要我在 小于等于 sqrt(x) 范围内尝试是否有一个因子,那么另外一个因子也就被确定了。合乎数学逻辑。
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];boolisPrime(int x){if(x <=1)returnfalse;for(int i =2; i * i <= x;++i){if( x % i ==0&& x !=2){returnfalse;}}returntrue;}intmain(void){int n; cin >> n;for(int i =2; i <= n;++i){if(isPrime(i)){ cout << i << endl;}}return0;}7.2 埃氏筛 O(nloglogn)
朴素筛的时候,我们就知道用数学所谓的 平方根,因子相关知识进行优化。那么也不难想到,任何数的倍数其实都不可能是质数的!所以我们只需要筛出不是质数的,剩下的就是质数喽。
#include<iostream>#include<string.h>usingnamespace std;constint maxN =1e5+10;bool primeArr[maxN];intmain(void){int n; cin >> n;memset(primeArr,true,sizeof(bool)* maxN); primeArr[0]=false; primeArr[1]=false;for(int i =2; i * i <= n;++i){for(int j =2* i; j < n; j += i){ primeArr[j]=false;}}for(int i =0; i < n;++i){if(primeArr[i]){ cout << i << endl;}}return0;}7.3 欧拉筛 O(n)
其实朴素筛、埃氏筛 速度已经不错了,但有些时候可能需要最好的性能。此时就需要 使用 欧拉筛,它主要是优化了埃氏筛的缺点。
埃氏筛中,有这样一种情况,会导致多筛!比如 6 = 2 * 3,那么 对于2来说会有 * 3的时候,而对于 3 来说又会有*2的时候,但其实只需要筛一次就够,如果能保证筛一次的话,时间复杂度就可以 O(n)
- 基本观察:每个非质数(合数)都会有唯一的
最小质因子 - 关键机制:如果我们可以确保每个合数只在它 最小质因子与对应因数相乘时才被标记为
合数,那么就只筛了一次,不会多筛。就比如说 6 只在 最小质因子 2 乘以 3 的时候,才会被标记,其余情况不会被标记。
#include<iostream>#include<string.h>usingnamespace std;constint maxN =1e5+10;int primes[maxN];// 升序bool vis[maxN];intmain(void){int n; cin >> n;memset(vis,false,sizeof(bool)* maxN);// 假设都没被标记为合数int pos =0;for(int i =2; i <= n;++i){if(!vis[i])// 先判断是不是合数{ primes[pos++]= i;// i 是质数// 当前这个质数 * 其它已经确认的质数 = 合数for(int j =0;j < pos && i * primes[j]<= n;++j){ vis[i * primes[j]]=true;// 这个是合数}}else{// 当前这个合数 * 已经确认的质数(从最小的开始) = 合数// 但当前这个合数很可能是 某个最小质数的倍数,所以如果用它继续筛就筛多了// 必须判断出它是哪个最小质数的倍数for(int j =0;j < pos && i * primes[j]<= n;++j){ vis[i * primes[j]]=true;// 这个是合数if(i % primes[j]==0){// 看下当前这个 i 是不是最小质数break;// 证明不是最小质数了,可以是 primes[j] 的倍数,那么这个 primes[j] 在后续就可以帮他继续筛完// 即 当前的 i 就不用去做没意义的筛了。}}}}for(int i =0; i <pos;++i){ cout << primes[i]<< endl;}return0;}八、前缀和与差分
前缀和:快速求某个区间中所有元素的累加和。(所以这些数肯定是要 反复使用进行累加的)差分:对多个区间进行加数,快速得到操作后的数组集合(需要学习前缀和)
8.1 一维前缀和
- 朴素写法
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];intmain(){int n; cin >> n;for(int i =0; i < n;++i){ cin >> arr[i];}int p,q; cin >> p >> q;int sum =0;for(int i = p; i <= q;++i){ sum += arr[i];} cout << sum << endl;return0;}- 前缀和写法
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN];int sum[maxN]={0};intmain(){int n; cin >> n;for(int i =0; i < n;++i){ cin >> arr[i];} sum[1]= arr[0];for(int i =2; i <= n;++i){ sum[i]= sum[i -1]+ arr[i -1];}// 1 2 3 4 5// 1 3 6 10 15for(int i =0; i < n +1;++i){ cout << sum[i]<<" ";}int p,q; cin >> p >> q; cout << endl; cout << sum[q +1]- sum[p -1]<< endl;return0;}8.2 二维前缀和
核心思想还是 前缀和,只不过是从 一维数组拓展到了二维矩阵。所以我们要探索有哪些变化,总结出通式,解决二维前缀和问题。
#include<iostream>usingnamespace std;constint maxN =1005;int arr[maxN][maxN]={0};int sum[maxN][maxN]={0};intmain(){int n,m; cin >> n >> m;for(int i =1; i <= n; i++){for(int j =1; j <= m; j++){ cin >> arr[i][j];}}for(int i =1; i <= n; i++){for(int j =1; j <= m; j++){ sum[i][j]= sum[i -1][j]+ sum[i][j -1]- sum[i -1][j -1]+ arr[i][j]; cout << sum[i][j]<<" ";} cout << endl;}int q; cin >> q;while(q--){int x1, y1, x2, y2; cin >> x1 >> y1; cin >> x2 >> y2; cout << sum[x2][y2]- sum[x1 -1][y2]- sum[x2][y1 -1]+ sum[x1 -1][y1 -1]<< endl;}return0;}8.3 一维差分
原理:构建差分数组,将其当作新的原数组。然后进行前缀和操作,意外发现由差分数组还原成了真正的原数组。所以,只要我们在差分数组里进行区域内的加数操作,得到普遍规律可循,最后进行前缀和就可以得到我们的目标数组。
#include<iostream>usingnamespace std;constint maxN =1e5+10;int arr[maxN]={0};int d[maxN]={0};intmain(){int n,q; cin >> n >> q;for(int i =1; i <= n; i++){ cin >> arr[i];}for(int i =1; i <= n; i++){ d[i]= arr[i]- arr[ i -1];}while(q--){int l, r, c; cin >> l >> r >> c; d[l]+= c;if(r +1< n){ d[r +1]-= c;}}for(int i =1; i <= n; i++){ arr[i]= d[i]+ arr[i -1]; cout << arr[i]<<" ";}return0;}8.4 二维差分
#include<iostream>usingnamespace std;constint maxN =1005;int arr[maxN][maxN]={0};int d[maxN][maxN]={0};// 差分数组int sum[maxN][maxN]={0};// 前缀和数组intmain(){int n, m, q; cin >> n >> m >> q;// 输入原始数组并初始化差分数组for(int i =1; i <= n; i++){for(int j =1; j <= m; j++){ cin >> arr[i][j];// 计算差分数组 d[i][j]= arr[i][j]- arr[i-1][j]- arr[i][j-1]+ arr[i-1][j-1];}}// 处理查询while(q--){int x1, y1, x2, y2, c; cin >> x1 >> y1 >> x2 >> y2 >> c;// 更新差分数组 d[x1][y1]+= c;if(y2 +1<= m) d[x1][y2+1]-= c;if(x2 +1<= n) d[x2+1][y1]-= c;if(x2 +1<= n && y2 +1<= m) d[x2+1][y2+1]+= c;}// 通过差分数组计算前缀和数组for(int i =1; i <= n; i++){for(int j =1; j <= m; j++){ sum[i][j]= d[i][j]+ sum[i-1][j]+ sum[i][j-1]- sum[i-1][j-1]; cout << sum[i][j]<<" ";} cout << endl;}return0;}九、链表、队列、栈
9.1 单链表
#include<iostream>usingnamespace std;structNode{ Node* next;int data;};intmain(){ Node* A =newNode(); Node* B =newNode(); A -> data =1; B -> data =2; A -> next = B; B -> next =NULL; Node* currentNode = A;// 1. 遍历// Node* currentNode = A;// while (currentNode != NULL) {// cout << currentNode -> data << endl;// currentNode = currentNode -> next;// }// 2. 通关下标找到目标节点// int pos;// cin >> pos;// for (int i = 0; i < pos; i++) {// if (i == 0) {// currentNode = A;// }else {// currentNode = currentNode -> next;// }// if (currentNode == NULL) {// break;// }// }// if (currentNode == NULL) cout << A->data << endl;// else cout << currentNode->data << endl;// 3. 通过下标删除目标节点// int pos;// cin >> pos;// Node* preNode = A;// currentNode = preNode -> next;// if (pos == 0) {// A = currentNode;// delete preNode;// }else if (pos > 0) {// for (int i = 2; i <= pos; i++) {// preNode = currentNode;// currentNode = preNode -> next;// if (currentNode == NULL) {// break;// }// }// if (currentNode == NULL) {cout << "删除失败" << endl;}// else {// preNode -> next = currentNode->next;// delete currentNode;// }// }//// currentNode = A;// while (currentNode != NULL) {// cout << currentNode -> data << endl;// currentNode = currentNode -> next;// }// 4. 在下标后面插入一个节点int pos; cin >> pos;int data; cin >> data;for(int i =0; i <= pos; i++){if(i ==0){ currentNode = A;}else{ currentNode = currentNode -> next;}if(currentNode ==NULL){break;}}if(currentNode ==NULL) cout <<-1<< endl;else{ Node* newNode =newNode(); newNode -> data = data; newNode -> next = currentNode ->next; currentNode -> next = newNode;} currentNode = A;while(currentNode !=NULL){ cout << currentNode -> data << endl; currentNode = currentNode -> next;}return0;}9.2 双链表
#include<iostream>usingnamespace std;structNode{ Node* pre; Node* next;int data;};intmain(){ Node* head =newNode(); Node* A =newNode(); Node* B =newNode(); head -> next = A; A -> data =1; B -> data =2; A -> pre = head; A -> next = B; B -> pre = A; B -> next =NULL; Node* cNode = head -> next;while(cNode !=NULL){ cout << cNode -> data << endl; cNode = cNode -> next;}// 删除int pos; cin >> pos;int t = pos +1;// 第 pos + 1 个 cNode = head;for(int i =1; i <= t; i++){ cNode = cNode -> next;if(cNode ==NULL){ cout <<"删除失败"<< endl;return0;}} Node* preNode = cNode -> pre; preNode -> next = cNode->next;delete cNode; cNode = head -> next;while(cNode !=NULL){ cout << cNode -> data << endl; cNode = cNode -> next;}return0;}9.3 栈
#include<iostream>usingnamespace std;structNode{ Node* pre; Node* next;int data;}; Node* tail =NULL;voidpush(int data){ Node* newNode =newNode(); newNode->data = data; newNode->next =NULL; newNode->pre = tail; tail->next = newNode; tail = newNode;}intpop(){ Node* preNode = tail->pre; preNode -> next =NULL;int ret = tail -> data;delete tail; tail = preNode;return ret;}intmain(){ Node* head =newNode(); Node* A =newNode(); Node* B =newNode(); head -> next = A; A -> data =1; B -> data =2; A -> pre = head; A -> next = B; B -> pre = A; B -> next =NULL; tail = B; Node* cNode = head -> next;while(cNode !=NULL){ cout << cNode -> data << endl; cNode = cNode -> next;}push(3);push(4); cNode = head->next;while(cNode !=NULL){ cout << cNode->data << endl; cNode = cNode -> next;}pop(); cNode = head->next;while(cNode !=NULL){ cout << cNode->data << endl; cNode = cNode -> next;}return0;}9.4 队列
#include<iostream>usingnamespace std;structNode{ Node* pre; Node* next;int data;}; Node* tail =NULL;voidpush(int data){ Node* newNode =newNode(); newNode->data = data; newNode->next =NULL; newNode->pre = tail; tail->next = newNode; tail = newNode;}intpop(){ Node* preNode = tail->pre; preNode -> next =NULL;int ret = tail -> data;delete tail; tail = preNode;return ret;} Node* head =newNode();intoutQue(){ Node* firstNode = head->next;if(firstNode ==NULL)return-1; head -> next = firstNode -> next;int ret = firstNode -> data;delete firstNode;return ret;}intmain(){ Node* A =newNode(); Node* B =newNode(); head -> next = A; A -> data =1; B -> data =2; A -> pre = head; A -> next = B; B -> pre = A; B -> next =NULL; tail = B; Node* cNode = head -> next;while(cNode !=NULL){ cout << cNode -> data << endl; cNode = cNode -> next;}push(3);push(4); cNode = head->next;while(cNode !=NULL){ cout << cNode->data << endl; cNode = cNode -> next;}outQue(); cNode = head->next;while(cNode !=NULL){ cout << cNode->data << endl; cNode = cNode -> next;}return0;}9.5 数组模拟链表、栈、队列
链表既然能够实现栈和队列,那么自然 数组就也可以实现 栈和队列,甚至可以 模拟链表的逻辑。我们来看下。
- 模拟链表
链表结构的核心就在于 “链”,它可以直接找到对应的节点。那么我们可以通过 数组构建一个映射表,该数组的下标就相当于某个节点,然后存储的数据就是 链接的节点下标。这样我们就构建出了单链表的逻辑结构。
#include<iostream>usingnamespace std;constint maxN =1e5+10;int nodeArr [maxN];int linkArr [maxN];intmain(){ linkArr[0]=1;// 0号 指向 1号节点 nodeArr[1]=1;// 1号 节点 nodeArr[2]=2;// 2号 节点 linkArr[1]=2;// 1号 指向 2号节点 linkArr[2]=-1;// 定义 -1 是下个节点无 NULLint nextPos = linkArr[0];int cNode = nodeArr[nextPos];while(nextPos !=-1){ cout << cNode << endl; nextPos = linkArr[cNode]; cNode = nodeArr[nextPos];}return0;}- 模拟栈
#include<iostream>usingnamespace std;constint maxN =1e5+10;int stack [maxN];int tail =0;// 当前要入栈位置voidpush(int data){if(tail >= maxN){ cout <<"满了"<<endl;return;} stack[tail++]= data;}intpop(){if(tail ==0)return-1;return stack[--tail];}intmain(){push(1);push(2);for(int i =0; i < tail;++i){ cout << stack[i]<< endl;}pop();for(int i =0; i < tail;++i){ cout << stack[i]<< endl;}return0;}- 模拟队列
#include<iostream>usingnamespace std;constint maxN =1e5+10;int q[maxN];int top =0;int tail =-1;voidpush(int x){ q[++tail]= x;}intpop(){return q[top++];}intmain(){push(2);if(tail < top){ cout <<"空的"<< endl;} cout <<"数量"<< tail +1- top << endl; cout <<pop()<< endl;return0;}- 模拟循环队列
#include<iostream>usingnamespace std;constint maxN =1e5+10;int q[maxN];int top =0;// 头元素下标int tail =0;// 待插入节点的下标(即队尾的下一个下标)boolisFull(){return(tail +1)% maxN == top;}voidpush(int data){if(isFull()){ cout <<"队列满了"<< endl;return;} q[tail]= data; tail =(tail +1)% maxN;}intoutQ(){if(top == tail){ cout <<"队列空的"<< endl;return-1;}int ret = q[top]; top =(top +1)% maxN;return ret;}intmain(){push(1);push(2); cout <<outQ()<< endl;return0;}十、DFS、BFS
讲DFS与BFS 这就不得不说一个典故 (迷宫)
想象一下,你进入了一个巨大的迷宫,你的目标是从起点S找到终点T。 迷宫的结构如下,其中#代表墙,不能走;.代表路代表路,可以走。
S . . # . . . . # . . . # . . # . . . . . . . # # . . . # . # T . . . 现在有两个问题被提出:
- 走到终点的方案有多少种,分别都打印出来(DFS)
- 走到终点最近的方案是什么路径,打印出来(BFS)
10.1 DFS
DFS:深度优先搜索,被戏称为一路走到黑。随便选一条路,死磕到底,碰壁后再倒回来换条路去走。
- 你的行动方式:
- 从起点
S开始,开始,你标记“我已经来过这里了”。 - 你面前有多个岔路口,你随机选一个(比如总是先向右),然后一直走下去。
- 每到一个新地方,你都继续沿用这个规则(比如先向右),完全不管其他还没走的岔路。
- 如果有一天你发现前面是墙或者已经走过的地方(死胡同),你就原路返回,回到上一个有未探索岔路口的地方,换另一条路继续深入。
- 直到你在某条路的尽头发现了终点
T。
- 从起点
- 辅助容器:栈
- 为了管理这种“一路到底,然后回溯”的过程,你需要另一个工具——栈。
- 把起点
S压入栈。 - 只要栈不为空,就取出栈顶的那个位置(比如
B)。注意,拿的是最新的那个! - 检查
B是不是终点?如果是,胜利! - 如果不是,就把
B周围所有能走且没走过的邻居,按顺序压入栈。 - 重复步骤2。(这样,下一次就会先探索刚压进去的其中一个邻居,从而不断深入)
- 非递归版本
#include<iostream>usingnamespace std;// 我们是 节点,二维数组,所以最好是 建立 x,y // 题目要求我们 用 x,y 下标 去确定一个节点的话,那么我们就要定义 这样的结构typedefstruct{int x, y;}Node;typedefstruct{ Node point;int dir;// 下次探索的方向 0 1 2 3}SNode;constint maxN =105; Node d[4]={{-1,0},{0,1},{1,0},{0,-1}};bool vis[maxN][maxN];// 为走过的节点打标记 Node path[maxN * maxN];int pathLen =0;char arr[maxN][maxN]; SNode s[maxN * maxN];int top =-1;intmain(void){int n,m; cin >> n >> m;for(int i =0; i < n;++i){for(int j =0; j < m;++j){ cin >> arr[i][j]; vis[i][j]=false;if(arr[i][j]=='S'){ s[++top]={i,j,0}; vis[i][j]=true; pathLen =1; path[0]={i,j};}}}while(top >=0){// 只有栈为空的时候,才不去探索 SNode* cNode =&s[top];if(cNode->dir >3){ vis[cNode->point.x][cNode->point.y]=false; pathLen--; top--;continue;}int nx = cNode->point.x + d[cNode->dir].x;int ny = cNode->point.y + d[cNode->dir].y; cNode->dir++;if(nx >=0&& nx < n && ny >=0&& ny < m &&!vis[nx][ny]&& arr[nx][ny]!='#'){ vis[nx][ny]=true; path[pathLen++]={nx,ny};if(arr[nx][ny]=='T'){for(int i =0; i < pathLen;++i){ cout << path[i].x <<","<< path[i].y <<" ";} cout << endl; vis[nx][ny]=false; pathLen--;}else{ s[++top]={nx,ny,0};}}}return0;}- 递归版本
#include<iostream>usingnamespace std;constint maxN =1005;typedefstruct{int x, y;} Point;typedefstruct{ Point p;int nextDir;// 下一步尝试的方向 (0~3)} StackNode;// 方向:上下左右 Point d[4]={{-1,0},{0,1},{1,0},{0,-1}};bool vis[maxN][maxN];// 是否走过了 Point path[maxN];int pathLen =0;int totalPaths =0;// 记录多少种路径方案char arr[maxN][maxN];int n, m;;// 5,7voiddfs(int x,int y){ vis[x][y]=true; path[pathLen++]={x, y};if(arr[x][y]=='T'){printf("Path %d:\n",++totalPaths);for(int i =0; i < pathLen; i++){printf("(%d,%d) ", path[i].x, path[i].y);}printf("\n\n");return;}for(int i =0; i <4; i++){int nx = x + d[i].x;int ny = y + d[i].y;if(nx >=0&& nx < n && ny >=0&& ny < m &&!vis[nx][ny]&& arr[nx][ny]!='#'){// 没有访问过dfs(nx, ny); vis[nx][ny]=false; pathLen--;}}}intmain(void){ cin >> n >> m;for(int i =0; i < n;++i){for(int j =0; j < m;++j){ cin >> arr[i][j];}}int startX =-1, startY =-1;// 从 -1 开始for(int i =0; i < n;++i){for(int j =0; j < m;++j){if(arr[i][j]=='S'){ startX = i; startY = j;break;}}if(startX !=-1)break;}if(startX ==-1){printf("没有起始点\n");return1;}for(int i =0; i < n; i++)for(int j =0; j < m; j++) vis[i][j]=false;dfs(startX, startY);if(totalPaths ==0){printf("No path found!\n");}else{printf("Total paths: %d\n", totalPaths);}return0;}10.2 BFS
DFS:广度优先搜索,又称为地毯式搜索。先探索所有最近的地方(层次感),再探索更远的地方。
- 你的行动方式:
- 从起点
S开始,你标记 “我已经来过这里了”。 - 你站在
S,向四周(上、下、左、右)可通行的方向看,把所有能直接到达的点都记录下来。这些是你的 “第一层” 邻居。 - 然后,你不再自己往前走,而是召唤出几个分身。每个分身去一个“第一层”邻居点。
- 所有这些分身在各自的位置上,再次重复你的动作:记录他们周围还未被探索的、“第二层第二层”的邻居点。
- 这个过程一层一层地展开,就像,就像在水里扔下一块石头,涟漪一圈一圈地扩散出去,直到某个分身找到了终点
T。
- 从起点
- 辅助容器:队列
- 为了管理这个“分层探索”的过程,你需要一个强大的工具——队列。
- 队列的特点:先进先出 (FIFO),就像现实生活中的排队。先进去的人先出来。
- 如何使用:
- 把起点
S放进队列。 - 只要队列不为空,就拿出队首的那个位置(比如
A)。 - 检查
A是不是终点?如果是,胜利! ! - 如果不是,就把
A周围所有能走且没走过的邻居,按顺序(比如上、下、左、右)依次放入队列的末尾。 - 重复步骤2。
- 把起点
#include<algorithm>#include<iostream>#include<ranges>#include<vector>usingnamespace std;constint maxN =1005;structPoint{int x;int y;}; Point q[maxN];int top =0;int tail =0;boolifFull(){return(tail +1)% maxN == top;}voidpush(Point data){if(ifFull()){return;} q[tail]= data; tail =(tail +1)% maxN;} Point outQ(){if(top == tail){ cout <<"队列空的"<< endl;return{-1,-1};} Point ret = q[top]; top =(top +1)% maxN;return ret;} Point d[4]={{-1,0},{0,1},{1,0},{0,-1}};char arr[maxN][maxN];bool vis[maxN][maxN]; Point pre[maxN][maxN];// 记录当前节点的上一个节点intmain(){int n,m;int startX, startY;int endX, endY; cin >> n >> m;for(int i =0; i < n; i++){for(int j =0; j < m; j++){ cin >> arr[i][j]; vis[i][j]=false;if(arr[i][j]=='S'){ startX = i; startY = j;}if(arr[i][j]=='T'){ endX = i; endY = j;}}}push({startX, startY}); vis[startX][startY]=true; pre[startX][startY]={-1,-1};bool flag =false;// 默认没找到 Twhile(top != tail){ Point cNode =outQ();if(cNode.x == endX && cNode.y == endY){ flag =true;break;}for(int i =0; i <4; i++){int nx = cNode.x + d[i].x;int ny = cNode.y + d[i].y;if(nx <0|| nx >= n || ny <0|| ny >= m)continue;if(arr[nx][ny]=='#'|| vis[nx][ny])continue; vis[nx][ny]=true; pre[nx][ny]= cNode;push({nx, ny});}}if(!flag){ cout <<"根本没终点"<<endl;}else{ vector<Point> path;int x = endX, y = endY;while(x !=-1|| y !=-1){ path.push_back({x,y}); Point p = pre[x][y]; x = p.x; y = p.y;}reverse(path.begin(), path.end()); cout << endl;for(int i =0; i < path.size(); i++){ cout << path[i].x <<" "<< path[i].y;if(i != path.size()-1) cout <<" -> ";}}return0;}十一、贪心思想(完结)
贪心思想的核心是:每一步都做出当前看起来“最好”的选择,希望通过局部最优解最终达到全局最优解。
注意:贪心算法并不总能得到全局最优解,但在某些特定问题上,它是有效的,并且效率很高!
- 贪心选择性质:每一步都做当前看起来最好的选择,不考虑未来。
- 最优子结构:问题的最优解包含其子问题的最优解。
- 无后效性:一旦做出选择,就不会再改变,也不会影响之前的选择。
11.1 找零钱问题
问题:假设你有面值为 1元、5元、10元 的硬币,要找给顾客 36 元,要求用最少数量的硬币。
贪心:每次都尽可能用最大面值的硬币就行了。
#include<stdio.h>intmain(){// 定义硬币面值(按从大到小排序)int coins[]={10,5,1};int numCoins =3;// sizeof(coins) / sizeof(coins[0]); 这个写法是计算数组元素个数的技巧写法int amount;printf("请输入要找零的金额(元): ");scanf("%d",&amount);if(amount <0){printf("金额不能为负数!\n");return1;}int totalCoins =0;printf("找零方案:\n");for(int i =0; i < numCoins; i++){int count = amount / coins[i];// 当前面值能用多少个if(count >0){printf("%d元硬币:%d枚\n", coins[i], count); totalCoins += count; amount -= coins[i]* count;}}printf("总共需要硬币:%d枚\n", totalCoins);return0;}11.2 活动选择问题(区间调度)
问题:有 n 个活动,每个活动有开始时间和结束时间。你一次只能参加一个活动,如何选择最多的活动?
- 贪心:优先选择结束时间最早的活动。因为结束得早,留给后面的时间就多,能安排更多活动。
#include<stdio.h>#include<stdlib.h>// 定义活动结构体typedefstruct{int start;int finish;int id;// 活动编号(可选,用于输出原始顺序)} Activity;// 比较函数:用于 qsort,按结束时间升序排序intcompare(constvoid*a,constvoid*b){ Activity *actA =(Activity *)a; Activity *actB =(Activity *)b;return actA->finish - actB->finish;}intmain(){int n;printf("请输入活动数量: ");scanf("%d",&n);if(n <=0){printf("活动数量必须大于0。\n");return1;} Activity activities[n];printf("请输入每个活动的开始时间和结束时间(格式:start finish):\n");for(int i =0; i < n; i++){scanf("%d %d",&activities[i].start,&activities[i].finish); activities[i].id = i +1;// 编号从1开始// 简单校验if(activities[i].start >= activities[i].finish){printf("警告:活动%d的开始时间应小于结束时间!\n", i +1);}}// 按结束时间升序排序qsort(activities, n,sizeof(Activity), compare);// 贪心选择printf("\n最多可参加的活动如下(按结束时间排序):\n");int count =1;int lastFinish = activities[0].finish;printf("活动%d: [%d, %d]\n", activities[0].id, activities[0].start, activities[0].finish);for(int i =1; i < n; i++){if(activities[i].start >= lastFinish){printf("活动%d: [%d, %d]\n", activities[i].id, activities[i].start, activities[i].finish); count++; lastFinish = activities[i].finish;}}printf("\n最多可参加 %d 个活动。\n", count);return0;}