篇章一:Cpp语法基础
一. HelloWorld与输入输出
# include <iostream>
using namespace std;
int main()
{
cout << "gyx" ; //gyx
//基本输入输出
cin >> a >> b;
cout << a + b << endl; // a + b
//输出多个值
cout << a + b << ' ' << a * b << endl; // (a + b) (空格) (a * b)
return 0;
//printf和scanf
int n1;
float f1;
scanf_s("%d %f", &n1, &f1);
printf("%f_%f", n1 + f1, n1 * f1);
//字符的输入输出
char a, b;
//cin >> a >> b; cin输入不会读取空格
scanf_s("%c%c", &a,1, &b,1);// 5 6
printf("%c %c \n", a , b); //5(空格) (解释,空格也会被当作字符读取)
return 0;
//printf规定有效数字输出格式
double pi = 3.14159261535;
int Π = 3;
printf("%10lf!\n", pi); // (空格x2)3.141592!(总输出位数在为10)
printf("%.3lf!\n", pi); // 3.142! (会四舍五入)
printf("%015d!\n", Π); // 000000000000003!(总位数15,不够补0)
}
二.变量
cout << "HelloWorld" << endl; //会自动补一个回车
//布尔类型
bool b1 = true;
bool b2 = false;
cout << b1; // 0
cout << '\t'; //制表符
cout << b2; // 1
//字符型
char c1 = '\n';
char c2 = 'g';
cout << c1; //空格
cout << c2; //g
//整形
cout << c1;
int n1 = 2147483648;
int n2 = -2147483637;
cout << n1; // 2147483648
cout << '\t';
cout << n2; // 2147483647
//浮点型
cout << c1;
float f1 = 1.27;
float f2 = 3.1415926e2;
cout << f1; // 1.27
cout << '\t';
cout << f2; // 314.159(六位有效数字)
cout << c1;
double d1 = 3.1415926;
double d2 = 3.1415926e5;
double d3 = 3.157946837910833;
cout << d1; // 3.14159
cout << '\t';
cout << d2; // 314159
cout << '\t';
cout << d3; // 3.15795
//长整形(长浮点)
cout << c1;
long long ll = 1234567891011121LL;
long double ld = 123.45;
cout << ll; //1234567891011121
cout << '\t';
cout << ld; // 123.45
三. 条件 & 循环语句
条件涉及关键字:if ,else ,switch,case,break
循环涉及关键字:while ,break , continue, for, do while
案例1:斐波那契数列算法、
//斐波那契数列算法
int a = 1, b = 1; //f(1) & f(2)
int n , i = 0;
cin >> n;
while (i < n - 2) {
int c = a + b;
a = b;
b = c;
++i;
}
cout << b << endl;
案例2:输出指定形状的图形
/*
*
***
*****
***
*
*/
int n;
cin >> n;
int center = (n - 1) / 2;
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (abs(i - center) + abs(j - center) <= center) {
cout << '*';
}
else {
cout << ' ';
}
}
cout << endl;
}
案例三:在遇到读入数据个数未知时,可以使用以下方式来输入
while (cin >> x) {}
while (scanf("%d",&x) != -1){}
while (~scanf("%d",&x)) //按位取反
//如果读取到最后一个数字为0的情况结束,按照以下写法
while (cin >> x && x){}
while (cin >> x , x){} //,表达式最终值为后数(x),仅作用于逻辑表达式
四. 数组
程序 = 逻辑 + 数据。cpp中数组是存储数据的强有力手段。
//数组的定义
int arr[10];
//数组的初始化
int arr1[] = {0 ,1 ,3};
int arr2[5] = {0,1,3}; // 定义了了一个长度是5的数组,没有给出的值默认为0
int arr3[20] = {0}; //将数组全部初始化为0(常用)
全局变量的初始值默认为0。
局部变量的初始值随机。
故,当使用局部变量时,需要先对其进行初始化为0。且局部变量会存储在栈空间中,栈空间容易造成溢出。建议多使用全局变量定义数组,将数组变量存在堆内存中,防止内存溢出。
案例一:斐波那契数列的另一种做法
#include <iostream>
using namespace std;
int arr[100010];
int main()
{
//斐波那契数列:1,1,2,3,5
arr[0] = 0; arr[1] = 1;
int k;
cin >> k;
for (int i = 2; i <= k; ++i) arr[i] = arr[i - 1] + arr[i - 2];
for (int i = 1;i <= k;++i) cout << arr[i] << endl;
return 0;
}
案例二:将一个数组旋转k次之后的新数组
Example:将{5,4,3,2,1}旋转3次得到{3,2,1,5,4}。
方法一:常规做法
#include <iostream>
using namespace std;
int arr[100010];
int main()
{
//将一个数组旋转k次得到的新数组
int n , k;
cin >> n >> k;
for (int i = 0; i < n; ++i) cin >> arr[i];
while (k--) {
int temp = arr[n - 1];
for (int i = n - 1; i > 0; --i)
arr[i] = arr[i - 1];
arr[0] = temp;
}
for (int i = 0; i < n; ++i) cout << arr[i] << ' ';
return 0;
}
方法二:数组反转
#include <iostream>
using namespace std;
int arr[100010];
int main()
{
//将一个数组旋转k次得到的新数组
int n , k;
cin >> n >> k;
for (int i = 0; i < n; ++i) cin >> arr[i];
//做法二:
reverse(arr, arr + n); //反转整个数组
reverse(arr, arr + k); //反转前半部分数组
reverse(arr + k, arr + n); //反转后半部分数组
for (int i = 0; i < n; i++) cout << arr[i] << ' ';
return 0;
}
案例三:输出蛇形排列数组
/*
1 2 3
8 9 4
7 6 5
*/
#include <iostream>
using namespace std;
int matrix[110][110];
int main()
{
//蛇形矩阵模拟
int w, h;
int k = 1;
cin >> w >> h;
int top = 0, bot = h - 1, left = 0, right = w - 1;
while (top <= bot && left <= right) {
for (int i = left; i <= right; ++i) matrix[top][i] = k++;
top++;
for (int i = top; i <= bot; ++i) matrix[i][right] = k++;
right--;
for (int i = right; i >= left; --i) matrix[bot][i] = k++;
bot--;
for (int i = bot; i >= top; --i) matrix[i][left] = k++;
left++;
}
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
cout << matrix[i][j] << ' ';
}
cout << endl;
}
return 0;
}
有关数组的API(重要)
//数组初始化
# include<iostream>
# include<cstring>
using namespace std;
int main(){
int a[10],b[10];
//给数组a中的的长度为a数组的每一位的字节byte赋值为1(注意不是值赋值为1)
//memset效率比循环赋值快2~3倍
memset(a,0,sizeof a);
//比如下例:赋值出来是16843009,经过程序员计算器转换得0000 0001 0000 0001 0000 0001 0000 0001
memset(a,1,sizeof a);
//将原数组元素赋值到新数组
memcpy(b, a, sizeof a);
}
五.字符串
5.1 基础知识
常见的ASCII码对应关系:
- Space : 32
- A : 65
- a : 97
- 0 : 48
字符串 实际上就是 字符数组 + 结束符号\0
可以用字符串来初始化字符数组,但此时要注意,每个字符串结尾会暗含一个 ‘\0’,所以字符数组长度至少比字符串长度多1。
//字符串的区分与初始化
char a1[] = {'c','+','+'};//不是字符串,没有\0,长度为3
char a2[] = {'c','+','+','\0'};//是字符串,长度为4
char a3[] = "c++";//与上等价,长度为4
重点:在使用scanf读入字符串时,无需加 & 符号,由于字符串本质上是字符数组,其变量名本身为一枚指针,字符串在读入的时候,当遇到空格,回车或者读取结束会自动停止。
char s[100];
scanf("%s", s);
//cin >> s ;
cout << s << endl;
//由于string本质是字符数组,字符数组变量又是其首位内存地址所以存在以下用法
//从第5位开始存数据
cin >> s + 5;
cout << s + 5 << endl;
如果要读入一整行数据,无法使用cin,cin遇到空格会停止读入。读入一行数据可以用以下方法:
# include<string>
int main(){
string s;
getline(cin,s);
char str[110];
cin.getline(str);
}
一些常用的API:
#include<cstring>
#include<iostream>
using namespace std;
int main(){
char s[100],s2[100];
char s1[] = "string";
cin >> s;
cout << s << endl;
//输出字符串s的长度
cout << strlen(s) << endl;
//输出字符串s和s1是否相等
cout << strcmp(s, s1) << endl;
strcpy_s(s2, s);
cout << "s2 = " << s2 << endl;
}
案例一:只出现一次的字符
给你一个只包含小写字母的字符串。请你判断是否存在只在字符串中出现过一次的字符。如果存在,则输出满足条件的字符中位置最靠前的那个。如果没有,输出 no
。
#include <iostream>
#include <cstring>
#include<cstdio>
#include<string>
using namespace std;
#define N 100010
char s[N];
int cnt[26];
int main()
{
//只出现一次的字符
cin >> s;
int slen = strlen(s);
for (int i = 0; i < slen; i++) cnt[s[i] - 'a']++;
for (int i = 0; i < slen; i++)
if (cnt[s[i] - 'a'] == 1) {
cout << s[i] << endl;
return 0;
}
puts("no");
return 0;
}
string类型详解
//字符串的定义
string s1; //默认的空字符串
string s2 = s1; //s2是s1的一个副本
string s4(10,'c'); //s4的内容:cccccccccc
//如果要读入一整行数据
cin >> s1; //遇到空格回车会停止读入
getline(cin,s1); //读入一整行数据
string类型常用API汇总:
string s1;
//字符串的长度
s1.size();
//字符串是否为空
s1.empty();
//字符串的拼接(拼接运算符两边至少要有一个string类型变量)
cout << s1 + 'safjlk' << endl;
cout << "gyx" + "666" << endl; //报错
字符串的遍历
string s = "Hello World";
//方法一:将其当成字符数组
for (int i = 0; i < s1.size(); i++) cout << s1[i] << ' ';
cout << endl;
//方法二:使用foreach循环
for (char c : s1) cout << c << ' ';
案例二:在字符串指定位置插入字符串
有两个不包含空白字符的字符串 strstr 和 substrsubstr,strstr 的字符个数不超过 1010,substrsubstr 的字符个数为 33。(字符个数不包括字符串结尾处的 \0
。)将 substrsubstr 插入到 strstr 中 ASCII 码最大的那个字符后面,若有多个最大则只考虑第一个。
# include <iostream>
# include <cstring>
using namespace std;
int main(){
string a,b;
while (cin >> a >> b){
int p = 0;
for (int i = 1;i < a.size();i++)
if (a[i] > a[p])
p = i;
cout << a.substr(0,p + 1) + b + a.substr(p + 1,a.size()) << endl;
}
}
5.2 第一类双指针算法
案例三:去掉多余的空格
输入一个字符串,字符串中可能包含多个连续的空格,请将多余的空格去掉,只留下一个空格。
做法一:使用cin读取到空格会截至的特性。
#include<iostream>
#include<cstring>
using namespace std;
int main() {
string s;
while (cin >> s) cout << s << ' ';
}
做法二:直接处理一整段字符串
#include <iostream>
#include <cstring>
#include <string>
using namespace std;
int main() {
string s,ans;
getline(cin, s);
for (int i = 0; i < s.size(); i++)
if (s[i] != ' ') ans += s[i];
else {
ans += ' ';
//第一类双指针算法核心
int j = i;
while (j < s.size() && s[j] == ' ') j++;
i = j - 1;
//第一类双指针算法核心
}
cout << ans << endl;
}
案例四:字符串中最长的连续出现的字符
求一个字符串中最长的连续出现的字符,输出该字符及其出现次数,字符串中无空白字符(空格、回车和 tabtab),如果这样的字符不止一个,则输出第一个。
# include<cstring>
# include<iostream>
using namespace std;
int main(){
int n;
cin >> n;
while (n--){
string s ;
int cnt = 0;
char c;
cin >> s;
for (int i = 0;i < s.size();i++){
int j = i;
while (j < s.size() && s[i] == s[j]) j++;
if (j - i > cnt) cnt = j - i,c = s[i];
i = j - 1;
}
cout << c << ' ' << cnt << endl;
}
return 0;
}
案例五:最长单词
一个以 .
结尾的简单英文句子,单词之间用空格分隔,没有缩写形式和其它特殊形式,求句子中的最长单词。
# include<cstring>
# include<iostream>
using namespace std;
int main(){
string res,str;
while (cin >> str){
if (str.back() == '.') str.pop_back();
if (str.size() > res.size()) res = str;
}
cout << res << endl;
return 0;
}
案例六:倒排单词
编写程序,读入一行英文(只包含字母和空格,单词间以单个空格分隔),将所有单词的顺序倒排并输出,依然以单个空格分隔。
做法一:字符拼接
# include <cstring>
# include <iostream>
using namespace std;
int main(){
string a,b;
while (cin >> a) b = a + ' ' + b;
cout << b << endl;
}
做法二:字符串数组的倒排
# include <cstring>
# include <iostream>
using namespace std;
int main(){
string strs[100];
int n = 0;
while (cin >> strs[n]) n++;
for (int i = n - 1;i >= 0;i--) cout << strs[i] << ' ';
}
5.3 KMP算法的暴力解决方案
案例七:字符串移位包含问题(暴力KMP算法)
对于一个字符串来说,定义一次循环移位操作为:将字符串的第一个字符移动到末尾形成新的字符串。
给定两个字符串 s1s1 和 s2s2,要求判定其中一个字符串是否是另一字符串通过若干次循环移位后的新字符串的子串。
例如 CDAA
是由 AABCD
两次移位后产生的新串 BCDAA
的子串,而 ABCD
与 ACBD
则不能通过多次移位来得到其中一个字符串是新串的子串。
# include <iostream>
# include <algorithm>
using namespace std;
int main(){
string s1,s2;
cin >> s1 >> s2;
if (s1.size() < s2.size()) swap(s1,s2);
int s1len = s1.size();
while (s1len--){
s1 = s1.substr(1) + s1[0];
//KMP暴力核心算法
for (int j = 0;j + s2.size() <= s1.size();j++){
int k = 0;
for (;k < s2.size();k++){
if (s1[j + k] != s2[k]){
break;
}
}
if (k == s2.size()){
puts("true");
return 0;
}
}
}
puts("false");
return 0;
}
案例八:
有三个字符串 S,S1,S2,其中,S 长度不超过 300,S1 和 S2 的长度不超过 10。现在,我们想要检测 S1 和 S2 是否同时在 S 中出现,且 S1 位于 S2 的左边,并在 S 中互不交叉(即,S1 的右边界点在 S2 的左边界点的左侧)。计算满足上述条件的最大跨距(即,最大间隔距离:最右边的 S2 的起始点与最左边的 S1 的终止点之间的字符数目)。如果没有满足条件的 S1,S2 存在,则输出 −1。例如,S= abcd123ab888efghij45ef67kl
, S1= ab
, S2= ef
,其中,S1 在 SS 中出现了 22 次,S2 也在 S中出现了 22 次,最大跨距为:18。
# include <iostream>
using namespace std;
int main(){
//输入模块
string s,s1,s2;
char c ;
while (cin >> c,c != ',') s += c;
while (cin >> c,c != ',') s1 += c;
while (cin >> c) s2 += c;
if (s.size() < s1.size() || s.size() < s2.size()) {
puts("-1");
return 0;
}
//KMP暴力匹配
int l = 0;
while (l + s1.size() <= s.size()){
int k = 0;
while (k < s1.size()){
if (s[l + k] != s1[k]) break;
k++;
}
if (k == s1.size()) break;
l++;
}
int r = s.size() - s2.size();
while (r >= 0){
int k = 0;
while (k < s2.size()){
if (s[r + k] != s2[k]) break;
k++;
}
if (k == s2.size()) break;
r--;
}
l += s1.size() - 1;
//后处理模块
if (l > r) puts("-1");
else cout << r - l - 1 << endl;
}
5.4 字符串最长公共后缀
给出若干个字符串,输出这些字符串的最长公共后缀。
# include <iostream>
using namespace std;
const int N = 210;
string str[N];
int n;
int main(){
while (cin >> n,n){
int len = 1000;
for (int i = 0;i < n;i++){
cin >> str[i] ;
if (len > str[i].size()) len = str[i].size();
}
//核心算法模块
while (len){
bool issuccess = true;
for (int i = 1;i < n;i++){
bool issame = true;
for (int j = 1;j <= len;j++){
if (str[0][str[0].size() - j] != str[i][str[i].size() - j]){
issame = false;
break;
}
}
if (!issame){
issuccess = false;
break;
}
}
if (issuccess) break;
len--;
}
//cout << len << endl;
cout << str[0].substr(str[0].size() - len) << endl;
}
return 0;
}
六. 函数
6.1 静态变量
补充知识:静态变量
void output (){
static int cnt = 0;
cnt ++;
cout << cnt << endl;
}
int main(){
output(); //1
output(); //2
output(); //3
output(); //4
output(); //5
//静态变量始终操纵一个变量,每次调用不会重新赋值为0。
}
问题一:全局变量和局部静态变量的联系与区别
局部静态变量 <==> 在函数内部开了一块只有该函数能调用的全局变量
局部变量会赋值随机值。局部静态变量默认值为0,在这一点上与全局变量一致。
此外,局部静态变量和全局变量的空间会开在堆中。而局部变量会开在栈中。
6.2 函数形参传值
一般情况下,在将一个值类型(int,double,bool等)作为形参传入方法时,在函数内部修改其值,不会影响到外部实参的改变。如果需要在函数内部修改变量值影响到函数外部实参变量。需要对变量加上取地址符号,将其转换为引用传值。
# include <iostream>
# include <cstring>
using namespace std;
int max(int &a, int b) {
a = 10, b = 20;
return a;
}
int main() {
int x = 3, y = 4;
max(x, y);
cout << x << ' ' << y << endl;//x = 10,y = 4
}
对于数组来说,传入的值为引用。但由于数组的底层实现,在作为形参传值时,只可省略第一个数组的下标:
# include <iostream>
using namespace std;
void output1(int n,int a[]) {
for (int i = 0; i < n; i++) cout << a[i] << ' ';
cout << endl;
}
//在作为形参传值时,只可省略第一个数组的下标
void outputMatrix(int m, int n, int matrix[][3]) {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++)
cout << matrix[i][j] << ' ';
cout << endl;
}
}
int main() {
int matrix[][3] = {
{1,2,3},
{4,5,6},
{7,8,9} };
outputMatrix(3, 3, matrix);
cout << endl;
int a[] = { 1,2,3,4,5,6 };
output1(6,a);
}
关于数组不同位置的长度问题:
# include <iostream>
using namespace std;
void output1(int n,int a[]) {
cout << sizeof a << endl; //4(数组指针的长度为4)
}
int main() {
int a[] = { 1,2,3,4,5,6 };
cout << sizeof a << endl; //24(6个int类型总共24字节)
output1(6,a);
}
案例一:
读取字符数组的另一种方式 cin.getline(str,size);
# include<iostream>
using namespace std;
void print(char str[]){
//for (int i = 0;i < 110;i++) cout << str[i];
//cout << endl;
printf("%s",str);
}
int main(){
char str[110];
cin.getline(str,101);
print (str);
}
案例二:写一个选择排序函数
# include <iostream>
using namespace std;
const int n = 1010;
int a[n];
void sort(int a[],int l,int r){
for (int i = l;i < r;i++)
for (int j = i + 1;j <= r;j++)
if (a[i] > a[j]) swap(a[i] ,a[j]);
}
int main(){
int n,l,r;
cin >> n >> l >> r;
for (int i = 0;i < n;i++) cin >> a[i];
sort(a,l,r);
for (int i = 0;i < n;i++) cout << a[i] << ' ';
}
6.3 DFS思想初步
案例一:跳台阶
一个楼梯共有 n 级台阶,每次可以走一级或者两级,问从第 0 级台阶走到第 n 级台阶一共有多少种方案。
# include <iostream>
using namespace std;
int ans;
int n;
void dfs(int k){
if (k == n) ans++;
else if (k < n){
dfs(k + 1);
dfs(k + 2);
}
}
int main(){
cin >> n;
dfs(0);
cout << ans << endl;
}
案例二:走方格
给定一个 n×m 的方格阵,沿着方格的边线走,从左上角 (0,0) 开始,每次只能往右或者往下走一个单位距离,问走到右下角 (n,m)一共有多少种不同的走法。
# include <iostream>
using namespace std;
int ans ;
int m,n;
void dfs(int x,int y){
if (x == n && y == m ) ans++;
else {
if (y < m ) dfs(x,y + 1);
if (x < n ) dfs(x + 1,y);
}
}
int main(){
cin >> n >> m;
dfs(0,0);
cout << ans << endl;
}
案例三:排列
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
# include <iostream>
using namespace std;
const int N = 10;
int n;
int nums[N];
int sta[N];
void dfs(int u){
if (u == n){
for (int i = 0;i < n;i++) printf ("%d ",nums[i]);
printf("\n");
return;
}
for (int i = 1;i <= n;i++){
if (!sta[i]){
sta[i] = true;
nums[u] = i;
dfs(u + 1);
sta[i] = false;
}
}
}
int main(){
scanf("%d",&n);
dfs(0);
}
补充:处理大数据使用printf和scanf能大幅加快程序运行速率。
七. 结构体 类 指针和引用
7.1 类和结构体
在C++中结构体和类在语法上很大的区别是:
- 结构体中的成员默认为public
- 类中的成员默认为private
# include <iostream>
using namespace std;
class Person {
public :
int age;
bool isDead;
double atk;
Person(){}
Person(int age,double atk) : age(age),atk(atk){}
Person(int age, bool isDead, double atk) {
this->age = age;
this->isDead = isDead;
this->atk = atk;
}
void say() {
cout << "Hello MyFriends" << endl;
cout << endl;
}
};
struct Hero {
int age;
bool isDead;
double atk;
Hero() {}
Hero(int age, double atk) : age(age), atk(atk) {}
Hero(int age, bool isDead, double atk) {
this->age = age;
this->isDead = isDead;
this->atk = atk;
}
void say() {
cout << "Hello MyFriends" << endl;
}
};
int main() {
Person p1 = {};
cout << p1.age << endl; //乱码
Person p2 = { 18,999 };
cout << p2.age << ' ' << p2.atk << endl; // 18 999
Person p3(18, false, 1.01);
cout << p3.age << ' ' << p3.isDead << ' ' << p3.atk << endl;
p3.say();
Hero h1 = { };
cout << h1.age << endl;
Hero h2 = { 18,999 };
cout << h2.age << ' ' << h2.atk << endl;
Hero h3(18, false, 8999);
cout << h3.age << ' ' << h3.isDead << ' ' << h3.atk << endl;
h3.say();
}
内存地址详解:
# include <iostream>
using namespace std;
//此处定义为堆内存变量(从小到大)
int a = 6;
int b = 10;
int e = 18;
int main() {
//此处定义为栈内存变量(从大到小)
int c = 8;
int d = 15;
int f = 21;
cout << (void*)&a << endl; //0056C034
cout << (void*)&b << endl; //0056C038
cout << (void*)&e << endl; //0056C03C
cout << (void*)&c << endl; //004FF860
cout << (void*)&d << endl; //004FF854
cout << (void*)&f << endl; //004FF848
}
7.2 内存地址 指针和引用
指针的加减**:
Cpp会自动判断指针所指变量的类型,在内存地址上表现为加上对应该类型长度的数值。
# include <iostream>
using namespace std;
int a = 6;
int e = 18;
int main() {
int* p = &a;
cout << p << endl; //004AC034
cout << p + 1 << endl; //004AC038
}
//------------------------------
char a = '6';
char e = 'a';
int main() {
char* p = &a;
cout << (void*)p << endl; //00E3C020
cout << (void*)(p + 1)<< endl; //00E3C021
}
数组指针:
# include <iostream>
using namespace std;
int main() {
int nums[] = { 2,3,5,8,1 };
cout << nums << endl; //00AFF754
cout << nums + 1 << endl; //00AFF758
cout << *(nums + 2) << endl; //5
}
cpp的引用:(类比起别名)
# include <iostream>
using namespace std;
int main() {
int a = 100;
int& p = a;
cout << "原来的a值:" << a << endl; //原来的a值:100
cout << "原来的p值:" << p << endl; //原来的p值:100
//修改其一值
a = 99;
cout << "修改后的a值:" << a << endl; //修改后的a值:99
cout << "修改后的p值:" << p << endl; //修改后的p值:99
}
八. C++ STL
8.1 vector变长数组
# include <iostream>
# include <vector>
using namespace std;
int main() {
vector<int> v = { 1,2,5,8,12 }; //定义一个一维变长数组
vector<int> a[233]; //定义一个二维变长数组
struct Rec {
int x, y;
};
vector<Rec> v1;
//迭代器(指针){ *it.begin() <==> v[0] }
vector<int> ::iterator it = v.begin();
//使用迭代器遍历vector
//遍历方法1:使用for循环
for (int i = 0; i < v.size(); i++) cout << v[i] << ' ';
cout << endl;
//遍历方法2:使用迭代器
for (vector<int> ::iterator it = v.begin(); it != v.end(); it++) cout << *it << ' ' ;
cout << endl;
//可以简化写为下述语句
for (auto it = v.begin(); it != v.end(); it++) cout << *it << ' ';
cout << endl;
//遍历方法3:使用foreach
for (int val : v) cout << val << ' ';
cout << endl;
//API
cout << v.size() << endl; //5
cout << v.empty() << endl; //0(false)
v.clear();
cout << v.empty() << endl; //1(true)
cout << *v.begin() << ' ' << v[0] << ' ' << v.front() << endl; //等价,都输出:1
cout << v.back() << ' ' << v[v.size() - 1] << endl; //等价,都输出12
}
Vector的扩容:
当长度不够的时候,会开一个新的数组,其长度为 原数组长度的二倍,将旧数组中的元素拷贝到新数组:10 -> 20 -> 40 ...
。
8.2 queue队列
FIFO
队列的基本定义:
# include <iostream>
# include <queue>
using namespace std;
int main() {
queue<int> q1; //定义一个队列
priority_queue<int> pq1; //定义一个优先队列(大根堆)
priority_queue<int,vector<int>,greater<int>> pq2; //定义一个优先队列(小根堆)
struct Rec {
int x, y;
bool operator< (const Rec& t) const {
return x < t.x;
}
};
priority_queue<Rec> pq3; //当在优先队列中使用自己定义的结构体或类时,需要重载> < 号
}
队列相关API:
# include <iostream>
# include <queue>
using namespace std;
int main() {
//普通队列
queue<int> q1; //定义一个队列
q1.push(6);
q1.push(9);
cout << q1.front() << endl; //6
cout << q1.back() << endl; //9
q1.pop();
//优先队列
priority_queue<int> pq1;
pq1.push(8);
pq1.push(12);
cout << pq1.top() << endl; //弹出最大值
pq1.pop();
}
8.3 stack栈和deque双端队列
stack定义方式与队列大差不差,此处不再详细阐述。下述开始介绍双端队列。
# include <iostream>
# include <deque>
using namespace std;
int main() {
deque<int> a;
a.push_front(6);
a.push_back(8);
a.push_back(12);
a.push_front(20);
//20 6 8 12
a.pop_front();
// 6 8 12
cout << a.back() << ' ' << a.front() << endl; //12 6
a.end(); a.begin();
cout << a[0] << endl; // 6
a.clear();
}
8.4 set和map
set底层实现:红黑树。
# include <iostream>
# include <set>
# include <map>
using namespace std;
int main() {
//set
set<int> a; //定义一个set集合,元素不可重复,有序的
multiset<int> b; //定义一个多元set集合,元素可重复
struct Rec {
int x, y;
bool operator< (const Rec& t) const {
return x < t.x;
}
};
set<Rec> c; //由于set底层有排序操作,故使用自己定义的结构体时需重写运算符
//set迭代器相关
set<int>::iterator it = a.begin();
it++; it--;
++it; --it;
a.end();
//API相关
a.insert(6); //向容器中插入一个元素,O(logn),如果元素存在,则不会重复插入状态)
a.find(6); //查找容器中是否有该元素,返回迭代器,如果不存在,返回s.end(),O(logn)
a.lower_bound(6); //找到>=6的最小元素的迭代器
a.upper_bound(6); //找到>6 的最小元素的迭代器
a.count(6); //返回6元素再set集合中存在的个数,由于不可重复,故再set中仅返回1或0,O(k + logn)
}
map底层实现:红黑树
类比为Py,C#中的 字典 。
# include <iostream>
# include <map>
using namespace std;
int main() {
//map
map<string, int> map;
map["gyx"] = 666;
cout << map["gyx"] << endl;
}
补充知识:无序容器
unordered_set
底层实现:哈希表(实现原理看数据结构!!!)
属于无需容器,相比set来说,其大部分操作都是O(1)的,效率比set高。
# include <iostream>
# include<unordered_map>
# include <unordered_set>
using namespace std;
int main() {
unordered_map<string,string> umap;
unordered_set<int> uset;
}
补充知识:二元组pair
# include <iostream>
using namespace std;
int main() {
//二元组pair
pair<int, string> p;
p = { 999,"hhh" };
cout << p.first << ' ' << p.second << endl;
}
九. 位运算与系统函数
9.1 位运算相关
位运算有以下几种: &,|,~,&&,||,^...
常见的位运算方法总结:
# include <iostream>
using namespace std;
int main() {
//求解十进制数的二进制
int a = 11;
for (int i = 7; i >= 0; i--) cout << ((a >> i) & 1 );
cout << endl;
//求解某二进制数最后一位1以及以后的数字组合
int b = 11011000; //返回1000
cout << (b & (~b + 1)) << endl; //8 即为1000
//由于计算机中使用补码存负数,所以也可以写成如下格式:
cout << (b & -b) << endl; //8 即为1000
}
9.2 常用库函数
需引入头文件 algorithm
去重和翻转
# include <iostream>
# include <algorithm>
# include <vector>
using namespace std;
int main() {
//翻转函数reverse(O(n))
vector<int> a1 = { 1,2,3,4,5 };
reverse(a1.begin(), a1.end());
for (int n : a1) cout << n << ' '; //5 4 3 2 1
cout << endl;
int a2[] = { 6,7,8,9,10 };
reverse(a2, a2 + 5);
for (int n : a2) cout << n << ' '; //10 9 8 7 6
cout << endl;
//去重函数unique
vector<int> a3 = { 1,1,2,3,5,5,8,10 };
int m = unique(a3.begin(), a3.end()) - a3.begin();
cout << m << endl; // 6
for (int i = 0; i < m;i++) cout << a3[i] << ' '; // 1 2 3 5 8 10
cout << endl;
int a4[] = { 1,1,2,3,5,5,8,10 };
int n = unique(a4, a4 + 8) - a4;
cout << n << endl;
for (int i = 0; i < n; i++) cout << a4[i] << ' ';
cout << endl;
}
随机打乱和排序
# include <iostream>
# include <vector>
# include <algorithm>
# include <ctime>
using namespace std;
struct Rec {
int x, y;
bool operator< (const Rec& t) const {
return y < t.y;
}
}a6[5];
bool cmp(int a, int b) {
return a < b;
}
int main() {
//生成随机数(打乱元素顺序)
vector<int> a1({ 1,2,3,5,8 });
srand(time(0)); //随机生成种子
random_shuffle(a1.begin(), a1.end());
for (int n : a1) cout << n << ' ';
cout << endl;
int a2[] = { 1,2,3,5,8 };
random_shuffle(a2, a2 + 5);
for (int n : a2) cout << n << ' ';
cout << endl;
//排序函数sort
vector<int> a3({ 5,8,99,10,6 });
sort(a3.begin(), a3.end()); //从小到大排序
for (int n : a3) cout << n << ' '; //5 6 8 10 99
cout << endl;
int a4[] = { 5,8,99,10,6 };
sort(a4, a4 + 5,greater<int>());
for (int n : a4) cout << n << ' '; //99 10 8 6 5
cout << endl;
//自定义排序规则
vector<int> a5({ 6,9,18,93,87 });
sort(a5.begin(), a5.end(), cmp);
for (int n : a5) cout << n << ' ';
cout << endl;
//结构体泛型比较规则自定义
for (int i = 0; i < 5; i++) {
a6[i].x = i;
a6[i].y = -i * 2;
}
sort(a6,a6 + 5);
for (int i = 0; i < 5; i++) printf("(%d,%d)", a6[i].x, a6[i].y);
cout << endl;
}
篇章二:数据结构基础
一. 线性表基础
1.1 模拟单链表基础操作
- 单链表初始化
- 在头结点后插入一个元素
- 在指定位置k后插入一个元素
- 删除指定位置k后的元素
- 遍历单链表所有元素
# include <iostream>
using namespace std;
//模拟链表相关操作
int head, index, val[100010], nex[100010];
//链表的初始化
void init() {
head = -1; //指向头结点的index
index = 0; //链表中元素个数
}
//从链表头插入一个元素
void add_to_head(int x) {
val[index] = x;
nex[index] = head;
head = index;
index++;
}
//从链表任意位置后插入一个元素
void add(int k, int x) {
val[index] = x;
nex[index] = nex[k];
nex[k] = index;
index++;
}
//移除某节点后的元素
void remove(int k) {
nex[k] = nex[nex[k]];
}
void traversal() {
for (int i = head;i != -1 ;i = nex[i]) cout << val[i] << " ";
cout << endl;
}
int main() {
int M;
cin >> M;
init();
while (M--){
char op ;
cin >> op;
int k,x;
if (op == 'I'){
cin >> k >> x;
add(k-1 ,x);
}else if (op == 'D'){
cin >> k;
if (!k) head = nex[head];
remove(k - 1);
}else if (op == 'H'){
cin >> x;
add_to_head(x);
}
}
traversal();
}
1.2 模拟双链表基础操作
- 双链表初始化
- 在指定位置k后插入一个元素
- 删除指定位置k的元素
#include <iostream>
using namespace std;
//模拟实现双链表
const int N = 100010;
int index, val[N], rig[N], lef[N];
//双向链表初始hi
void init() {
rig[0] = 1; //设定0为头指针
lef[1] = 0; //设定1为尾指针
index = 2;
}
//在指定位置k右边插入元素x
void add(int k, int x) {
val[index] = x;
rig[index] = rig[k];
lef[index] = k;
lef[rig[index]] = index;
rig[k] = index;
}
//删除指定位置k的结点
void remove(int k) {
lef[rig[k]] = lef[k];
rig[lef[k]] = rig[k];
}
1.3 模拟栈基础操作
- 压入元素
- 弹出元素
- 判空
- 查看栈顶元素
# include <iostream>
using namespace std;
// 数组模拟栈
const int N = 100010;
int tt, stack[N];
//压入元素
void push(int x) {
stack[++tt] = x;
}
//弹出元素
void pop() {
tt--;
}
//判空
bool empty() {
return tt == 0;
}
//查看栈顶元素值
int top() {
return stack[tt];
}
1.4 队列基本操作
- 元素入队
- 元素出队
- 判空
- 取队头元素
- 取队尾元素
# include <iostream>
# include <queue>
using namespace std;
//模拟队列
const int N = 100010;
int head, tail = -1, que[N];
//元素入队
void push(int x) {
que[++tail] = x;
}
//元素出队
void pop() {
head++;
}
//判空
bool empty() {
return tail < head;
}
//得到队头元素
int front() {
return que[head];
}
int back() {
return que[tail];
}
1.5 单调栈的应用
案例一:
给定一个长度为 N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
Input:
5
3 4 2 7 5
output:
-1 3 -1 2 2
# include <iostream>
using namespace std;
const int N = 10010;
int n, tt, stk[N];
int main(){
//加速C++代码可用以下两语句
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n;
while (n--){
int x;
cin >> x;
while (tt && stk[tt] >= x) tt--;
if (tt) cout << stk[tt] << ' ';
else cout << -1 << ' ';
stk[++tt] = x;
}
}
总结:单独使用cin,cout效率较低,对于大数据算法可使用printf和scanf,如果实在需要使用cin,cout,可在函数开始加上 cin.tie(0)
,或者 ios::sync_with_stdio(false)
可以很好的给输入输出加速运行。
1.6 单调队列(滑动窗口)
案例一:
给定一个大小为 n≤10六次方 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7]
,k� 为 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
# include <iostream>
using namespace std;
const int N = 1000010;
int q[N],a[N];
int n,k;
int main(){
scanf("%d%d",&n,&k);
for (int i = 0;i < n;i++) scanf("%d",&a[i]);
int hh = 0,tt = -1;
//滑动窗口求最小值
for (int i = 0;i < n;i++){
while (hh <= tt && i - k + 1 > q[hh]) hh++;
while (hh <= tt && a[q[tt]] >= a[i]) tt--;
q[++tt] = i;
if (i >= k - 1) printf("%d ",a[q[hh]]);
}
cout << endl;
hh = 0,tt = -1;
//滑动窗口求最大值
for (int i = 0;i < n;i++){
while (hh <= tt && i - k + 1 > q[hh]) hh++;
while (hh <= tt && a[q[tt]] <= a[i]) tt--;
q[++tt] = i;
if (i >= k - 1) printf("%d ",a[q[hh]]);
}
}
二. 字符串匹配基础
2.1 字符串匹配的暴力解法
string s;//长串长度为S
string n;//短串长度为N
for (int i = 1;i <= S;i++){
bool flag = true; // 字符串匹配是否成功
for (int j = 1;j <= N;j++){
if (s[i + j - 1] != n[j]){
flag = false;
break;
}
}
}
2.2 KMP算法雏形
Next数组含义:
next记录的就是当前作为后缀末位的j对应的前缀末位的位置。
实例:
长串S = “abababc”,下标对应1234567
模式串N = “abababab”,下标对应12345678
通过观察模式串,我们可以得到以下next[j]:
Next[1] = Next[2] = 0,Next[3] = 1
Next[4] = 2,Next[5] = 3,Next[6] = 4,Next[7] = 5,Next[8] = 6。
当长串匹配到 i = 7
,模式串匹配到 j = 6
时,匹配 S[i] == N[j+1]
我们发现 c != a
,此时,为了不重复比较,我们选择让 j = Next[j]
,继续按上述步骤比较即可。
算法模板:
# include <iostream>
using namespace std;
const int N = 10010 ,M = 100010;
int n,m;
char p[N],s[M];
int nex[N];
int main(){
cin >> n >> p + 1 >> m >> s + 1;
//求Next数组
for (int i = 2,j = 0;i <= n;i++){
while (j && p[i] != p[j + 1]) j = nex[j];
if (p[i] == p[j + 1]) j++;
nex[i] = j;
}
//KMP的匹配过程
for (int i = 1,j = 0;i <= m;i++){
while (j && s[i] != p[j + 1]) j = nex[j];
if (s[i] == p[j+ 1]) j++;
if (j == n){
printf("%d ",n - i);
j = nex[j];
}
}
return 0;
}
三. 树 基础
3.1 Trie字典树
概述:将字符串的各位字符以 树 的形式进行存储,加强了字符串存或取操作的便利性与高效性。
案例一:
维护一个字符串集合,支持两种操作:
I x
向集合中插入一个字符串 x;Q x
询问一个字符串在集合中出现了多少次。
共有 N个操作,所有输入的字符串总长度不超过 10五次方,字符串仅包含小写英文字母。
Input:
5
I abc
Q abc
Q ab
I ab
Q ab
Output:
1
0
1
代码样例:
# include <iostream>
using namespace std;
const int N = 100010;
int index, son[N][26], cnt[N];
void insert(char str[]){
int p = 0;
for (int i = 0;str[i];i++){
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++index;
p = son[p][u];
}
cnt[p] ++;
}
int query(char str[]){
int p = 0;
for (int i = 0;str[i] ;i++){
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
int main(){
int n;
cin >> n;
while (n--){
char op,x[N];
cin >> op >> x;
if (op == 'I'){
insert(x);
}else if (op == 'Q'){
cout << query(x) << endl;
}
}
}
3.2 并查集
主要作用:
- 将两个集合合并
- 询问两个元素是否在同一结合中
基本原理: 每个集合用一棵树表示。树根的编号就是整个集合的编号。每个结点存储它的父节点,p[x]表示x的父节点。
解决问题:
- 1.判断是否为树根:
if (p[x] == x)
- 2.如何求x的集合编号:
while(p[x] != x) x = p[x];
- 3.如何合并两个集合:px是x的集合编号,py是y集合的集合编号。p[x] = y。
案例一:合并集合
一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
M a b
,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为 a 和 b 的两个数是否在同一个集合中;
输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
Yes
No
Yes
代码样例:
# include <iostream>
using namespace std;
const int N = 100010;
int n ,m ;
int p[N]; //使用树的双亲表示法模拟存储
//通过当前节点找到他的祖宗结点(路径优化,时间复杂度O(1))
int find( int x ){
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
int main(){
cin.tie(0);
ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 1;i <= n;i++) p[i] = i;
while (m--){
char op;
int a,b;
cin >> op >> a >> b;
if (op == 'M') p[find(a)] = find(b);
else if (op == 'Q')
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
return 0;
}
案例二:连通块
给定一个包含 n个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m 个操作,操作共有三种:
C a b
,在点 a 和点 b 之间连一条边,a 和 b 可能相等;Q1 a b
,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;Q2 a
,询问点 a 所在连通块中点的数量;
# include <iostream>
using namespace std;
const int N = 100010;
int n,m;
int p[N],len[N];
//找到某节点的祖宗结点
int find(int x){
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
int main(){
cin.tie(0);
//ios::sync_with_stdio(false);
cin >> n >> m;
for (int i = 1;i <= n;i++) {
p[i] = i;
len[i] = 1;
}
while (m--){
int a,b;
char op[2];
cin >> op;
if (op[0] == 'C'){
cin >> a >> b;
if (find(a) == find(b)) continue;
len[find(b)] += len[find(a)];
p[find(a)] = find(b);
}else if(op[1] == '1'){
cin >> a >> b;
if (find(a) == find(b)) puts("Yes");
else puts("No");
}else if (op[1] == '2'){
cin >> a;
cout << len[find(a)] << endl;
}
}
}
3.3 堆与堆排序
适用于在一定范围数中筛选出满足一定条件的一部分数。比如:选出十亿数据中前一万大的数字。
核心算法:
- 元素上浮算法
- 元素下沉算法
# include <iostream>
# include <algorithm>
using namespace std;
const int N = 100010;
int n,m;
int h[N],len;
//上浮算法
void swim(int u){
while (u / 2 && h[u / 2] > h[u]){
swap(h[u / 2],h[u]);
u /= 2;
}
}
//下沉算法
void sink(int u ){
int min = u;
if (2 * u <= len && h[u * 2] < h[u]) min = u * 2;
if (2 * u + 1 <= len && h[u * 2 + 1] < h[min]) min = u * 2 + 1;
if (min != u) {
swap(h[u] ,h[min]);
sink(min);
}
}
//插入操作
void insert(int x) {
h[++len] = x;
swim(len);
}
//得到最小值
int getMinValue() {
return h[1];
}
//删除最小值
void removeMin() {
h[1] = h[len--];
sink(1);
}
//删除指定位置k处的值
void remove(int k) {
h[k] = h[len--];
swim(k);
sink(k);
}
//修改指定位置k处的值
void modify(int k, int x) {
h[k] = x;
swim(k);
sink(k);
}
int main(){
cin >> n >> m;
while(n--) cin >> h[++len] ;
for (int i = len / 2;i ; i--) sink(i); //构建一个堆
while (m--){
//弹出堆顶元素
cout << h[1] << ' ';
h[1] = h[len--];
sink(1);
}
}
四. 哈希表 基础
4.1 拉链法
拉链法实现哈希表:
# include <iostream>
# include <cstring>
using namespace std;
const int N = 100003; //使用质数且尽量规避2的整数次方能有效防止哈希冲突
int n;
int h[N]; //数组
int e[N],nex[N],idx; //链表相关
//插入x到哈希表中
void insert(int x){
int k = (x % N + N) % N; //哈希算法
e[idx] = x;nex[idx] = h[k];h[k] = idx++; //插入链表操作
}
//查看x是否在哈希表中
bool find(int x){
int k = (x % N + N) % N;
for (int i = h[k];i != -1;i = nex[i])
if (e[i] == x) return true;
return false;
}
int main(){
cin >> n;
memset(h,-1,sizeof h);
while (n--){
char op;
int x;
cin >> op >> x;
if (op == 'I'){
insert(x);
}else if (op == 'Q'){
if (find(x)) puts("Yes");
else puts("No");
}
}
}
4.2 开放寻址法
在算法竞赛中,我们常常需要用到设置一个常量用来代表“无穷大”。比如对于int类型的数,有的人会采用INT_MAX,即0x7fffffff作为无穷大。但是以INT_MAX为无穷大常常面临一个问题,即加一个其他的数会溢出。而这种情况在动态规划,或者其他一些递推的算法中常常出现,很有可能导致算法出问题。**所以在算法竞赛中,我们常采用0x3f3f3f3f来作为无穷大。**0x3f3f3f3f主要有如下好处:
0x3f3f3f3f的十进制为1061109567,和INT_MAX一个数量级,即109数量级,而一般场合下的数据都是小于109的。0x3f3f3f3f * 2 = 2122219134,无穷大相加依然不会溢出。可以使用memset(array, 0x3f, sizeof(array))来为数组设初值为0x3f3f3f3f,因为这个数的每个字节都是0x3f。
# include <iostream>
# include <cstring>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
int h[N];
int n;
int find(int x){
int k = (x % N + N) % N;
while (h[k] != null && h[k] != x){
k++;
if (k == N ) k = 0;
}
return k;
}
int main(){
cin >> n;
char op;
int x;
memset(h,0x3f,sizeof h);
while (n--){
cin >> op >> x;
int k = find(x);
if (op == 'I'){
h[k] = x;
}else if (op == 'Q'){
if (h[k] != null) puts("Yes");
else puts("No");
}
}
}