C++技巧
卡常小技巧:
数组长度为奇数,循环展开,多维数组越小放前面,提高访址连续性,非递归函数加 inline
cin语句的使用 :
在使用 cin 输入的时候,系统会根据变量的类型从输入流中提取相应长度的字节
- cin语句把 空格,制表,换行,回车 作为分隔符,不输入给变量
- cin语句忽略多余的输入数据,要组织好输入流数据
这种做法得看数据的空白字符吧,可以用更普适的方法。
char get() { unsigned char c=getchar(); while(c<=32)c=getchar(); return c; }
或者是用 scanf(" %c",&ch);
,读入下一个非空白字符。
字符串题大全:
https://www.maxuetang.cn/submit/7/9594/
https://blog.csdn.net/wei2019_/article/details/112756741
https://www.acwing.com/problem/content/761-769/
全局变量、局部变量的作用域
在函数外部定义的变量称作全局变量,在函数内部定义的称为局部变量。
全局变量作用域是从定义位置开始到文件结束,可在文件中位于定义变量后的所有函数中使用。
过多的使用全局变量,会增加调试难度和程序的通用性,代码移植过程中可能会出现重名的问题
全局变量在程序运行过程中一直占用内存单元,若没赋初值,则默认为 0。
局部变量作用域是在定义该变量的函数内部。函数的形参也是局部变量,函数执行完毕时,局部变量的空间就会被释放。
不同函数中的局部变量名可以相同。一个全局变量和局部变量的也是可以重名的,在相同作用域时局部变量有效时全局变量无效,即局部变量可以屏蔽全局变量。
在代码块中定义的变量的存在时间和作用将被限制在该代码块中,如for循环的i
局部变量的初始值是随机的,且受栈空间大小限制,大数组需要注意。全局变量随便。
结构体
- 结构类型和结构变量是两个不同的概念
- 结构类型中的成员名,可以与程序中的变量重名,它们代表不同给的对象,互不干扰
- 结构类型的数据项,既可以是基本数据类型,也可以是结构体类型
//typedef 类型名 别名 typedef long long ll; struct student{ int name,age; struct teacher{ int age; }th; }stu; typedef struct student{//typdef为数据类型起一个别名 int name,age; }stu;//此时stu是一个数据类型 //结构体指针 //指向结构变量的指针来访问结构变量的成员,与直接使用结构变量得效果一样 struct student{ int name,age; student(int _name,int _age){ name=_name;age=_age; } student(){} }*p; p->name;p->age; (*p).name,(*p).age
引用
引用可以看做是 C++ 封装过的指针,用来传递它所指向的对象。引用的基本原则是在声明时必须指向对象,以及对引用的一切操作都可以看成对原对象的操作。引用不是对象。
void swap(int &x,int &y){ int t=x;x=y;y=t; } swap(a,b);
上述代码中,我们在调用函数的时候需要改变实参的值,则需要采用 “传引用” 的方式
我们正在 int
后面加了一个 &
,表示对于 int
的 引用。在调用函数 swap
时,调用处的 a
和 b
变量分别初始化了 swap
中两个对 int
类型的引用 x
和 y
,可以理解为 x
和 y
分别为 a
和 b
的别名,即 swap
中对 x
和 y
的操作就是对 a
和 b
的操作 。
指针
指针定义:类型说明符 *指针变量名
,类型说明符表示指针所指向变量的类型,指针实际上是指向一块内存空间
指针赋值:int *p=&a;
代表将p
指向a
的地址
指针间接操作:要访问指针所指向的空间,需要对变量进行解引用,*p=20
,即指向地址变量的值
指针的初始化:
int *p=NULL; int *p=new int; int *p=new int(1234);//向系统申请可一块 int 大小的空间,值初始化为 1234
指针的运算:
指针变量的内容是内存地址,+,-
操作对应指针的移动,也称为指针的偏移,例:
int *p=&a[1];cout<<*(p+1);
这里的加1是广义的,并不是指向的地址加1,根据一个int
4
个字节,其实是跳过”一个整数空间,到达下一个整数
无类型指针:
定义:void *p; p=&a; cout<<*(int*)p; p=&b; cout<<*(double*)p;
注:用的时候必须明确 p
指向空间的数据类型,需要强制类型转换
指针与数组:
数组在内存空间里是一段连续的地址,数组名就是该数组首地址,即 a=&a[0]
,所以 a
也是一种指针,
a+i=&a[i] int a[5],*p; p=a; cout<<*(p+2);
指针也可以看成数组名,动态数组
int *a=new int[n+1]; (之后可以当成一个长度为n+1的数组用) delete[] a;// 释放空间 cin>>a[i];a[i]+=a[i-1];cout<<a[i]
二维数组:
二维数组是数组的数组。而计算机内存可视为一个很长的一维数组。
连续:即二维数组的任意一行的末尾与下一行的起始,在物理地址上是相邻的。换句话说,整个二维数组可视为一个一维数组;反之,则二者在物理地址上不一定相邻。
对于连续的二维数组,只需要一层循环、一个不断递增指针可遍历数组中的所有元素。而对于不连续的,则需要不断访问每一行的首地址,再不断访问这一行的元素。注:存储方式可分为行优先存储和列优先存储。
指针与字符串:
字符指针:
char str[]="fuck"=char *str="fuck";printf("%s",str);
C++
对字符串常量当成字符数组处理,字符串的名字对应首地址,指向了第一个字符的内存空间
字符串指针可做函数参数,用字符数组名或用指向字符的指针变量做参数是等价的,实参可以被改变
指针与函数:
调用函数时使用的参数,均以拷贝的形式传入。函数仅仅能通过返回值,将结果返回到调用处。但是如果某个函数希望修改外部的数据,或者某个结构体数据的量过于庞大,不宜进行拷贝,这时可以传入外部数据的地址,达到在函数内部访问或修改的目的。引用
指针可做为函数参数,传指针,而并非简单的赋值
void swap(int *x,int *y){int t=*x;*x=*y;*y=t;} swap(&a,&b);
函数返回指针:
int *solve(int a,int b)
调用后可得到一个指向int
类型的指针,注意()
的优先级高于*
,所以a
先于()
结合
函数指针:
指针也可以指向一个函数,函数名就是该函数的指针。返回类型,形参格式必须相同。
int (*hs)(int a,int b),hs=swap;
之后hs
可当成函数使用
结构体指针:
上面有
自引用结构:
struct Node{ int l,r; Node *p; }node;
在动态数据结构中有重要作用
递归栈
- 将调用程序的返回地址、相应的调用前的变量都保存在系统堆栈中;
- 执行被调用的函数;
- 若满足退出递归的条件,则退出递归,并从栈顶上弹回返回地址、取回保存起来的变量值,延着返回地址,向下执行程序
- 否则继续递归调用,只是递归调用的参数发生变化;增加一个量减少一个量,执行直到递归结束
自动类型转换
在混合运算中,会隐式的进行数据类型转换
自动类型转换遵循以下规则:
- 若参与运算的数据类型不同,则先转换为同一类型,然后进行运算
- 转换按数据长度增加的方向进行,以保证精度不降低,即当参加算术或比较的两个操作数类型不统一时,将简单类型向复杂类型转换,char -> int ->float -> double
- 在赋值运算中,赋值号两边的数据类型不相同时,将把右边表达式的类型转换为左边表达式的类型,易丢失数据
- 在赋值语句中,两边的数据类型一定是相兼容的,否则会报错
混合运算时的类型转换规则
char,short->int-->unsigned int -->long long -->double <-float
短箭头指必定会进行的转换,
长箭头指运算对象为不同数据类型时的转换方向,例如 int 与 unsigned 型做运算, int 型会转换成 unsigned 型
赋值时的类型转换规则
赋值时,若等号两边的数据类型不一样,则需要发生类型转换,遵循最终右边的数据类型与左边一致。
当左边的精度大于右边时,提升右边精度,不影响答案。
否则:
- int = double,截断小数点保留整数位。
- int = long long,截断二进制高位,保留低位。
- short = unsigned int,一般情况下,可以认为保留为 的结果
- int = unsigned int ,由于二进制位数相同,将会保留 意义下范围内的数
- unsignded int = int,将会保留 的结果
强制类型转换
只是临时转换
(类型名)变量, (类型名)(表达式)
字符串
用cin输入字符串时,空格和换行符都被认为是字符串的结束
两个字符串常量不能用加号连接,其中一个必须是字符串变量
string可以做到字符串间的 复制,连接,比较,倒置,获取长度,查找,输入,删除,截取
string有很多成员函数,默认比较为比较字典序,即不考虑长度
getline默认碰到换行符才结束,getline(cin,s)
注意,对于输入格式
3 5 abc deiide fef
string s;int a,b;cin>>a>>b; getline(cin,s); cout<<s;
s为空串,因为有换行符
- size()
- substr(开始位置,长度)
- find(字符串):查找到就返回第一个出现S的位置,否则返回string::npos
- replace(开始位置,长度,要换上的符串)
- erase(开始位置,长度)
- insert(开始位置,字符串)
二进制从低位开始,每四位对应十六进制的一位
灵活运用stringstream流,比如将一个浮点数转化为一个字符串,可以对各种类型进行相互转换
迭代器
在 STL 中 ,迭代器用来访问和检查 STL 容器中元素的对象,与指针类似但不等价。
支持两个运算符:自增 ++
和 解引用 *
,自增实现迭代器的移动,解引用可以获取和修改容器中它指向的元素。
输入迭代器:只要求支持拷贝、自增、解引访问。
输入迭代器:只要求支持拷贝、自增、解引赋值。
向前迭代器:满足输入和输出迭代器的要求。
双向迭代器:在向前的基础上支持自减。
随机访问迭代器:在双向的基础上支持加减运算和比较运算(即随机访问)。
不同的 STL 容器的迭代器类型不同,指针是一种随机访问迭代器。迭代器的类型之间并不互斥,比如要求使用向前时也可以使用双向。
advance(it,n)
将 it
向后移动 n
步,若 n
为负数,则向前移动,必须满足是双向迭代器,否则是未定义行为。
next(it)
获得向前迭代器 it
的后继(此时 it
不变),next(i,n)
获得 it
的第 n
个后继
prev(it)
获得双向迭代器 it
的前驱(此时 it
不变),prev(i,n)
获得 it
的第 n
个前驱
begin()
:指向容器第一个元素的迭代器
rbegin()
:指向容器最后一个元素的迭代器
end()
:指向容器尾端的迭代器
序列式容器
序列式容器包括:vector,array,deque,list,forward_list
vector
内存连续,可变长度的数组,线性复杂度的插入和删除
支持动态分配内存
当我们不能提前开好足够大的空间时(比如处理 1~n 的约数),尽管我们知道数据总量在空间允许的级别,但是单份数据量可能非常大,这时候我们就需要 vector 把内存占用量控制在合适范围之内。vector
还支持动态扩容,内存紧张时可排上用处。
重载了比较运算符及赋值运算符
vector
重载了比较运算符,并以字典序为关键字。所以比较两个容器是否相等是可行的(复杂度与容器大小成线性关系),且我们可以拷贝容器。
初始化/构造函数
vector<int> v
大小为 0 的空vector,保证常数时间的复杂度
vector<int> v(N,i)
初始化为一个大小为 的容器,每个元素的值为 。特殊地将 i 去掉后每个元素的值为 0.,与 N 的大小成线性关系
vector<int> v{1,2,3}
列表初始化
vector<int> v(move(vv))
移动 vv 到新创建的 v,不发生拷贝,常数复杂度
元素访问
front()
首元素的引用
back()
尾元素的引用
迭代器
begin()
指向首元素的迭代器
end()
指向数组尾端没有占位符的迭代器
rbegin()
指向尾元素的迭代器
与长度相关
size()
容器长度
empty()
判断容器是否为空
resize(n,m)
将元素个数调整为 n ,多退少补,补的元素初始化为 m
元素插删及修改
clear()
清空容器
insert(it,x)
支持在一个迭代器后面插入元素,复杂度与 pos 距末尾长度是线性的
erase(it)
支持删除某个迭代器,复杂度与 insert
一样
push_back(x)
在尾端插入元素,复杂度均摊为常数
pop_back()
删除末尾元素,常数
swap(v1,v2)
将两个容器进行交换,常数
deque
双端队列,支持线性复杂度的插入和删除,以及常数复杂度的随机访问。
初始化/构造函数
deque<int> v
空双端队列
deque<int> v(n,m)
设置初始大小为 n ,且每个值为m,线性复杂度
元素访问
front()
首元素的引用
back()
尾元素的引用
迭代器
与 vector 一致
与长度相关
与 vector 一致
元素插删及修改
clear()
清空所有元素
insert(it,x)
支持在一个迭代器后面插入元素,复杂度与 pos 距两端距离较小者长度是线性的
erase(it)
支持删除某个迭代器,复杂度与 insert
一样
push_front(x)
在头部插入一个元素,常数
pop_front()
删除头部元素,常数
push_back(x)
在末尾插入元素,常数
pop_back()
删除末尾元素,常数
swap(v1,v2)
将两个容器进行交换,常数
关联式容器:set,map,multiset,multimap
list
双向链表
初始化/构造函数
list<int> a;
int arr[5]={1,2,3};list<int> a(arr,arr+3)
从 arr 中的前 3 个元素作为链表的初始值
list<int>::iterator it
定义一个为 it 的迭代器(指针),是双向迭代器
成员函数
size()
返回链表的结点数量
begin()
链首指针
end()
链尾后一个的迭代器
insert(it,x)
在 it 前插入元素 x
erase(it)
删除迭代器 it
push_front(x)
在链首插入一个值为 x 的元素
push_back(x)
在链尾插入一个值为 x 的元素
pop_front()
删除链首元素
pop_back()
删除链尾元素
set
插入与删除
insert(x)
当set中不含有等价元素时,将 x 插入到 set 中
erase(x)
删除值为 x 的 所有 元素
erase(it)
删除迭代器为 it 的元素
erase(first,last)
删除迭代器在 范围内的所有元素
clear
清空 set
迭代器
begin()
指向首元素的迭代器
end()
指向集合尾端没有占位符的迭代器
rbegin()
指向尾元素的迭代器
查找操作
count(x)
返回 x 的元素个数
find(x)
若存在 x 则返回 x 的迭代器,否则返回 end()
lower_bound(x)
返回首个大于等于 x 的迭代器。否则返回 end()
upper_bound(x)
返回首个大于 x 的迭代器。否则返回 end()
empty()
判断容器是否非空
size()
返回容器的元素个数
-
关于lower_bound和upper_bound()的时间复杂度
如果用 set 自带的复杂度为 ,用
<algorithm>
库的复杂度为 -
关于nth_element 的时间复杂度
set 不自带,所以用
<algorithm>
里的复杂度为
set 的应用
在贪心问题中,经常遇到类似 找出并删除最小的满足大于等于 x 的元素
map
map 是 有序键值对 的容器,元素键是唯一的。搜索,插入和删除具有 log 的复杂度
可以存储一些键值对(比如:<Alice,0>
,<Bob,1>
),但数组下标只能是非负整数。
map 重载了 operator,可以用任意定义了 operator<
的类型作为下标(key
,索引 )
初始化
map<key,val> mp;
,其中 key
是键值的类型,val
是值的类型
map
不会存在同一键值的元素,但 multimap
存在不同元素拥有同一键值
插入与删除
mp["Alice"]=233
通过下标访问来进行查询和插入操作
mp.insert(pair<string,int>("Bob",114514))
通过向 map 里插入一个类型为 pair<key,val>
的值可达到插入元素的目的
erase(key)
删除键值为 key 的所有元素
erase(it)
删除迭代器为 it 的元素
erase(first,last)
删除迭代器在 范围内的所有元素
clear
清空 map
查询
count(x)
返回容器内键值为 x 的元素个数,复杂度为
find(x)
若存在键值为 x 的元素,则返回该元素的迭代器,否则 end
lower_bound(x)
返回首个大于等于键值 x 的迭代器
upper_bound(x)
返回首个大于键值 x 的迭代器
empty()
返回容器是否为空
size()
返回容器内元素个数
map 应用
map 可用于存储复杂状态,在搜索中,我们常常需要存储一些复杂状态(坐标,无法离散化的数值,字符串)以及关于它们的答案。map 可以做法,其中 key 是状态,val 是答案
遍历容器
set<int> s; typedef set<int>::iterator si for(si it=s.begin();it!=s.end();it++){ cout<<*it<<'\n'; } for(auto x:s){ cout<<x<<'\n'; }
需要注意的是,对 map 进行解引用的时候,得到的是类型为 pair<key,val>
的键值对
自定义比较方式
set 默认情况下比较函数为 <
,我们可以自定义 set
内部的比较方式,这时可以传入 自定义比较器
我们需要一个类,并在这个类中重载 ()
struct cmp{ bool operator()(int a,int b){return a>b;} } set<int,cmp> s;
容器适配器:stack,queue
stack
top()
访问栈顶元素(如果队栈为空,此处会出错)
push(x)
将 stack 中放入 x
pop()
删除栈顶元素
size()
查询容器中的元素个数
empty()
询问容器是否为空
这些成员函数均为 常数复杂度
queue
front()
访问队首元素(如果队列为空,此处会出错)
push(x)
将 x 放入队尾
pop()
弹出队首元素
size()
查询容器中的元素个数
empty()
询问容器是否为空
这些成员函数均为 常数复杂度
priority_queue
初始化/定义
priority_queue<TypeName> q
定义一个数据类型为 TypeName 的大根堆 q
priority_queue<TypeName,Container,Compare> q
使用 container 为底层容器,Compare 为比较类型
以下为 常数 复杂度
top()
访问堆顶元素(此时优先队列不能为空)
size()
查询容器中的元素个数
empty()
询问容器是否为空
以下为 log 复杂度
push(x)
将 stack 中放入 x
pop()
删除栈顶元素
bitset
bitset 是一个 01 串,每位占 1 bit ,可支持 单点 0/1 修改,位运算等操作。一个很好的用处是统计每个数是否出现过,类似一个桶。同时也可以获得两个集合是否有重复元素的信息。
初始化与声明
bitset<N> s
其中 N 为 s 的长度,默认全部为 0
string t="00110101";bitset<N> s(string("00110101"))
支持用同长度的 01 串进行赋值,这时把字符串当成了一个二进制数,赋值到bitset里对应的每一位上
由于 bitset 的字长放在 RAM 模型中,所以时间大部分为 ,空间为
修改与运算
bitset 中的每个元素可以通过下标来访问,一个长度为 N 的bitset 编号范围为 [0,N)
单点修改:s[pos]=1
O(1)
左移/右移:s=s<<x;
与,或,异或:s=s1&s2
输入输出与转化
可以直接用 cin,cout 输入/输出一个bitset 的所有元素
cin>>s;//1101 cout<<s;//00001101
注意到,输入与初始化是类似的。
bitset 提供的转换函数:to_string(), to_ulong, to ullong()
,即转换成 string,unsigned int,unsigned long long
,注意当 bitset 的位数超过目标类型时,会导致 RE
成员函数
reset()
将 bitset 的所有元素置成 0,n/w
set()// 将所有元素赋值为 1, n/w set(pos)//将 pos 位置的数赋值为 1, O(1) set(pos,v)//将 pos 位置的数赋值为 v, O(1)
test(pos)
返回 pos 位置的值,O(1)
any()
返回整个bitset 是否存在 1, n/w
none()
返回整个 bitset 是否不存在 1 ,即全为 0, n/w
count(x)
返回 bitset 中值为 x 的元素个数,n/w
bitset& flip(); bitset& flip (size_t pos);//两种声明 filp()//将所有元素按位取反,n/w filp(pos)//将pos按位取反, O(1)
技巧性函数
注:所有的传参规则(首位置,末位置+1)
-
upper_bound,lower_bound,目标序列具有有序性
lower_bound(a+begin,a+end,val)
,返回的是迭代器在数组中复杂度为 ,但在
set
的关联式容器将会退化成 ,直接用它们内部封装的 lower 和upper 就行了 -
deque 可以 sort ,sort 不单纯是快排,
-
partial_sort(first,middle,last)
http://m.biancheng.net/view/7469.html
将前 范围内最小/大的元素放在 里,并按升序/降序排序
对一个长为 的数组,排出前 小个来,比如
partial_sort(a+begin,a+begin+mid,a+end) vector<int> a; D(i,10,1)a.pb(i); partial_sort(a.begin(),a.begin()+3,a.end()); //partial_sort(...,...,...,greater<int>()); for(auto it:a)cout<<it<<" "; 1 2 3 10 9 8 7 6 5 4 平均复杂度为
-
nth_element(first,nth,last)
找到范围 内第 小的数 ,并把 放到第 个位置,保证 之前的数都比 小,之后的都比 大。
nth_element(a+begin,a+begin+mid,a+end,cmp);
-
rbegin,back
vector<int> a; F(i,1,10)a.pb(i); cout<<a.front()<<'\n'; cout<<a.back()<<'\n'; cout<<*a.begin()<<'\n'; cout<<*a.rbegin()<<'\n'; set<int> b; F(i,1,10)b.insert(i); cout<<*b.begin()<<'\n'; cout<<*b.rbegin()<<'\n'; -
min, max
min(a,min(b,c)) <=> min({a,b,c}) (C++11) -
max_element, min_element
查找最大值/最小值对应的地址
int a[]={1,4,3,2}; cout<<max_element(a,a+4)-a; -
prev_permutation, next_permutation
next 将当前排列更改为 下一个排列,返回 1;若当前排列已经是最后一个排列,返回 0,并将排列更改为 全排列中的第一个排列
do{ }while(next_permutation(a+1,a+n+1)); next_permutation(a+begin,a+end) 全遍历 ,单次均摊
-
__int128
128位整数
输入输出要手写快读快写(注意0的特判
如果有些题中间过程爆了
long long
,可以强制转换成__int128
处理 -
to_string
将数字按位转化成字符串
-
emplace_back
写了构造函数这样减小常数
struct TSY{ int age; TSY(int x){ age=x; } }; vector<TSY> v; v.emplace_back(21); v.push_back(TSY{21}); -
__gcd
-
__lg
在二进制下数的最高位的位数,如
12=(1100),__lg(12)=3
,由此可以计算二进制下的位数注意
—__lg(0)
会出现问题!结果是未定义的,挺快的
-
rotate
-
reverse
写字符串常用
reverse(s.begin(),s.end());
-
__builtin_popcount()
快,统计二进制下有多少个位为1
-
shuffle
srand(time(NULL)); random_shuffle(a+1,a+n+1); 复杂度
-
memset, fill
memset是将某一块内存的内容全部设置为以指定的值按字节为单位 进行填充的
所以用 fill
memset(a,0x3f,sizeof(a));
,1e9 左右fill(a,a+n+1,1)
-
iota
对 赋一段连续且递增的值
iota(a,a+n,0)//0 1 2 3 4 5..
-
next, prev
移动迭代器,大部分容器为
auto it=s.begin(); auto newit=next(it,..) -
isalpha, iaalnum, isdigit
判断字符是否为 字母,字母或数字,数字
-
random 库
-
bit 库
-
bitset
-
pb_ds
STL排序函数的运算符必须满足严格弱序
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】