算法归纳
前言:数组知识
c语言,c++语言的数组地址是连续的,而java语言当中不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。
(一)二分查找
写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。
第一种写法:[left, right]
注意点:
①while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <=;
②if (nums[middle] > target) right 要赋值为 middle - 1;(因为nums[middle]不等于target,而我们是右闭区间,包括了right)
代码如下:
// 版本一
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <=
int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,所以[middle + 1, right]
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
第二种写法:[left, right)
注意点:
①while (left <right) 要使用 < ,因为left == right是没有意义的;
②if (nums[middle] > target) right 要赋值为 middle ;(而我们是右开区间,不包括right)
代码如下:
// 版本二
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right)
while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 <
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target 在左区间,在[left, middle)中
} else if (nums[middle] < target) {
left = middle + 1; // target 在右区间,在[middle + 1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值,直接返回下标
}
}
// 未找到目标值
return -1;
}
};
(二)双指针
用途
双指针算法一般是采用左右指针,对数组、字符串进行查找或排序
1.对撞指针
一个指针指向最左端,一个指针指向最右端,向中间逼近
eg:翻转数组,翻转字符串
2.快慢指针
一个指针走得快,一个指针走得慢
eg:1. 判断单链表是否存在环
2. 寻找循环链表的入口
3. 寻找链表的中间节点(快指针一次走两节点,慢指针一次走一个节点,当快指针指向最后一个节点时,慢指针刚好指向中间节点)
4. 判断两个链表是否相交
3.滑动窗口
(三)前缀和
前缀数组定义
前缀数组a[n];
数组b[n];
a[n]实际上就是对应的b数组的前n项和;
eg:
a[5]=b[1]+b[2]+b[3]+b[4]+b[5];
作用
在蓝桥杯每日一题中的3956截断数组中有用到
(四)差分
差分数组定义
差分数组b[n];
数组a[n];
b[n]实际上就是每一项数值都等于对应a[n]-a[n-1];恰好是和前缀和相反的
作用
当我们需要改变a数组中的某一段区域的值(让a[m]到a[n]中的每一项加c)时:
1.可以借助差分数组b,让b[m]+c,此时会导致如果利用差分数组求前项和得到的a数组中自m项起都会在原来的基础上加一个c;
2.但是此时的a[n]之后的项也加c了,所以我们可以让b[n+1]减去一个c,此时借助b数组前缀和求得的新a数组就满足要求了
在蓝桥杯每日一题中的3729改变数组元素中用到过
(五)字符哈希
最后得到的子串哈希值是:((hash[q]-hash[p-1]*p[q-p+1])%MOD+MOD)%MOD
(六)哈希表(主要针对C++中的unordered_map和unordered_set的常用操作)
哈希表讲解和构造
见bilibili链接
b站哈希表讲解视频
大概就是把需要储存查找的数据,找到其中的关键值,通过哈希函数求k值,储存在哈希表结构内,然后方便我们进行查找
求k值常用方法:除留余数法
处理冲突方法(个人觉得也就是储存结构):
开放定址法,链地址法
开放地址法
储存的时候,如果发现当前key计算之后的地址与之前的数据地址有冲突,直接去寻找空着的地址(一直向后寻找,或者+1,-1,+4,-4…),存入数据;那么当寻找的时候,就可能遇到去key本来计算出来的地址位置时,找不到对应的数据,这个时候就可以使用探测法,具体探测方法根据储存时的方法有关。
链地址法
储存的时候,构建一个类似于邻接表的结构,一个数组后链接很多个链表,当我们计算key值的地址出现冲突时,就将他们都链接到对应地址后的链表上,方便于寻找
unordered_map常用操作(还是不太会用)
建立
unordered_map <int,int>哈希表名字
向哈希表中添加元素
insert 函数
m.insert(pair<int,int>(1, 10));
m.insert(pair<int,int>(2, 20));
用数组方法直接添加
m[3]=30;
m[4]=40;
成员函数
begin(),end()函数
m.begin() //指向哈希表的第一个容器
m.end() //指向哈希表的最后一个容器,实则超出了哈希表的范围,为空
find()查找函数
m.find(2) //查找key为2的键值对是否存在 ,若没找到则返回m.end()
if(m.find(2)!=m.end()) //判断找到了key为2的键值对
count() 查找函数
查找哈希表中key为3的键值对,返回其数量,为1,则找到,若没找到则返回0
m.count(3) //返回 1
m.count(5) //返回0
size()函数
m.size() //返回哈希表的大小
empty()函数
m.empty() //判断哈希表是否为空,返回值为true/false
clear()函数
m.clear() //清空哈希表
swap()函数
交换两个哈希表中的元素,整个哈希表的键值对全部都交换过去
unordered_map<int,int> m1;
unordered_map<int,int> m2;
m1.swap(m2);
swap(m1,m2);
**unordered_set常用操作(更好用)
unordered_set<string> first//容器定义
first.empty()//判断容器是否是空,是空返回true,反之为false
first.size()//返回容器大小
first.maxsize()//返回容器最大尺寸
first.begin()//返回迭代器开始
first.end()//返回迭代器结束
first.find(value)//返回value在迭代器的位置
first.count(key)//返回key在容器的个数
first.insert(value)//将value插入到容器中
first.erase(key)//通过key删除
first.clear()//清空容器
count比较适合来判断容器中是否有这个元素(根据返回值是否为0判断)
(七)递推
具体CSDN讲解:
打开链接方法:ctrl+点击
1.汉诺塔
移动规则:
将A塔座上的n个盘子挪动到C塔座上
每次只能移动一个圆盘;圆盘可以插在X、 Y和Z中的任何一个塔座上;任何时刻都不能将一个较大的圆盘压在较小的圆盘之上
设f(n)为有n个盘子时需要挪动的次数。
f(0)=0;
f(1)=1;(限制条件)
f(n)=f(n-1)+1+f(n-1)=2*f(n-1)+1;
解释(f(n)公式的由来)
第一个f(n-1)表示将A塔座上的n-1个盘子挪到B塔座上(这里的次数是和f(n-1)相等的,因为f(n-1)就代表将n-1个盘子从A塔座上挪到C塔座上,是同理的);
中间的1表示将A塔座上剩余的最后一个盘子直接挪到C塔座上;
最后一个f(n-1)表示将B塔座上的n-1个盘子挪到C塔座上(这里的次数同样等于f(n-1));
2.求斐波那契数(较简单)
3.猴子吃桃(较简单,逆推法)
4.数字三角形(顺推法)
请编一个程序计算从顶到底的某处的一条路径,使该路径所经过的数字总和最大。只要求输出总和
a【n】【m】存放数据(如图右侧),除去存放数据位置以外其余位置均为0(方便后续正确计算)
设f【n】【m】表示当前位置的最大路径和;
f【1】【1】=a【1】【1】(存放数据时每一层都从下标1开始存放)(限制条件)
f【n】【m】=max{f【n-1】【m-1】,f【n-1】【m】};(实际上就是对应左图除去第一层外的每一个元素,都将自己和头顶上的两个数字中较大的那一个加上,一直这样连续地求下去,就能得到每一个位置的最大路径和了)
最后在得到的最后一层f中循环遍历,找到最大的路径和即可。
5.骨牌排满方格
有 2n 的一个长方形方格,用一个12 的骨牌铺满方格。
编写一个程序,试对给出的任意一个n(n>0), 输出铺法总数。
算法分析:
1.当n=1时:只有一种情况
2.当n=2时,要不横向排列,要不纵向排列,两种情况,如上图左侧
3.当n=3时,如上图右侧
4.当n=4时,如下图:
总结:除了n=1,n=2以外,可以进行讨论,如果第一个骨牌纵向分布,那么还剩下n-1个骨牌位置需要确定(如上图前三个),即f(n-1);
如果第一个骨牌横向分布,那么第二个骨牌的位置也就确定了,那么还剩下n-2个骨牌位置需要确定(如上图后两个),即f(n-2);
因此有:
f(1)=1;
f(2)=2;(限制条件)
f(n)=f(n-1)+f(n-2);
6.吃糖果
名名的妈妈从外地出差回来,带了一盒好吃又精美的巧克力给名名(盒内共有 N 块巧克力,20 > N >0)。
妈妈告诉名名每天可以吃一块或者两块巧克力。假设名名每天都吃巧克力,问名名共有多少种不同的吃完巧克力的方案。
算法分析:同5.骨牌排满方格
(八)kmp算法
主要针对前缀循环元的问题—见蓝桥杯每日一题Week2—141.周期。
题目描述:
一个字符串的前缀是从第一个字符开始的连续若干个字符,例如 abaab
共有 55 个前缀,分别是 a
,ab
,aba
,abaa
,abaab
。
我们希望知道一个 N 位字符串 S的前缀是否具有循环节。
换言之,对于每一个从头开始的长度为 i(i>1)的前缀,是否由重复出现的子串 A组成,即 AAA…A (A 重复出现 K次,K>1)。
如果存在,请找出最短的循环节对应的 K 值(也就是这个前缀串的所有可能重复节中,最大的 K 值)。
算法分析:
总结:
一个字符串的next数组,如果当前字符对应的 i%(i-next【i】)==0,那么当前的字符以前的前缀字符串具有循环元,**i/(i-next【i】)**就是它的循环元个数
(九)通过中序遍历和后序遍历重建二叉树,二叉树的层序遍历(直接给根节点的情况)
见蓝桥杯每日一题:1497. 树的遍历 - AcWing题库
代码:
#include<stdio.h>
#include<malloc.h>
typedef struct Bitree
{
int data;
struct Bitree* lchild;
struct Bitree* rchild;
}*pBitree,bitree;
typedef struct queue
{
int data;
pBitree p;
}queue;
void buildtree(int* a,int* b,int n,pBitree* t);
void cengtraverse(pBitree root,int n);
int main()
{
int n;
scanf("%d",&n);
int a[n];//后序遍历数组
int b[n];//中序遍历数组
int i;
for(i=0;i<n;i++)
{
scanf("%d",&a[i]);
}
for(i=0;i<n;i++)
{
scanf("%d",&b[i]);
}
pBitree root;
buildtree(a,b,n,&root);//根据中序遍历和后序遍历重建二叉树
cengtraverse(root,n);//层序遍历二叉树
return 0;
}
void buildtree(int* a,int* b,int n,pBitree* t)
{
int i=0;
//找到中序遍历中根节点的位置,确定左右子树的节点个数
for(i=0;i<n;i++)
{
if(a[n-1]==b[i])
{
break;
}
}
//如果没有找到就return
if(i>=n)
{
return;
}
//创建根节点
*t=(pBitree)malloc(sizeof(bitree));
(*t)->lchild=NULL;
(*t)->rchild=NULL;
(*t)->data=b[i];
//创建左子树
buildtree(a,b,i,&((*t)->lchild));//这里传入的n变成了i是因为左子树的节点个数只有i个(中序遍历中根节点左边的部分就是左子树的全部节点)
//创建右子树
buildtree(a+i,b+i+1,n-i-1,&((*t)->rchild));//这里传入的n变成n-i-1,是因为右子树节点的个数是n-i-1个(中序遍历中根节点右半部分就是右子树的全部节点),传入的后序遍历数组变成了原来后序遍历中右子树的数组部分,传入的中序遍历数组变成了原来中序遍历数组部分中右子树的部分。(关键点)
}
void cengtraverse(pBitree root,int n)
{
int traverse[n];
queue Q[n];
int rear=0;
int front=0;
Q[rear].data=root->data;
Q[rear++].p=root;
int num=0;
while(rear>front)
{
traverse[num++]=Q[front].data;
//printf("%d\n",traverse[num-1]);
if(Q[front].p->lchild!=NULL)
{
Q[rear].data=Q[front].p->lchild->data;
Q[rear++].p=Q[front].p->lchild;
}
if(Q[front].p->rchild!=NULL)
{
Q[rear].data=Q[front].p->rchild->data;
Q[rear++].p=Q[front].p->rchild;
}
front++;
}
int i;
for(i=0;i<n;i++)
{
printf("%d ",traverse[i]);
}
}
中序,后序建立二叉树思路:(递归)
根据中序加后序遍历重建二叉树
构造该二叉树的过程如下:
- 根据后序序列的最后一个元素建立根结点;
- 在中序序列中找到该元素,确定根结点的左右子树的中序序列;
- 在后序序列中确定左右子树的后序序列;
- 由左子树的后序序列和中序序列建立左子树;
- 由右子树的后序序列和中序序列建立右子树。
(十)最小生成树
1.普利姆算法
2.克鲁斯卡尔算法
(十一)C++相关知识回忆(2024蓝桥杯准备)
1.模板
#include<iostream>
using namespace std;
int main(){
}
2.输入输出
string a
cin>>a;//输入
cout<<a<<endl;//输出
getline(a);//接收字符串(可以有空格)(要用<string>库)
3.字符串
string s;//字符串
cin>>s;//输入字符串(不保留空格)
cout<<s<<endl;//输出字符串
// string s2;
// getline(cin,s2);//输入字符串(保留空格)(但是当前情况,前面已经有输入的情况,无法正常输入,因为残留了空格或者\n;
// cout<<s2<<endl;
string s3;//字符串
cin>>s3;//输入字符串(不保留空格)
cout<<s3<<endl;//输出字符串
cout<<s3+s<<endl;//拼接字符串s和s3
for(int i=0;i<s.size();i++){//循环输出字符串
cout<<s[i]<<endl;
}
//长度
string a="cswedcfv";
cout<<a.size();
//增加
s.insert(1,"dewsf");//在字符串中插入(插入位置,“插入内容”)
cout<<s<<endl;//输出字符串
//删除
s.erase(6,2);//删除字符串中从第几个字符开始的几个字符
cout<<s<<endl;//输出字符串
//修改
s[1]='1';//修改字符串当中的字符
cout<<s<<endl;//输出字符串
//查找
s.find(args) // 查找 s 中 args 第一次出现的位置
s.rfind(args) // 查找 s 中 args 最后一次出现的位置
s.find_first_of(args) // 在 s 中查找 args 中任何一个字符最早出现的位置
s.find_last_of(args) // 在 s 中查找 args 中任何一个字符最晚出现的位置
例如:
string s1 = "nice to meet you~";
cout << s1.find_first_of("mey") << endl; // 输出结果为 3,'e' 出现的最早
s.find_first_not_of(args) // 查找 s 中 第一个不在 args 中的字符的位置
s.find_last_not_of(args) // 查找 s 中 最后一个不在 args 中的字符的位置
例如:
string s1 = "nice to meet you~";
cout << s1.find_first_not_of("nop") << endl; // 输出结果为 1 ,'i' 不在 "nop" 里
//转化
string s = to_string(val)//将数值转化为string
//反转
//需要<algorithm>库
string s2 = "12345"; // 初始化一个字符串
reverse(s2.begin(), s2.end()); // 反转 string 定义的字符串 s2
cout << s2 << endl; // 输出 54321
s.insert(1,“”);
s.erase(1,2);
s.find(“”);
s.rfind(“”);
s.find_fiirst_of(“”);
s=to_string(val);
4.algorithm库
(1).max()、min()、abs()函数
max()
:求两个数最大值
min()
:求两个数最小值
abs()
:求一个数的绝对值
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int a = 3, b = 4;
//求最大值
int Max = max(a,b);
//求最小值
int Min = min(a,b);
//求绝对值
int Abs = abs(-3);
cout << Max << Min << Abs;
return 0;
}
(2)交换函数:swap()
用来交换x和y的值
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int a = 3, b = 4;
swap(a,b);
cout << a << b;
return 0;
}
(3)翻转函数:reverse()
翻转x-y区间的数组、容器的值。
1、翻转整个数组
翻转整个数组:
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int a[5] = {11,22,33,44,55};
reverse(a,a+5);
for(int i = 0; i < 5; i++)
cout << a[i] << ' ';
return 0;
}
2、对部分值的翻转,
翻转部分数组:
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int a[5] = {11,22,33,44,55};
reverse(a+3,a+5);
for(int i = 0; i < 5; i++)
cout << a[i] << ' ';
return 0;
}
3、翻转容器:
翻转整个容器:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main() {
vector<int>v;
//输入:
for(int i = 0; i < 5; i++)
v.push_back(i);
//输出:
reverse(v.begin(), v.end());
for(int i = 0; i < v.size(); i++) {
cout << v[i] << ' ';
}
return 0;
}
(4)排序函数:sort()
1、对x-y区间的数组、容器进行排序。默认升序排列。
数组升序排序:
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int a[5] = {55,44,33,22,11};
sort(a,a+5);
for(int i = 0; i < 5; i++)
cout << a[i] << ' ';
return 0;
}
2.数组降序排序,写一个简单的函数,改变排序功能:
数组降序排序:
#include<iostream>
#include<algorithm>
//意思是:若a>b,则a的优先级更大! 也就是说大的在前面。
bool cmp(int a, int b) {
return a > b;
}
using namespace std;
int main() {
int a[5] = {55,44,33,22,11};
sort(a,a+5,cmp); //这里需要加上自己自定义的函数
for(int i = 0; i < 5; i++)
cout << a[i] << ' ';
return 0;
}
3、同理,如果想对结构体排序,也需要自定义优先级。像这样:
结构体排序:
#include<iostream>
#include<algorithm>
using namespace std;
//用sort函数对结构体排序
struct Student {
int high;
int weigh;
}student[10];
//a.high如果小于b.high,则a结构体的优先级更大, 也就是说:high小的结构体排在前面。
bool cmp(Student a, Student b) {
return a.high < b.high;
}
int main() {
for(int i = 0; i < 10; i++) {
student[i].high = i ;
}
sort(student, student+10, cmp); //将自定义的函数添加上。
for(int i = 0; i < 10; i++) {
cout << student[i].high << ' ';
}
return 0;
}
4、对容器排序:
容器升序排序:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int main() {
vector<int>v;
vector<int>::iterator it;
//输入:
for(int i = 5; i > 0; i--)
v.push_back(i);
//输出:
it = v.begin();
sort(it, it+3);
// sort(v.begin(), v.end())
for(int i = 0; i < v.size(); i++) {
cout << v[i] << ' ';
}
return 0;
}
(5)查找函数:find()
查找某数组指定区间x-y内是否有x,若有,则返回该位置的地址,若没有,则返回该数组第n+1个值的地址。(好烦有木有,为啥要返回地址。还要转化o(╥﹏╥)o)
1、数组中查找是否有某值:一定一定一定要满足代码中这两个条件。
第一个条件是:p-a != 数组的长度。p是查找数值的地址,a是a[0]的地址。
第二个条件是:*p == x; 也就是该地址指向的值等于我们要查找的值。
最后输出p-a+1; p-a相当于x所在位置的地址-a[0]所在位置的地址, 但因为是从0开始算, 所以最后需要+1。
对数组查找:
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int a[5] = {11,22,33,44,55}
int *p = find(a,a+5,33); //定义指针,指向查找完成后返回的地址,5为a2数组长度
if(((p-a) != 5) && (*p == x)) //若同时满足这两个条件,则查找成功,输出
cout << (p-a+1); //输出所在位置
return 0;
}
(6)查找某值出现的次数:count()
在数组中查找x 在某区间出现的次数:
代码:
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int a[5] = {11,22,33,44,44};
cout << count(a, a+5, 44);
return 0;
}
(7)求最大公因数:__gcd()
震惊把!在我最开始知道竟然有这个函数时,我也是震惊的!
另外,用二者乘积除以最大公因数即可得到最小公倍数。 因此没有求最小公倍数的函数。
代码:
#include<iostream>
#include<algorithm>
using namespace std;
int main() {
int a = 12, b = 4;
int Gcd = __gcd(a,b);
cout << Gcd;
return 0;
}
6.容器长度
//字符串string
string a="cswedcfv";
cout<<a.size();
//字符串数组char a1[]
//对于字符串数组,使用strlen()函数来获取字符串数组的长度。
char a1[] = "fwegweasdasdwqew";
cout << strlen(a1) << endl;
//数字数组int a[]
int arr[] = { 1,5,9,10,9,2 };
int n=sizeof(arr) / sizeof(arr[0]);
7.cmath库
(1)绝对值函数
浮点数:
fabs(double x);
整型:
abs(x);
效果与上方if同等。
(2)幂计算:
开方
√a
sqrt(double x);
乘方
pow(a,b);
表示b个a相乘。
(3)三角函数:
hypot(double x,double y);
x、y为直角三角形两条直角边,它可以求出第三条边。
应用
#include<iostream>
#include<cmath>
using namespace std;
int main(){
int x,y;
cin>>x>>y;
cout<<hypot(x,y);
return 0;
}
(4)取整:
double ceil(double x);//取上整,返回比x大的最小整数
double floor(double x);//取下整,返回比x小的最大整数,即高斯函数[x]
double round(double x); //返回x的四舍五入值
(5)实践
#include<iostream>
#include<cmath>
//1.绝对值
//2.乘方
//3.取整
using namespace std;
int main(){
float a=-5.63;
cout<<fabs(a);//1.绝对值
cout<<"\n";
float b=2.44;
cout<<sqrt(b);//2.乘方
cout<<"\n";
cout<<pow(a,2);
cout<<"\n";
cout<<ceil(a);//3.取整
cout<<"\n";
cout<<floor(a);
cout<<"\n";
cout<<round(a);
return 0;
}
运行结果:
5.63
1.56205
31.6969
-5
-6
-6
8.栈和队列
使用标准库的栈和队列时,先包含相关的头文件
#include
#include
定义栈如下:
stack stk;
定义队列如下:
queue q;
栈提供了如下的操作
s.empty() //如果栈为空返回true,否则返回false
s.size() //返回栈中元素的个数
s.pop() //删除栈顶元素但不返回其值
s.top() //返回栈顶的元素,但不删除该元素
s.push() //在栈顶压入新元素
队列提供了下面的操作
q.empty() //如果队列为空返回true,否则返回false
q.size() //返回队列中元素的个数
q.pop() //删除队列首元素但不返回其值
q.front() //返回队首元素的值,但不删除该元素
q.push() //在队尾压入新元素
q.back() //返回队列尾元素的值,但不删除该元素
——————————————————————————————————
持续更新中(2024.7.13)