c++常用知识点总结

写在最前面:
这是我大一刚开始学c++自己边学边整理的知识点汇总,可能整理的比较乱,敬请大家谅解
,重要的知识点都有单独的文章详细介绍

1 scanf与cin

//scanf输入格式
int a;
sancf("%d",&a);
//cin读入格式
cin>>a;
//scanf和cin都不能输入空格(遇到空格,回车,结束符会自己停止)

2 char数组输入带有空格的字符串

2.1 gets

          char s[10];
	gets(s);//VS需要加_s,用gets_s
	for (int i = 0; i < strlen(s); i++)
		cout << s[i] << ' ';
//用gets(gets_s)来输入带有空格的一串字符。配合数组输出  

2.2 fgets

虽然用 gets() 时有空格也可以直接输入,但是 gets() 有一个非常大的缺陷,即它不检查预留存储区是否能够容纳实际输入的数据,换句话说,如果输入的字符数目大于数组的长度,gets 无法检测到这个问题,就会发生内存越界,所以编程时建议使用 fgets()

int s[];
fgets(数组名,很大的数,stdin);
fgets(s,100000stdin);//stdin是固定的,不需要改

fgets() 的原型为:

# include <stdio.h>
char *fgets(char *s, int size, FILE *stream);

fgets() 虽然比 gets() 安全,但安全是要付出代价的,代价就是它的使用比 gets() 要麻烦一点,有三个参数。它的功能是从 stream 流中读取 size 个字符存储到字符指针变量 s 所指向的内存空间。它的返回值是一个指针,指向字符串中第一个字符的地址。

其中:s 代表要保存到的内存空间的首地址,可以是字符数组名,也可以是指向字符数组的字符指针变量名。size 代表的是读取字符串的长度。stream 表示从何种流中读取,可以是标准输入流 stdin,也可以是文件流,即从某个文件中读取,这个在后面讲文件的时候再详细介绍。标准输入流就是前面讲的输入缓冲区。所以如果是从键盘读取数据的话就是从输入缓冲区中读取数据,即从标准输入流 stdin 中读取数据,所以第三个参数为 stdin。

3输入不确定个数的字符串并输出(可以包含空格)

1 getline(cin,s);

//(string与getline配合)!!!getline头文件是string而不是string.h也不是cstring;
string s;
	getline(cin, s);
	for (int i = 0; i < s.size();  i++){
		cout << s[i] << ' ';
	}  


2 cin.getline()

char s[100];
cin.getline(s,100);
cin.getline(数组名,最大读取数)(可以不用读满,是动态的)   

4长度和strcmp

4.1 求长度

 //  1 strlen函数(不包含结束字符)
 //  2 sizeof来求长度(可以不用加括号也可以加)(包含结束字符,使用时要长度-1)
 //  3 a.size()     

4.2 字符大小比较

strcmp(a,b);
strcmp(字符数组名字或者字符串,同前);
//大于:返回值1
//等于:0
//小于:-1
//按照字典序比较

//也可以直接使用> < >= <= == != 这6种比较运算符来比较

5 绝对值头文件

C语言中:

求整数的绝对值abs()和labs()应该包含stdlib.h

求浮点数的绝对值fabs()应该包含math.h

在C++中,只需要包括cmath即可

// c语言
#include<stdlib.h>
  abs()//整数
    
 #include<math.h> 
 fabs()//浮点数
      
 //c++
  #include<cmath>  

6 输入不确定个数的数字

while(cin>>n){ //(不输入时退出循环)
    }

 while(cin>>n,n){ //当输入是0时停止输入
 }

7 运算符及其优先级

1、 赋值运算符
(1)赋值运算符“=”用于给变量指定变量值。
(2)复合赋值运算符:+=、-=、=、/=、%=
例子:a+=3;------->a=a+3;
2、 算术运算符
(1)算术运算符包括:+、-、
、/、%、++、–
(2)对于除法运算符,如果两个操作数均是整数,结果也是整数,会舍弃小数部分;如果两个操作数中有一个是浮点数,将进行自动类型转换,结果也是浮点数,保留小数部分。
(3)对于取模运算符(取余运算符)/,如果两个操作数均是整数,结果也是整数;如果两个操作数中有一个是浮点数,结果也是浮点数,保留小数部分
(4)自增运算符有i++、++i两种使用方式,它们的特点是都相当于i=i+1;不同点是i++是先进行表达式运算再加1,而++i是先加1再进行表达式运算。
3、关系运算符(比较运算符)
(1)关系运算符用于比较两个变量或者常量的大小,运算结果是布尔值true或false。
(2)c++中共有6个关系运算符,分别是==、!=、>、<、>=、<=。
(3)>、<、>=、<=只支持数值类型的比较。
(4)、!=支持所有数据类型的比较,包括数值类型、布尔类型、引用类型。
(5)>、<、>=、<=优先级别高于
、!=。
4、 逻辑运算符
(1)逻辑运算符用于对两个布尔型操作数进行运算,其结果还是布尔值。
(2)c++ 中共有6个逻辑运算符,分别是&(逻辑与)、|(逻辑或)、^(逻辑异或)、!(逻辑反、逻辑非)、&&(短路与)、||(短路或)
(3)逻辑运算符运算规律
& :逻辑与,两个操作数都是true,结果才为true;不论左边取值,右边表达式都会进行计算
| :逻辑或font> ,两个操作数一个是true,结果为true;不论左边取值,右边表达式都会进行计算
^ : 逻辑异或,两个操作数相同,结果为false;两个操作数不同,结果为true
! :逻辑反、逻辑非 ,操作数为true,结果为false;操作数为false,结果为true
&& : 短路与 ,两个操作数都是true,结果才为true;如果左边为false,右边表达式不会进行计算
|| :短路或 , 两个操作数一个是true,结果为true;如果运算符左边的值为true,右边表达式不会进行计算
(4)优先级别:“!”>“&”>“^”>“|”>“&&”>“||”
(5)&和&&的区别:当&&的左侧为false时,将不会计算其右侧的表达式,即左false则false;无论任何情况,“&”两侧的表达式都会参与计算。
(6)|和||的区别:当||的左侧为true时,将不会计算其右侧的表达式font>,即左true则true;无论任何情况,“|”两侧的表达式都会参与计算。
5、 位运算符
(1)位运算符及运算规则
运算符 含义 运算规则
& 按位与 两个操作数都是1,结果才为1
| 按位或 两个操作数一个是1,结果为1
^ 按位异或 两个操作数相同,结果为0,;两个操作数不同,结果为1
~ 按位非/取反 操作数为1,结果为0;操作数为0,结果为1
<< 左移 二进制右侧空位补0 效果等价于乘2
>> 右移 二进制左侧空位补最高位,即符号位
>>> 无符号右移 左侧空位补0
(2)一个整数每向左移动1位,其值扩大2倍,前提是移出位数不包含有效数字。
(3)使用位运算符执行效率高font>。
(4)一个整数每向右移动1位,其值缩小2倍,前提是溢出位中不包含有效数字。
(5)位运算符对操作数以二进制为单位进行运算
(6)位运算符的操作数是整型数,包括int、short、long和char。
(7)位运算符的运算结果也是整型数,包括int、long。
(8)如果操作数是char、short,位运算前其值会自动晋升为int,运算结果也为int。
6、 条件运算符(三目运算符、三元运算符)
(1)条件运算符是C++中唯一的需要3个操作数的运算符。
(2)语法格式:
条件?表达式1:表达式2;
(3)运算规律:
首先对条件进行判断,如果其值为true,则返回表达式1的值;如果条件值为false,则返回表达式2的值。
(4)条件运算符的嵌套
条件?(条件?表达式1:表达式2):(条件?表达式1:表达式2);

运算符的优先级

(1)C++中的各种运算符都有自己的优先级和结合性
(2)优先级:在表达式运算中的运算顺序。优先级越高,在表达式中运算顺序越靠前font>。
(3)结合性:运算的方向,大多数运算符的结合性都是从左向右,即从左向右依次进行运算。
(4)优先级别最低的是赋值运算符,其次是条件运算符。
(5)单目运算符包括!、~、++、–,优先级别高。
(6)可以通过“()”控制表达式的运算顺序,“()”优先级最高font>。
(7)总体而言:优先顺序为算术运算符>关系运算符>逻辑运算符
(8)结合性从右向左的只有赋值运算符、三目运算符和单目运算符(一个操作数)font>

简单记就是:! > 算术运算符 > 关系运算符 > && > || > 赋值运算符

8 当不确定个数循环时

// 1
while(true)  {
 if(条件)break;
}  
 // 2 
while(cin>>n){
}
 //应对没有任何终止条件的循环

9 基础常识 (期末考试考点)

1 char c[]="very good";//是合法赋值语句
2 while循环可以不含语句
3 cahr s[]="abdc";printf("%s",s);//是正确的,输出为abcd
char s[];s="abcd"//是错误的 会报错
4 二维数组列数必须要有 int[]][3];这个三必须要有
5 int a=6,b=4;
if(a++>5)printf("%d",a);
else pritnf("%d",a--)//结果是a=7;
 
 if(b++>5)printf("%d",b);//就算if语句没有实现,但是括号内也会进行
else pritnf("%d",b--)//结果是b=5;  
 6 && 逻辑运算符 只要两边都不为0 返回true 如果有一个是0 返回false;
    例如 printf("&d",(a=2)&&(b=-2));结果为1

10 memset 初始化数组

#include<cstring>
memset(数组名,值,长度);
//(长度是字节(个数*4))
//  int 类型占四个字节
//  字符类型占一个字节
例如:int a[10];
memset(a,040;
一般是赋值0

11 memcpy 拷贝函数

拷贝函数 可以拷贝整数

//跟10很相似
memcpy(目的地,源头,源头长度)
int a[10],b[10]
memcpy(b,a,sizeof a)替换

12 string用法

用printf必须用 c-str() !!!!

1 初始化
string s1;默认的空字符串
string s2=s1;将s1拷贝给s2
string s3="abc";直接赋值
string s4(10,'c');内容是10个c//跟vector很像

 
2 输入输出
不能用scanf输入
puts 同理;
string s;
forchar c:s)cout<<c;
等同于for(int i=0;i<s.size();i++)cout<<s[i];

3 函数
    
1: 判断是否是空字符串empty
s1.empty()空输出true;不空输出false
    
2
string s1, s2;

s1.find(s2);    //查找
// 在 s1 中查找字符串 s2,找到返回 s2 首字母在字符串中的下标,找不到返回 -1

s1.replace(pos, len, s2);//替换
// 把 s1 中从下标 pos 开始的长度为 len 的子串替换为 s2

s1.erase(it);    //删除
// 把 s1 字符串中迭代器 it 处的字符删除

s1.erase(pos, len);//删除
// 把 s1 中从下标 pos 开始的长度为 len 的子串删除
  
 // 尾插一个字符
    a.push_back('a');

// insert(pos,char):在指定的位置pos前插入字符char
    a.insert(a.begin(),'1'); //输出 1a

  //插入字符串
  string str2="hello";
    string s2="weakhaha";
    str2.insert(2,s2,1,3);
//将字符串s2从下标为1的e开始数3个字符,分别是eak,插入原串的下标为的字符h前    

 4 
    运算
    string a;
    a+=‘ b’
    可以用来追加
 string还可以定义字符串数组
     
 5c-str()
 // 返回这个string对应的字符数组的头指针
string s = "Hello World!";
// 用printf必须用 c-str() !!!!
printf("%s", s.c_str()); //输出 "Hello World!"

   
#include<iostream> 
#include<string>
using namespace std;

int main () {
    string a = "ac";
    a += "w";//支持比较操作符>,>=,<,<=,==,!=
    cout << a << endl; //输出子串a :acw

    a += "ing";  
    cout << a << endl;
    //以字符串数组理解
    cout << a.substr(0, 3) << endl; //当第一个数是0 则后一位数:输出从头开始的长度为3的子串
    cout << a.substr(1, 3) << endl; //当第一个数是1 则输出下标为1 到下标为3的子串  
    cout << a.substr(0, 9) << endl;//如果超出长度范围 则输出原子串
    cout << a.substr(1) << endl; //从下标为1开始输出
    cout << a.substr(0) << endl; //原子串
    printf("%s\n", a.c_str());//如果用printf输出  

    return 0;
}  

13 求最小值及其下标的代码例子

#include<iostream>
using namespace std;
int main()
{
    int n,a[10000],min=10000,c=0;
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        if(min>a[i])
        {
            min=a[i];
            c=i;
        }
    }
    printf("Minimum value: %d\n",min);
    cout<<"Position: "<<c<<endl;
    return 0;
}

14 求整数二进制中1的个数

#include<iostream>
using namespace std;
int main()
{
	int a=0,c=0;
	cin >> a;
	for (int i = 0; i < 32; i++)
	{
		if ((a & 1) == 1)c++;
		a = ( a>>1);
	}
	cout << c << endl;
	return 0;
}

15字符串问题思路

1 更改字符串时,未必要对原字符串动手,可以直接再合适的位置输出

例如

在a字符串asc||码值最大的字符后面添加b字符串,直接输出,不动a字符串;

#include<iostream>
using namespace std;
int main()
{
    while (true)
    {
        string a, b;
        int c = 0;
        cin >> a >> b;
        char d = a[0];
        for (int i = 0; i < a.size(); i++)
        {
            if (d > a[i])
            {
                d = a[i];
                c = i;
            }
        }
        for (int i = 0; i < a.size(); i++)
        {
            cout << a[i];
            if (i == c)cout << b;
        }
        cout << endl;
    }
    return 0;
}

16 find查找函数

s1.find(s2);    
// 在 s1 中查找字符串 s2,找到返回 s2 首字母在字符串中的下标,找不到返回 -1

s1.replace(pos, len, s2);
// 把 s1 中从下标 pos 开始的长度为 len 的子串替换为 s2

s1.rfind() 
//反向查找字符串,即找到最后一个与子串匹配的位置(全匹配)(从前往后搜索)


1.find_first_of() 查找子串中的某个字符最先出现的位置(非全匹配)

2.find_last_of() 这个函数与find_first_of()功能差不多,只不过find_first_of()是从字符串的前面往后面搜索,而find_last_of()是从字符串的后面往前面搜索(非全匹配)


3.find_first_not_of() 找到第一个不与子串匹配的位置(非全匹配

17 erase删除函数

string s;1)s.erase(pos,n);
 //删除s从pos开始的n个字符,比如erase(0,1)就是删除第一个字符2)s.erase(position)
 //删除s的position处的一个字符(position是个string类型的迭代器)3)s.erase(first,last);
 //删除从first到last之间的字符(first和last都是迭代器)
//string的迭代器一般用s.begin()和s.end()

18 substr复制函数

substr()是C++语言函数,主要功能是复制子字符串,要求从指定位置开始,并具有指定的长度//**左闭右开

形式 : s.substr(pos, len)//pos是开始下标,len是长度

#include<iostream>
#include<string>
using namespace std;
int main()
{
	string s="sfsa";
	string a=s.substr(0,3);//拷贝从下标0开始长度为3的字符串
	string b=s.substr();//全拷贝
	string c=s.substr(2,3);//拷贝从下标2开始长度为3的字符串 若是长度不够则只拷贝有效的字符串
	cout<<a<<endl; //sfs
	cout<<b<<endl; //sfsa
	cout<<c<<endl; //sa
	return 0;
}

19 字典排序法

在这里插入图片描述

20字典序函数next_permutation()

next_permutation函数将按字母表顺序生成给定序列的下一个较大的排列,直到整个序列为降序为止。注意添加头文件#include

使用方法:next_permutation(数组头地址,数组尾地址);

返回的同时也将这个数组变成了下一个字典序

若下一个排列存在,则返回真,如果不存在则返回假
若求上一个排列,则用prev_permutation

若有返回值1

没有返回值-1;

22结构体定义总结(自己整理)(c和c++完全等价)

1

struct Student
{
	string name;
	string class;
	double score;
};
struct Student Stu;
//只能用  struct Student Stu; 来定义变量,Stu是一个结构体变量
//Stu类似于 int a=10 中a的地位,是变量

2

struct Student
{
	string name;
	string class;
	double score;
}Stu1;
 struct Student Stu2;
//Stu1 是结构体变量,与1中的Stu是一样的
//还可以继续定义struct Student Stu2;Stu2也是结构体变量,与Stu1同地位
//Stu1与Stu2都是结构体变量,类似于int a;中a的地位

3 加typedef后

typedef struct stdudent
 
{
       char name[20];
       int age;
}Stu;
  Stu stu3;
//Stu是一个数据类型,类似于 int,char,可以用来定义变量
//如Stu s1;s1在这里是结构体变量=>等价于struct stdudent s1
//Stu是struct stdudent的别名,两者等价

21结构体定义(来源CSDN)

一 C语言

1

先定义结构体类型,再定义结构体变量。

struct student
{
    char no[20];       //学号
    char name[20];    //姓名
     char sex[5];    //性别
    int age;          //年龄
};             
struct student stu1,stu2;
//此时stu1,stu2为student结构体变量

2

定义结构体类型的同时定义结构体变量

此时还可以继续定义student结构体变量,如:

struct student stu3;

stu1 和 stu2 与 stu3 都是结构体类型的变量

类似于int a=10;a是整型变量地位

struct student
{
    char no[20];        //学号
    char name[20];     //姓名
    char sex[5];      //性别
    int age;            //年龄
} stu1,stu2;   

3

不指定类型名而直接定义结构体变量

一般不使用这种方法,因为直接定义结构体变量stu1、stu2之后,就不能再继续定义该类型的变量。

struct
{
    char no[20];        //学号
    char name[20];      //姓名
    char sex[5];      //性别
    int age;          //年龄
} stu1,stu2;   

4

用typedef定义结构体变量

typedef struct stdudent
 
{
       char name[20];
       int age;
}student_t;

上面的代码,定义了一个结构体变量类型,这个类型有2个名字:第一个名字是struct student;第二个类型名字是student_t.

定义了这个之后,下面有2中方法可以定义结构体变量

第一种: struct student student_1; //定义了一个student_1的结构体变量

第二种:student_t student_1 //定义了一个student_1的结构体变量

第一种和第二种完全等价,取了个别名

推荐在实际代码中使用第四种方法定义结构体变量。

二 C++

1

普通定义,类型为struct Student,由于struct Student是一个整体,所以声明变量只能:struct Student stu;

struct Student
{
	string name;
	string class;
	double score;
};

赋值:
struct Student stu;
//stu.name="zhangsan";//这种赋值方式是不对的
strcpy(stu.name, "zhangsan");
stu.score=99;

2

第二种与第一种相比,多了Stu。它的类型也是struct Student,而Stu是定义的一个结构体实例,相当于第一种struct Student stu中的stu 意思就是直接定义了个结构体变量,名字是Stu

struct Student
{
	string name;
	string class;
	double score;
}Stu;

赋值:
//struct Student stu1;//错误,不允许这样声明变量
//Stu stu1;//错误
//Stu.name="zhangsan";//可以这样赋值
strcpy(Stu.name, "zhangsan");
Stu.score=99;

3

无名定义,且定义了一个结构体实例Stu,赋值方式同第二种。但是有局限性,以后不能再定义结构体变量了

struct 
{
	string name;
	string class;
	double score;
}Stu;

4

重定义结构体,这里使用了typedef关键字,此关键字的作用就是声明数据类型的别名,所以类型为struct Student,别名为Stu。
这种方式在声明结构体变量时有两种方式
方式一:struct Student stu;
方式二:Stu stu1;//以结构体别名声明一个结构变量,此处可以省略关键字struct
赋值略

与c语言一样

typedef struct Student
{
	string name;
	string class;
	double score;
}Stu;

5

重定义结构体,类型为Stu,声明变量:Stu stu;

但是也不能struct Student stu了,只能用 Stu stu;

与c语言一样

赋值略

typedef struct
{
	string name;
	string class;
	double score;
}Stu;

23 STL容器

一 vector

头文件 #include

vector是变长数组,支持随机访问,不支持在任意位置O(1)插入。为了保证效率,元素的增删一般应该在末尾进行。

1 声明和初始化

		#include <vector> 	//头文件

   // 几种常用的定义方法

		vector<int> a;		//开辟一个长度动态变化的int数组,未初始化 输出 0
		vector<int> b[233];	//相当于第一维是静态的长233,第二位长度动态变化的int数组
         vector<vector<int>> c; //这是一个二位动态数组,第一维和第二维都是动态的  
        vector<int> a(3);//定义一个长度为3的vector  未初始化 输出,0 0 0
 vector<int> a(10, 3); //定义一个长度为10,且每个数赋值为3
	    struct rec{};       //   结构体
	    vector<rec> c;		//自定义的结构体类型也可以保存在vector中

	//将向量b中从下标0 1 2(共三个)的元素赋值给a,a的类型为int型
	//它的初始化不和数组一样 
	vector<int>a(b.begin(),b.begin+3);

	//从数组中获得初值
	int b[7]={1,2,3,4,5,6,7};
	vector<int> a(b,b+7;
	
  
 

2 常用函数

1 a.size(); //长度函数,返回向量a长度

2  a.empty(); //判空数组,如果是空返回真

// size函数返回vector的实际长度(即包含的元素个数),empty函数返回一个bool类型,表明vector是否为空,空则返回true。二者的时间复杂度都是O(1)。
//所有的STL容器都支持这两个方法,含义也相同,之后我们就不再重复给出。

3 clear
  a.clear()
  // clear函数把vector清空。
  
4  
a.front();
//返回a的第1个元素,当且仅当a存在,是引用
a.back(); 
//返回vector的最后一个数,也是引用

5
a.pop_back();
//删除a向量的最后一个元素
a.push_back(5);
//在a的最后一个向量后插入一个元素,其值为5

6
a.begin();// 返回向量头指针,指向第一个元素
a.end();// 返回向量尾指针,指向向量最后一个元素的下一个位置
//通常与for循环结合使用

7
 a.erase(p)//从a中删除迭代器p指定的元素,p必须指向c中的一个真实元素,不能是最后一个元素end()
a.erase(b,e)//从a中删除迭代器对b和e所表示的范围中的元素,返回e,左闭右开
8
  
vector<int> a={1,2,3,4,5};
reverse(a.begin(),a.end());//a的值为5,4,3,2,1  倒置

3 比较运算

int main () {
    //支持比较运算
    vector<int> a(4, 3), b(3, 4);
    //a: 3 3 3 3   b:4 4 4 
    //比较原理字典序 (根据最前面那个判断,如果一样就往后比较)
    if (a < b) {
        puts("a < b"); 
    } 
    return 0;
}


4 三种遍历

int main () {
    vector<int> a;
    for (int i = 0; i < 10; i ++) {
        a.push_back(i);
    }
    //三种遍历vector的方法
 //one
    for (int i = 0; i < a.size(); i ++) {
        cout << a[i] << ' ';
    }
    cout << endl;
    
//two
    for (auto i = a.begin(); i != a.end(); i ++) {
        cout << *i << ' ';//a.begin()是一个迭代器,又叫指针
    }
    cout << endl;
    
//three
    //C++11的新语法,推荐用这个
    for (auto x : a) {
        cout << x << ' ';
    }
    cout << endl;  
    return 0;
}

4 vector传参

​ vector传参的三种方式:

  • void func1(vector vet); //传送数值

  • void func2(vector &vet); //引用

  • void func3(vector *vet); //指针

这三种方式产生的效果:

  • 调用拷贝构造函数,形参改变不会影响到实参
  • 不调用拷贝构函数,形参改变影响到实参
  • 不调用拷贝构函数,形参改变影响到实参
void func1(vector<int> vet) {
	vet.emplace_back(1);
}
void func2(vector<int>  &vet) {
	vet.emplace_back(2);
}
void func3(vector<int>  *vet) {
	vet->emplace_back(3);	//指针用 ->
}
int main() {
	vector<int> vet;
	func1(vet);
	cout << vet[0] << endl;	//!!!报错,因为实参不会改变(想正常运行可注释掉这句)

	vector<int> vet2;
	func2(vet2);
	cout << vet2[0] << endl;	//输出 2

	vector<int> vet3;
	func3(&vet3);	//这里取得是指针
	cout << vet3[0] << endl;	//输出 3
	//system("pause");
	return 0;
}


二 queue【队列】 和priority_queue 【优先队列、堆】

队列是一种数据结构

原理:先进先出,元素从一端入队,从另一端出队,就像是排队。
优先队列和队列特性不同:按优先级排序 和 获取

1 头文件

#include < queue >//都在这个头文件

2初始化

//queue <类型> 变量名   类型可以是内置类型也可以是容器
//priority_queue <类型> 变量名;
queue <int> q; //定义一个名为q队列
priority_queue <int> q; //默认是大根堆 //优先级为由大到小
//定义小根堆
小根堆:priority_queue <类型,vecotr <类型>,greater <类型>> 变量名


在这里插入图片描述

在这里插入图片描述

3 共同函数

q.size();// 这个队列的长度
q.empty();//用于判断这个队列是否为空,空则返回true,非空则返回false
q.push(); //往队尾插入一个元素
q.pop(); //队列:把队头弹出  优先队列 :弹出堆顶元素
q.front() //队列独有 返回队首元素;
q.top()  //优先2队列独有

4清空

⑤清空
注意:队列和堆没有clear函数
所以清空的方法就是重新初始化

q = queue <int> ();

三 stack 【栈】

C++ STL模板的栈通过现有的序列容器来实现的,默认使用双端队列deque的数据结构,也可以采用其他线性结构如:vectorlist等,前提是提供栈的入栈、出栈、栈顶元素访问和判断是否为空的操作。stack栈容器的C++标准头文件为stack。为了严格遵循栈元素后进先出原则,stack不提供元素的任何迭代器操作,提供的基础操作

1头文件

include<stack>

2初始化

//stack<类型> 名字;
stack<int> s;

3 函数

size()
返回这个栈的长度
push()
向栈顶插入一个元素
top()
返回栈顶元素
pop()
弹出栈顶元
empty()判断栈是否为空,若空则返回true,否则返回false

四 deque【双向队列】

好用,几乎其他容器的都有,就是慢一点

1 头文件

include<queue>

2 初始化

deque<int> dq;//定义一个int类型的双向队列

3 常用函数

        dq.size(); //返回这个双端队列的长度
        dq.empty(); //返回这个队列是否为空,空则返回true,非空则返回false
        dq.clear(); //清空这个双端队列
        dq.front(); //返回第一个元素
        dq.back(); //返回最后一个元素
        dq.push_back(); //向最后插入一个元素
        dq.pop_back(); //弹出最后一个元素
        dq.push_front(); //向队首插入一个元素
        dq.pop_front();//弹出第一个元素
        dq.begin(); //双端队列的第0个数
        dq.end(); //双端队列的最后一个的数的后面一个数

五 set 集合

集合set就是数学上的集合,其中的每个元素没有重复的,但是set中的元素在数据结构中是有序存储的(默认升序),为了高效的实现插入、删除和查找等操作,这与数学上的集合中元素无序性有点区别

集合set也是STL中的一种标准关联容器,其底层数据结构是基于平衡搜索树(红黑树)实现的,插入删除等操作都是通过迭代器指针实现的,不涉及内存操作,因此效率非常高。

1 头文件

集合set被包含在set头文件中

include<set>

2 定义和初始化

// 1 使用默认构造函数
set<int> st;//创建一个空的整数集合

// 2 使用初始化列表
set<int> st = {1,2,3,4,5}; // 创建包含整数的集合并初始化

// 3 使用迭代器范围
 set<int> st1 = {1,2,3,4};
 set<int> st2(st1.begin(),st.end());//利用另一个集合创建

// 4 利用自定义函数创建
struct MyComparator {
    bool operator()(const std::string& a, const std::string& b) const {
        return a.length() < b.length();
    }
};
set<string, MyComparator> st; //改变了默认升序的排序原则

// 5 使用复制函数构造
set<int>s1 = {1,2,4,5};
set<int>st2(st1);  //副本构造法

3常用函数

            size();// 返回元素个数
            empty(); //返回set是否是空的 空返回真
            clear(); //清空集合,集合长度为0
            begin(); //返回指向集合第一个元素的迭代器 允许++自增和--
            end(); //返回指向集合最后一个元素的下一个位置的迭代器
            insert(); //插入一个数 按照升序插入,若是已经有元素,返回fasle
            find(); //查找一个数 查找成功返回该元素位置的迭代器,否则返回st.end()
            count(); //返回某一个数的个数
            erase(x); //删除所以x  时间复杂度 O(k + logn)
            erase(s.begin(),s.end());//删除一个迭代器
//核心函数!!!
 		lower_bound(x); //返回大于等于x的最小的数的迭代器  核心操作
        upper_bound(x); //返回大于x的最小的数的迭代器  不存在返回end()

4 遍历输出

1 使用迭代器遍历
//可以用auto替换set<int>::iterator
for(auto it = st.begin();it!= st.end();it++ )cout<<*it<<' ';
2 使用范围循环(c++11及更高版本)
//可以用auto 替换 int 
for(const int &it :st)cout<<it<<' ';

for(auto it :st)cout<<it<<' ';//也是正确的

这种方式不需要你显示的使用迭代器,他会帮你自动处理

3 自由遍历

有些时候,我们并不想遍历输出所有的set集合,但是用迭代器的方法又不行,我们可以换个思路,用长度来遍历

set<int> st = {1,2,3,4,5};
int l = st.size(); //统计st的长度
auto it = st.begin(); //定义一个迭代器

for(int i = 0; i < l ; i++){
    cout<<*it<<' ';
    it++;
}

我们可以通过控制l来实现自由遍历

六 map

键值对映射map是由键key和值value构成的一对单元,其中key value可以是任意的数据类型。map通过建立一颗红黑树(平衡二叉树)来实现对数据自动排序的功能,从而达到高效查询和检索的目的。c++ map类似于python里面的字典

键值对映射map被包含在map头文件中

1 定义和初始化

(1) 基本定义和初始化

map<int,string> mp; //定义一个空的string->int的映射
mp[1] = "wcm";
mp[2] = "我喜欢你"

(2 )使用insert函数初始化

map<string,int> mp; //定义一个空的string->int的映射
mp.insert(make_pair(1,"hello"));

(3) 初始化列表的方式:

map<int,string> mp = {{1,"wcm"},{2,"我喜欢你"}};

2 插入元素

可以使用insert函数或者使用[]操作符来赋值

mp.insert(make_pair(1,"hello"));
mp[2] = "you";

3 访问和遍历元素

你可以通过键直接访问元素的值,这可以通过[]或者find实现:

// 1 通过键直接访问 
    map<string,int> mp = {{"wcm",520},{"wmk"},999};
cout<<mp["wcm"]<<endl;//输出的是520;

// 2 通过find访问
auto it = mp.find("wmk");
if(it!= mp.end())
   cout<<it->first<<' '<<it->second<<endl;//输出的是 wmk 999

// 3 键值对映射的遍历可以通过迭代器的方式
for(map<char, int>::iterator it=mp.begin();it!=mp.end();it++)
    cout<<it->first<<" "<<it->second;

4 删除元素

可以通过erase()函数来删除指定键的元素

// 方式一:通过key键删除,方便实用
mp.erase("wmk");//删掉键为wmk的元素

// 方式二:通过迭代器指针的方式删除
map<char, int>::iterator it=mp.begin();
mp.erase(it);

5 是否存在

若要检查是否存在某个键,可以使用count或find()函数。如果键存在,count()将返回1,否则返回0。find()返回指向元素的迭代器,不存在则返回mp.end()

if(mp.count("wcm")!=0){
    //键wcm在mp中
}

6 清空键值对映射

键值对映射的清空通过调用clear()方法实现,清空后大小变为0

mp.clear();

24 new与malloc

当使用c++需要动态分布内存空间时,通常有malloc和new

1 malloc

malloc是c语言中的函数,可以用于c++中分配内存

#include<cstdlib>  //必须包含这个头文件

//分配一维数组
int *a = (int*)malloc(sizeof(int)*5);
free(a);  //释放掉a

//分配二维数组
int **b = (int **)malloc(rows*sizeof(int *));
for(int i=0;i<rows;i++) b[i] = (int *)malloc(cols*sizeof(int));
//释放内存
for(int i=0;i<rows;i++)free(b[i]);
free(b);

//使用malloc分配单个对象
MyClass *obj = (MyClass*)malloc(sizeof(MyClass));

需要注意以下几点:

1 一定要使用头文件来访问malloc函数

2 使用sizeof来计算需要分配的内存大小

3 注意malloc返回的是void类型空间,需要强制转化成需要的类型,比如这里强制转化为了int类型

4 当不需要这块内存后,使用free函数释放掉内存空间

5 二维数组申请内存空间时,需要用for循环将第二维的每一个维度都申请空间

2 new

new是c++中的运算符,它不仅分配内存空间,还可以调用构造函数来初始化对象

// 使用new分配单个对象
MyClass *obj = new MyClass(2);
  delete obj;   

//使用new分配单个元素
int *c = new int;

//使用new分配一维数组
int *a = new int[5];
delete[] a;//[]是必须要有的

//使用new分配二维数组
int ** b = new int*[rows];
for(int i=0 ;i<rows;i++)b[i] = new int [cols];
delete[] b;
//第二维要使用for循环单独为每个申请动态空间

与malloc’不同,new返回的是一个指向分配内存的指针,并且会调用适当的构造函数。同时,当你不需要用这块内存时,你应该用delete释放它

3 区别和注意事项

1 类型安全性:new 在分配内存时会考虑类型,而malloc不会,malloc需要强制转换才能使用,这意味着new更容易避免类型错误

2 构造函数调用:new会调用适当的构造函数来初始化对象(如果有的话),而malloc只是分配了一块原始的内存空间

3 数组分配 使用new分配数组时,需要使用delete[]释放内存空间,而使用malloc分配数组时,需要用free释放内存

4 异常处理: 如果new无法分配所需的,他会抛出std:bad_alloc异常,而malloc会返回nullptr

25 排序方法总结

一 归并排序

定义:1将序列中待排序数字分为若干组,每个数字分为一组

​ 2将若干个组两两合并,保证合并后的组是有序的

​ 3重复步骤二直到只剩下一组,排序完成

在这里插入图片描述
void merge_sort(int q[], int l, int r)//y总算法c++
{
    if (l >= r) return;

    int mid = l + r >> 1;
    merge_sort(q, l, mid);
    merge_sort(q, mid + 1, r);

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}


def merge_sort(alist):#//博客算法python
 #//   """归并排序"""
    n = len(alist)
    if n <= 1:
        return alist
    mid = n//2
   #// # left 采用归并排序后形成的有序的新的列表
  #//  left_li = merge_sort(alist[:mid])
  # // # right 采用归并排序后形成的有序的新的列表
    right_li = merge_sort(alist[mid:])
   #// # 将两个有序的子序列合并为一个新的整体
   #// # merge(left, right)
    left_pointer, right_pointer = 0, 0
    result = []
    while left_pointer < len(left_li) and right_pointer < len(right_li):
        if left_li[left_pointer] <=  right_li[right_pointer]:
            result.append(left_li[left_pointer])
            left_pointer += 1
        else:
            result.append(right_li[right_pointer])
            right_pointer += 1
    result += left_li[left_pointer:]
    result += right_li[right_pointer:]
    return result

归并排序是一个递归的过程,为什么这么说呢?首先我们将这个序列2分,然后再把子序列 二分,直到每一个子序列都只含有两个元素,我们将这两个元素排序(这应该是很简单的),然后在将每两个子序列排序合并成一个更大的序列,两个更大的序列再排序合并成更更大的序列。中间的排序过程是怎样的呢?首先每一个要合并的子序列都有一个指向第一个元素的游标,两个子序列游标所指的数中找出最小的数,该数所指的游标+1,再比较,找出小的数跟原来那个数构成一个序列,以此类推,知道序列有序为止。

二 快速排序

步骤为:

从数列中挑出一个元素,称为"基准"(pivot)。
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

img

void quick_sort(int q[], int l, int r)//y总算法c++
{
    if (l >= r) return;

    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

def quick_sort(alist, first, last):#博客算法python
    """快速排序"""
    if first >= last:
        return
    mid_value = alist[first]
    low = first
    high = last
    while low < high:
        # high 左移
        while low < high and alist[high] >= mid_value:
            high -= 1
        alist[low] = alist[high]
        while low <high and alist[low] < mid_value:
            low += 1
        alist[high] = alist[low]
    # 从循环退出时,low==high
    alist[low] = mid_value
    # 对low左边的列表执行快速排序
    quick_sort(alist, first, low-1)
    # 对low右边的列表排序
    quick_sort(alist, low+1, last)

三 冒泡排序

冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

img

def insert_sort(alist):
    """插入排序"""
    n = len(alist)
    # 从右边的无序序列中取出多少个元素执行这样的过程
    for j in range(1, n):
        # j = [1, 2, 3, n-1]
        # i 代表内层循环起始值
        i = j
        # 执行从右边的无序序列中取出第一个元素,即i位置的元素,然后将其插入到前面的正确位置中
        while i > 0:
            if alist[i] < alist[i-1]:
                alist[i], alist[i-1] = alist[i-1], alist[i]
                i -= 1
            else:
                break

四 选择排序

选择排序(Selection
sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

在这里插入图片描述

def select_sort(alist):
    """选择排序"""
    n = len(alist)
    for j in range(0, n-1):
        min_index = j
        for i in range(j+1, n):
            if alist[min_index] > alist[i]:
                min_index = i
        alist[j], alist[min_index] = alist[min_index], alist[j]

这些代码是个啥意思呢?先看里面的那个循环,假设我第一个数是最小的,然后我依次遍历后面的n-1个数,如果我找到了一个比这个数更小的数,我把最小值的下标记住,然后直到找到最小的,第一层遍历结束,再把这个数与第一个数交换,那么最小的数就排在第一个了。按照这个规律,我再遍历n-2遍直到整个序列有序为止。

五 插入排序

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

在这里插入图片描述

这个图片的演示就很直观了:从第二个元素开始,如果第二个元素小于第一个元素,我就把第二个元素放在最左边,然后是第三个元素,如果小于第二个,再与第一个比较,如果小于第一个,那第三个元素就放在第一个元素的位置,以此类推。

def insert_sort(alist):
    """插入排序"""
    n = len(alist)
    # 从右边的无序序列中取出多少个元素执行这样的过程
    for j in range(1, n):
        # j = [1, 2, 3, n-1]
        # i 代表内层循环起始值
        i = j
        # 执行从右边的无序序列中取出第一个元素,即i位置的元素,然后将其插入到前面的正确位置中
        while i > 0:
            if alist[i] < alist[i-1]:
                alist[i], alist[i-1] = alist[i-1], alist[i]
                i -= 1
            else:
                break

                

先看while循环,如果第i个元素小于他前面的一个元素,我们就将两个元素交换位置,也就是较小的元素前移一位,然后这个元素再与他前面的元素比较,直到有个元素比他小了,循环结束,这个元素的位置也就确定了。再看外层for循环,i表示是第几个元素,我有几个元素就要执行几次,每次找到一个元素的正确位置,知道最后序列有序为止。

26 位运算及其符号 (二进制)

AND 与 & 1&1=1; 1&0=0; 0&0=0;

OR 或 | 1|1=1; 1|0=1; 0|0=0;

NOT 取反 ~ ~1=0; ~0=1;

XOR 异或 ^ 0^0=0; 1^1=0; 1^0=1;

//>> 右移
    int a=6//0000......110
a=a>>1;//此时a的二进制是00......11;  值是3
a=a>>1;//此时是00....1   值是1;
//a右移n次等价于a/2^n;a除以2的n次方
//<< 左移
//左移就是在最后补零
//a右移k次等价于a*2^k;

27常用库函数algorithm

更详细版本见下面的37

1 reverse 翻转

//翻转一个vector:
reverse (a.begin(), a.end());//用的迭代器
//翻转一个数组,元素存放在下标0~n:
reverse (a , a + n);

2 unique 去重

 //返回去重之后的尾迭代器(或指针),仍然为前闭后开,即这个迭代器是去重之后末尾元素的下一个位置。该函数常用于离散化,利用迭代器(或指针)的减法,可计算出去重后的元素个数。
//1 1 2 2 3 3 4 5 5 6
//去重后是1 2 3 4 5 6 1 2 3 5,把重复的向后放
//把一个vector去重
int m = unique(a.begin(), a.end()) – a.begin();
//删掉后面的元素
a.erase(unique(a.begin(), a.end()) , a.end());
//把一个数组去重,元素存放在下标0~n:
int m = unique(a , a + n) – a ;

3 random_shuffle 随机打乱

//用法与reverse相同
#include<ctime>
srand(time(0));
rando_shuffle(a.begin(),a,end());//每次都随机打乱

4 sort 排序

//对两个迭代器(或指针)指定的部分进行快速排序。可以在第三个参数传入定义大小比较的函数,或者重载“小于号”运算符。

//例:把一个int数组(元素存放在下标0~n)从大到小排序,传入比较函数:
bool cmp(int a, int b){//a是否应该排在b的前面
    return a > b;//如果a>b,那么a应该排在b前面,则返回true,否则返回false 
}
int a[]={12345};
sort(a , a+n )//从小到大排列
sort(a , a + n, cmp);//从大到小排列

//把自定义的结构体vector排序,重载“小于号”运算符:

struct rec{ int id, x, y; }
vector<rec> a;
bool operator <(const rec &a, const rec &b) {
		return a.x < b.x || (a.x == b.x && a.y < b.y);
}
sort(a.begin(), a.end());

5 lower_bound/upper_bound 二分

//lower_bound 的第三个参数传入一个元素x,在两个迭代器(指针)指定的部分上执行二分查找,返回指向第一个大于等于x的元素的位置的迭代器(指针)。
//upper_bound 的用法和lower_bound大致相同,唯一的区别是查找第一个大于x的元素。当然,两个迭代器(指针)指定的部分应该是提前排好序的。

//例 :在有序int数组(元素存放在下标1~n)中查找大于等于x的最小整数的下标:
int I = lower_bound(a + 1, a + 1 + n, x) – a;//查找时记得-a;****原数列必须是从小到大排列

//在有序vector<int> 中查找小于等于x的最大整数(假设一定存在):
int y = *--upper_bound(a.begin(), a.end(), x);

28 时间复杂度

在这里插入图片描述

29前缀和与差分

1 一维前缀和

for(int i=1;i<=n;i++)
{
    cin>>a[i];
    sum[i]=sum[i-1]+a[i];
}
//求第4到10之间的数字和
cout<<sum[10]-sum[3];

2 一维差分

//先求出d,再加进去就行
int d[100]={0};//d是差分标记
//在第5到10加4
d[5]+=4;
d[10+1]-=4;
//在4到6减3
d[4]-=4;
d[6+1]+=4;
//模板是在r到l处加k
//d[r]+=k;d[l]-=k;
//前缀和一遍
int sumd[100]={0};//求出差分
for(int i=1;i=<n;i++)
{
    sumd[i]=sumd[i-1]+d[i];
}
//得到最终数组sum
for(int i=1;i<=n;i++)//可以与上面的循环合在一起,拆开便于理解
{
    sum[i]+=sumd[i];
    cout<<sum[i];
}
//for(int i=0;i<n;i++){
//sumd[i]=sumd[i-1]+d[i];
//sum[i]+=sumd[i];
// cout<<sum[i];
//}

3 二维前缀和

for(int i=1;i<=m;i++)
for(int j=1;j<=m;j++)
    {
        scanf("%d",&a);
     sum[i][j]=sum[i][j-1]+sum[i-1][j]+a-sum[i-1][j-1];   
    }

        int x1,y1,x2,y2;
        cin>>x1>>y1>>x2>>y2;//求(x1,y1),到(x2,y2)之间的矩阵
        printf("%d\n",sum[x2][y2]-sum[x2][y1-1]-sum[x1-1][y2]+sum[x1-1][y1-1]);
    

4 二维差分

水平有限,学会再来更新

30树状数组与线段树

1树状数组

如果有一个数能被2的几次方整除们就说明它在几层
比如:3225次方整除,那么就在第五层,22的一次方整除,就在第一层,320次方整除,就在第零层
    第零层等于本身
    第一层==本身加前一个零层(2个)
    第二层==本身加前一个一层加前一个零层(3个)
    第三层==本身加前一个二层加前一个一层加前一个零层(4个)
    
    x&-x返回2的k次方,k是x二进制表示末尾0的个数,也是可以被2整除的次方、
   代码实现:
    三个函数
    int lowbit(int x)
{
    return x-&x;
}
void add(int x,int y)//y是原数组//定点修改
{
    for(int i=x;i<=n;i+=lowbit(i))
        c[i]+=y;//c[i]是后来要求数组
    //原理是把每个需要加的节点都加一遍,达到类似遍历一边的效果
}
int sum(int x,int y)//求和
{
    int res=0;
    for(int i=x;i;i-=lowbit(i))
        res+=y;//y是当前,res是前缀和
         return res;
}
//要求某一个数组[a,b],则用sum(b)-sum(a-1);
//要是在第n个数加上一个数字i,就直接add(n,i);

2 线段树

1 
 //线段树的整体思路就是通过查询和修改叶节点来实现对根节点的修改和查询
  //  代码实现
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>

using namespace std;

const int N=100010;

int n,m;

int w[N];//开一个原数组

struct node{
    int l,r;//左右区间

    int sum;//总和
}tr[N*4];//记得开 4 倍空间

void push_up(int u)//利用它的两个儿子来算一下它的当前节点信息
{
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;//左儿子 u<<1 ,右儿子 u<<1|1  
}

void build(int u,int l,int r)/*第一个参数,当前节点编号,第二个参数,左边界,第三个参数,右边界*/
{
    if(l==r)tr[u]={l,r,w[r]};//如果当前已经是叶节点了,那我们就直接赋值就可以了
    else//否则的话,说明当前区间长度至少是 2 对吧,那么我们需要把当前区间分为左右两个区间,那先要找边界点
    {
        tr[u]={l,r};//这里记得赋值一下左右边界的初值

        int mid=l+r>>1;//边界的话直接去计算一下 l + r 的下取整

        build(u<<1,l,mid);//先递归一下左儿子

        build(u<<1|1,mid+1,r);//然后递归一下右儿子

        push_up(u);//做完两个儿子之后的话呢 push_up 一遍u 啊,更新一下当前节点信息
    }
}

int query(int u,int l,int r)//查询的过程是从根结点开始往下找对应的一个区间
{
    if(l<=tr[u].l&&tr[u].r<=r)return tr[u].sum;//如果当前区间已经完全被包含了,那么我们直接返回它的值就可以了
    //否则的话我们需要去递归来算
    int mid=tr[u].l+tr[u].r>>1;//计算一下我们 当前 区间的中点是多少
    //先判断一下和左边有没有交集

    int sum=0;//用 sum 来表示一下我们的总和

    if(mid>=l)sum+=query(u<<1,l,r);//看一下我们当前区间的中点和左边有没有交集
    if(r>=mid+1)//看一下我们当前区间的中点和右边有没有交集
    sum+=query(u<<1|1,l,r);

    return sum;

}

void modify(int u,int x,int v)//第一个参数也就是当前节点的编号,第二个参数是要修改的位置,第三个参数是要修改的值
{
    if(tr[u].l==tr[u].r)tr[u].sum+=v; //如果当前已经是叶节点了,那我们就直接让他的总和加上 v 就可以了

    //否则
    else
    {

      int mid=tr[u].l+tr[u].r>>1;
      //看一下 x 是在左半边还是在右半边
      if(x<=mid)modify(u<<1,x,v);//如果是在左半边,那就找左儿子
      else modify(u<<1|1,x,v);//如果在右半边,那就找右儿子

      //更新完之后当前节点的信息就要发生变化对吧,那么我们就需要 pushup 一遍

      push_up(u);
    }

}

int main()
{
    scanf("%d%d",&n,&m);

    for(int i=1;i<=n;i++)scanf("%d",&w[i]);

    build(1,1,n);/*第一个参数是根节点的下标,根节点是一号点,然后初始区间是 1 到 n */
    //build是建树操作

    //后面的话就是一些修改操作了

    while(m--)
    {
        int k,a,b;

        scanf("%d%d%d",&k,&a,&b);

        if(!k)printf("%d\n",query(1,a,b));//求和的时候,也是传三个参数,第一个的话是根节点的编号 ,第二个的话是我们查询的区间 
        //第一个参数也就是当前节点的编号
        else
        modify(1,a,b);//第一个参数是根节点的下标,第二个参数是要修改的位置,第三个参数是要修改的值

    }
    return 0;
}

31小知识点

1

n = n | 1;
若n是偶数,则n加1;
若n是奇数,则n不变;

等后面再更新

32双指针思路及题型

1 快慢指针—— 例题

求一个长度为n的数组的最长连续不重复区间长度

例如

输入:5

1 2 2 3 4

输出:3

/*做题思路,通过利用一个计数数组来记录每种数组的出现数目,当连续
相同的数字出现两次以上时,将j更新到i的位置,删除前面已经有的记录,防止影响后面的统计,并且利用ret记录前面已经出现过的区间长度,*/
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>

using namespace std;

int n;
const int N = 11000;
int arr[N], cnt[N];//arr是原数组,cnt是计数数组,统计这个数目前出现的次数

int main()
{
	cin >> n;
	int ret = 0;
	for (int i = 0; i < n; i++)cin >> arr[i];
	for (int i = 0, j = 0; i < n; i++) {
		cnt[arr[i]]++;//统计每种数字出现的次数,出现两次就代表重复了
		if (cnt[arr[i]] > 1) {
			while (j<i) {//释放掉前面的统计,确保后面是新的开始
				cnt[arr[j]]--;//释放数组
				j++;//将j更新到i,从新开始
			}
		}
		ret = max(ret, i - j + 1);//得到最大的连续区间数目
	}
	cout << ret << endl;
	return 0;
}

33BFS宽搜

1 模板代码

队列实现版本

模拟实现队列版本

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>

using namespace std;
const int N=110;
int n,m;
typedef pair<int ,int>PII;//宏定义
PII q[N*N];//模拟实现队列
int g[N][N];//存储地图
int d[N][N];//记录到起点的距离

int bfs(){
    int hh=1,tt=1;
    q[1]={1,1};//从(1,1)开始遍历
    int dx[]={-1,0,1,0},dy[]={0,1,0,-1};
    memset(d,-1,sizeof d);
    d[1][1]=0;//从(1,1)开始,距离为0
    while(hh<=tt){
        auto t=q[hh];//接收第一个出来的坐标
        hh++;//向后移一位,弹出队头;
        for(int i=0;i<4;i++){
            int x=t.first+dx[i],y=t.second+dy[i];//记录偏移量
            if(x>0&&y>0&&x<=n&&y<=m&&d[x][y]==-1&&g[x][y]==0){//判断此方位是否符合
                d[x][y]=d[t.first][t.second]+1;//符合的话,此方位距离加1
                tt++;
                q[tt]={x,y};//将此方位的坐标插入队列,即为下一次开始的坐标
                
            }
        }
    }
    return d[n][m];//输出终点距起点的距离
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        for(int x=1;x<=m;x++){
            scanf("%d",&g[i][x]);
        }
    }
    cout<<bfs()<<endl;
    return 0;
}

36 高精度

1 高精度加法

c++是不自带高精度运算的,这时候我们就要自己模拟高精度运算

思路是

我们用数组保存这个大数字的每一位,因为涉及到进位问题,所以我们让个位先开始存进数组,即 下标为0的数组存的是个位,下标为1存十位,以此类推

这样当最后一个数位涉及到进位时,我们可以很轻松自然的在数组最后一位加一

请同时看代码同时看下面的思路,不然你可能不知道t是什么,不知道我在描述什么

本题中的t,是当前两位数字的和,即n1[i]+n2[i],它的范围是0<=t<=18,最大取18是因为两个数字的每位最大取9,9+9=18;

然后将加完的数字的取模之后加进sum数组中,为什么取模,是因为取模后得到的是本位数字,举例8+7=15,本位只需要得到5就可,1是作为进位留在下次更高位中运算

我们以45+86=131举例举例,这样比较直观好理解

第一步

t初始为0 然后t=5+6=11;

接着sum[0]存下1 这个1是由11%10得来的

然后t=t/10 即t=1,这个1是进位

第二步

t初始为1 这个1是上次进位上来的

然后t=t+4+8=13

然后sum[1]=3 这个3是由t%10得来的,即13%10=3

然后t=t/10 即t=1;

此时退出循环

第三步

判断t=1 说明还有进位

那么sum[2]=1;
最终sum数组为{1 3 1}

倒叙输出即为131

这里恰好巧合,个位和百位一样,看似是正序输出,但是最终答案要倒序输出才对,因为个位是保存在sum[0],但是人类数字习惯个位是最后一位

代码中的注释也很详细

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>

using namespace std;

string a,b;//得到要相加的数字字符串

vector <int> n1,n2;//数字n1和数字n2
vector<int>sum;//得到最终的和

//&是引用类型,速度比较快
vector<int> Myadd(vector<int>&n1,vector<int>&n2){

       int t=0;  //作为本次计算完的数字  /10后t代表进位
       
       for(int i=0;i<n1.size()||i<n2.size();i++){
           //这里加if判断是防止两个数组长度不同,短的会有越界问题
           if(i<n1.size())t+=n1[i];
           if(i<n2.size())t+=n2[i];
           sum.push_back(t%10);//取模10是为了只加上个位数,十位数是进位数
           t/=10;  //若有机会 t=1,若没进位,t=0;
           
       }
       if(t)sum.push_back(t);  //若最后一位有进位,要加1
       return sum;
              
}

int main(){
    
    cin>>a>>b;
    //因为是从各位开始保存数字,所以要用倒序
    for(int i=a.size()-1;i>=0;i--)n1.push_back(a[i]-'0');//-'0'是为了让字符变成整形
    for(int i=b.size()-1;i>=0;i--)n2.push_back(b[i]-'0');
    
    Myadd(n1,n2);
    
    for(int i=sum.size()-1;i>=0;i--)cout<<sum[i];
    
    
}

2 高精度减法

同高精度加法一样,c++也不支持高精度减法

高精度减法,同高精度加法一样,利用数组保存每位数字

这里我们给出112-99=13举例讲解sub函数部分

请同时结合代码体会下面的讲解,不然是看不懂的

1 第一步

最初 t=0 然后 t = a[0] - 0 = 2

然后 t -= b[0] 即t = 2 - 9 = -7

接下来取模,取模是为了将负数变为正数

(-7+10)%10 =3

所以sum[0] = 3

此时因为 t= -7 < 0 所以说明 本次运算向上借位了,所以要将t赋值为1

2 第二步

因为上一步借位了,所以t开始等于1;

t=a[1]-t= 1-1 =0

然后 t -= b[1] = -9

(-9+10)%10 = 1

sum[1] = 1

因为t= -9<0 说明向百位借位了

所以 t 赋值为 1

3 第三步

t = a[2]-t = 1 -1 =0

因为此时b数组已经没有数字了,所以不在参与运算

(0+10)%10 = 0

所以sim[2] = 0

退出循环

然后去掉前导零

//高精度减法模板函数
void sub(v &a, v &b){
    int t=0;//记录进位情况;
    for(int i=0;i<a.size();i++){
        t=a[i]-t;
        if(i<b.size())t-=b[i];//防止越界
        sum.push_back((t+10)%10);//若t小于零,则加十取模为正的,因为借位了
        if(t<0)t=1;//t小于0说明不够减的,说明借位了,算下一位是要把借的位减去
        else t=0;
    }
    //要去掉它的前导零
    //若是答案就是0 就不用去掉了
    while(sum.size()>1&&sum.back()==0)sum.pop_back();
}

这是完整代码

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<vector>

using namespace std;
typedef vector<int > v;

string s1,s2;//读入的数字字符串

vector<int >sum; //保存最后答案

bool cmp(v &a,v &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];
    }
    //若是两数字相等,返回true
    return true;
}

// sum = a - b;
void sub(v &a, v &b){
    int t=0;//记录进位情况;
    for(int i=0;i<a.size();i++){
        t=a[i]-t;
        if(i<b.size())t-=b[i];//防止越界
        sum.push_back((t+10)%10);//若t小于零,则加十取模为正的,因为借位了
        if(t<0)t=1;//t小于0说明不够减的,说明借位了,算下一位是要把借的位减去
        else t=0;
    }
    //要去掉它的前导零
    //若是答案就是0 就不用去掉了
    while(sum.size()>1&&sum.back()==0)sum.pop_back();
}

int main(){
    
    cin>>s1>>s2;
    
    vector<int >a,b;//被减数和减数
    
    for(int i=s1.size()-1;i>=0;i--)a.push_back(s1[i]-'0');
    for(int i=s2.size()-1;i>=0;i--)b.push_back(s2[i]-'0');
    
    //如果a>b
    if(cmp(a,b)){
        sub(a,b);
        for(int i=sum.size()-1;i>=0;i--)cout<<sum[i];
    }
    //b>a
    else{
         sub(b,a);
        cout<<'-';
        for(int i=sum.size()-1;i>=0;i--)cout<<sum[i];
    }
   return 0;
    
}

3 高精度乘法

本代码主要用于一个大数乘以一个小数

本代码不支持两个大数相乘

跟高精度加法和高精度减法有所不同 ,高精度乘法只有一个大数字a,b是小数字,可以直接输入,不用开辟新数组

本代码中,t依旧是代表进位

请同时结合代码体会下面的讲解,不然是看不懂的

我们接下来以 19*14 = 266 举例讲解

1 第一步

首先t=0;

sum[0]=(9*14+0)%10 = 6

t = (9*14+0)/10 =12

2 第二步

此时t = 12

sum[1] = (1*14+12)%10 = 6

t = (1*14+12)/10 = 2

3 第三步

此时已经跳出循环,但是t并不等于零,说明还有进位,所以我们将t补到最后一位

//高精度乘法模板函数
void mul(v &a,  int &b){
  
   int  t = 0; //t在这作为进位
   
   for(int i=0;i<a.size();i++){
       //只留下个位数作为本位数字,其余都要进位上去
      sum.push_back((a[i]*b+t)%10);   
      t = (a[i]*b+t)/10;  //新的进位要把原先的进位也算上
   }
   if(t)sum.push_back(t);  //如果最后还有进位,把最后一位也算上
   
}
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<vector>

using namespace std;
typedef vector<int > v;

string s1;//读入的数字字符串

vector<int >sum; //保存最后答案
vector<int >a;//两个乘数
int b; //存储第二个数字

void mul(v &a,  int &b){
  
   int  t = 0; //t在这作为进位
   
   for(int i=0;i<a.size();i++){
       //只留下个位数作为本位数字,其余都要进位上去
      sum.push_back((a[i]*b+t)%10);   
      t = (a[i]*b+t)/10;  //新的进位要把原先的进位也算上
   }
   if(t)sum.push_back(t);  //如果最后还有进位,把最后一位也算上
   
}

int main(){
    
    cin>>s1>>b;
 
    for(int i=s1.size()-1;i>=0;i--)a.push_back(s1[i]-'0');

       if(a.back()==0||b==0)cout<<0;
       else{
           mul(a, b);
          for(int i=sum.size()-1;i>=0;i--)cout<<sum[i];
          
       }
        return 0;
    
}

4 高精度除法

与高精度乘法一样,高精度除法也是一个大数字除以一个小数字

四种高精度运算的思路都很相似

接下来我们以175/12 = 14 …7 举例讲解

1 第一步

最初 r= 0 即余数是零

然后sum[0] = 1/12 = 0

r= 1 % 12 =1

2 第二步

r = r*10 +a =10+7 = 17

r乘十的原因是空出个位

sum[1] = 17/12 = 1

r = r%12 = 5

3 第三步
r = 10*5+5 =55

sum[2] = 55/ 12 = 4

r= 55%12 = 7

退出循环

最后答案即为14…7

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<vector>

using namespace std;

typedef vector<int > v;

string s;
v a; //被除数
int b; //除数
int r ;//余数
v sum;// 答案数组

// sum = a/b
void div(){
    
    for(int i=a.size()-1;i>=0;i--){
        r=r*10+a[i];
        sum.push_back(r/b);
        r %=b ;
        
    }
    //翻转的目的是保持输出格式与其余高精度运算一致
    reverse(sum.begin(), sum.end());
   //pop删除函数并没有真正删除数组这个数,只是取消了对地址的引用
   //换个说法,即使们利用pop删掉后,其实还是可以通过下标访问这个元素
   //只不过这个元素的地址已经与数组没关系了,只是孤零零的出现在地址当中
   
    while (sum.size() > 1&& sum.back() == 0 ) sum.pop_back();
}

int main(){
    cin>>s>>b;
    
    for(int i=s.size()-1;i>=0;i--)a.push_back(s[i]-'0');
    
    div();
    
    for(int i = sum.size()-1;i>=0;i--){
        cout<<sum[i];
    }
    cout<<endl<<r;
    return 0;
    
}

37 c++常用库函数

1 min/max

min函数用于比较得到较小数

max函数用于比较得到较大数

int a=1,b=2;
string s1 = "wang";
string s2 = "ming";
cout<<min(a,b)<<endl;
cout<<max(a,b)<<endl;
cout<<min(s1,s2)<<endl;
cout<<max(s1,s2)<<endl;
//输出结果
// 1
// 2
// ming
// kang

2 sort 排序

Algorithm中的排序函数是基于快速排序算法实现的,时间复杂度为O(N*logN)

基本思想:通过一趟排序将待排序的数据分割成独立的两部分,左边部分的所有数据比右边部分的所有数据都要,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,最后达到整个数据变成有序序列。

1 sort构成

    sort(数组首指针,数组尾指针,排序规则);
    前两个位置都是填指针
    第三个位置可以不填,不填默认按由大到小排序

2 sort默认由小到大进行排序

int arr[4]={1,3,2,4};
sort(arr,arr+4);
//排序结果是 1,2,3,4

3 自定义排序规则

//通过自定义规则进行排序
bool map(int x1,int x2){
    return x1>x2;
}
int arr[5]={3,2,1,4,5};
sort(arr,arr+n,map);
//排序结果是 5,4,3,2,1
//规则的位置不仅可重载大小于号,还可以重载其他符号

3 二分查找

二分查找算法也称折半查找算法Binary Search Algorithm,它是一种效率较高的查找方法,复杂度为O(log**N)。二分查找算法要求线性表(序列)必须采用顺序存储结构,而且表中元素按关键字有序排列。

核心思想:将表中间位置记录的关键字与待查找的关键字进行比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。

1 binary_search

int arr[4] = {1,2,3,4}; // 升序数组
bool judge1 = binary_search(arr, arr+4, 1); // judge1结果为true
bool judge2 = binary_search(arr, arr+4, 5); // judge2结果为false
//binary_search利用的也是指针

2 lower_bound

在有序序列里查找不小于关键字的元素,并返回元素索引位置最低的地址,最后根据地址来判断是否查找成功

模板函数lower_bound的基本用途是查找有序区间中第一个大于或等于某给定值的元素的位置

通常最后利用减去数组首地址得到一个数组下标

例:

int arr[5] = {1,2,4,5,6};
int c = lower_bound(arr,arr+5,4)-arr;
// c的值就是2

3 upper_bound

模板函数upper_bound的基本用途与lower_bound相对,是查找有序区间中第一个大于某给定值的元素的位置,由此本关卡的任务可以利用upper_bound获取序列中第一个大于待查找关键字的元素的位置

int arr[5] = {1,2,4,5,6};
int c = upper_bound(arr,arr+5,4)-arr;
// c 的值是 3 

4 equal_range

模板函数equal_range综合了lower_boundupper_bound的功能,通过内部调用这两个上下界查找函数,返回两个地址并组成pair:第一个地址是序列中第一个大于等于待查找关键字的元素位置,而第二个地址是第一个大于待查找关键字的元素位置。

int arr[5] = {1,2,2,4,5};
auto bounds = equal_range(arr, arr+5, 2);//auto是自动判断定义类型
int a = bounds.first-arr; // a结果为1
int b = bounds.second-arr; // b结果为3

4 copy 复制

1 copy

复制整个数组到新的数组中

int arr1[4] = {1,3,2,4};
int arr2[4];
copy(arr1, arr1+4, arr2);

2 copy_n

是可选择的复制前n个元素到新的数组

int arr1[4] = {1,3,2,4};
int arr2[4];
copy_n(arr1, 4, arr2);

5 swap 交换

int a = 1,b = 2;
swap(a,b);
//此时  a = 2, b = 1

6 replace 取代

Algorithm中取代模板函数为replace,传入参数first为数组首地址,参数last为数组尾地址,要被替换的旧元素为参数old_value,替换的新的元素为参数new_value,函数功能是将数组中所有的old_value替换为new_value,其函数原型及其应用实例如下:

int arr[4] = {1,2,2,3};
replace(arr, arr+4, 2, 0);
// arr结果为{1,0,0,3}

7 fill 填充

Algorithm中填充模板函数为fill,传入参数first为数组首地址,参数last为数组尾地址,填充值为参数val,函数功能是将数组中的所有元素都重新赋值为val

int arr[4] = {1,2,2,3};
fill(arr, arr+4, 5);
// arr结果为{5,5,5,5}

特别的,类似的填充函数还有memset,在头文件string.h中,但是赋值有限,一般仅限于-10,若设置为其他数值,则实际结果有误。

8 reverse 翻转 倒置

Algorithm中倒置模板函数为reverse,传入参数first为数组首地址,参数last为数组尾地址,函数功能是将数组中的所有元素对称交换,例如比如1 2 3,变为3 2 1,其函数原型及其应用实例如下:

int arr[4] = {1,2,3,4};
reverse(arr, arr+4);
// arr结果为{4,3,2,1}

9 rotate 滚动

Algorithm中滚动模板函数为rotate,传入参数first为数组首地址,参数last为数组尾地址,而参数middle则是数组中要滚动的最后一个元素的后一个地址,滚动完成后该地址将成为首地址,比如数组arr=[0,1,2,3,4],相应的参数为first=arrlast=arr+5middle=arr+3,则滚动后的结果为3 4 0 1 2,其函数原型及其应用实例如下:

int arr[5] = {0,1,2,3,4};
rotate(arr, arr+3, arr+5);
\\ arr结果为{3,4,0,1,2}

10 无序查找

1 无序数组查找指定元素 Find

二分查找binary_search不一样,Algorithm中的模板函数find可以在无序数组中的查找指定元素x,若存在则返回第一个x所在的地址,否则返回数组尾地址,其函数原型及其应用实例如下,其中传入参数first为数组首地址,参数last为数组尾地址,待查找指定元素为参数val

int arr[4] = {1,3,2,3};
int *p = find(arr, arr+4, 3); // p结果为地址arr+1
int *q = find(arr, arr+4, 0); // q结果为地址arr+4

2 无序数组查找指定数组子序列 Find_End

Algorithm中的模板函数find_end可以在无序数组arr1中的查找指定子数组arr2是否存在,若存在则返回待查子数组arr2最后出现在原数组arr1的地址,否则返回原数组的尾地址,例如arr1=[0,3,4,3,4]arr2=[3,4],返回结果为arr1+3,其函数原型及其应用实例如下:

int arr[5] = {0,3,4,3,4};
int arr1[2] = {3,4};
int arr2[2] = {3,5};
int *p = find_end(arr, arr+5, arr1, arr1+2); // p结果为地址arr+3
int *q = find_end(arr, arr+5, arr2, arr2+2); // q结果为地址arr+5

特别的,如想要第一次出现的地址,模板函数search 可以实现这一功能,使用方式同上

11 count 统计

Algorithm中的模板函数count可以在数组中统计指定元素x出现的次数,传入参数first为数组首地址,参数last为数组尾地址,参数x为待统计的指定元素,其函数原型及其应用实例如下:

int arr[5] = {0,3,4,3,4};
int cnt = count(arr, arr+5,3); // cnt结果为2

12 比较 Equal

两个数组相等意思是数组个数相同,对应位置上的元素值也相同,Algorithm中的模板函数equal就可以比较两个数组是否相等,返回比较真值,

int arr1[2] = {3,4};
int arr2[2] = {3,4};
int arr3[2] = {3,5};
bool judge1 = equal(arr1, arr1+2, arr2); // judge1结果为地址true
bool judge2 = equal(arr1, arr1+2, arr3); // judge2结果为地址false

13 merge 合并

合并函数的核心思想是设置两个头指针,分别指向两个升序数组首地址,通过比较两个头指针的大小,每次都将小的数值放入新的数组,然后小数值指针后移,最后新的数组也是有序的,从而完成合并过程,复杂度为O(N+M)。

int arr1[3] = {1,2,3};
int arr2[4] = {2,3,4,5};
int arr3[7];
merge(arr1, arr1+n1, arr2, arr2+n2, arr3);
// arr3结果为{1,2,2,3,3,4,5}

14 includes 包含

无序数组的包含问题就像是字符串应用中的最长公共子序列,解法是动态规划,而有序数组的包含则是简单的判断问题,解法类似有序数组的合并。

同样的,其核心思想也是设置两个头指针分别指向两个升序数组,若指针指向的元素相等,则两个指针都往后移动,否则指向数组arr1的指针往后移动,直到指针移向数组尾地址。

int arr1[4] = {1,2,3,4};
int arr2[2] = {2,3};
bool judge(arr1, arr1+4, arr2, arr2+2);
// judge结果为true

15 集合相关函数

集合是由一个或多个确定的元素所构成的整体。集合中的元素有三个特征:

  • 确定性(集合中的元素是确定的);
  • 互异性(集合中的元素是互不相同),例如:集合A=(1,a),则a不能等于1;
  • 无序性:集合中的元素没有先后之分,如集合(3,4,5)和(3,5,4)算作同一个集合,

1 set_union 集合的并

集合A和集合B的并是由所有属于集合A或属于集合B的元素所组成的集合,记作AB 或者BA,并集的一个重要属性就是越并越多。假定集合A=(1,2,3,4,5,6,7),集合B=(6,7,8,9),那么集合A和集合B的并集为AB=(1,2,3,4,5,6,7,8,9)。

Algorithm算法模板中集成了集合的并操作,函数名称为set_union,其作用是将两个集合合并成一个集合,但是要求输入的两个集合必须是有序的,这看似违背了集合的定义,但是有序的目的是为了让求并的过程实现起来变得简单。

int arr1[3]={1,2,3};
int arr2[3]={2,3,4};
int arr3[4];
int n = set_union(arr1, arr1+n1, arr2, arr2+n2, arr3)-arr3;
// arr3结果为{1,2,3,4}
// n结果为4

2 集合的交 set_intersection

集合AB的交是由所有属于集合A以及属于集合B的元素所组成的集合,记作AB或者BA,交集的一个重要属性就是越交越少。假定集合A=(1,2,3,4,5,6,7),集合B=(6,7,8,9),那么集合A和集合B的交集为AB=(6,7)。

Algorithm算法模板中集成了集合的交操作,函数名称为set_intersection,其作用是将两个集合交成一个集合,同样的要求输入的两个集合必须是有序的。因此,首先需要将两个集合排序,然后才调用set_intersection函数计算出交集。其函数原型及应用实例如下,输入参数是两个集合的首尾地址以及一个保存交集结果的数组的首地址,最后返回数组尾地址:

int arr1[3]={1,2,3};
int arr2[3]={2,3,4};
int arr3[2];
int n = set_intersection(arr1, arr1+n1, arr2, arr2+n2, arr3)-arr3;
// arr3结果为{2,3}
// n结果为2

3 集合相对差集 set_difference

集合差集也叫集合补集,是一个相对的定义:由属于A而不属于B的元素组成的集合,称为B关于A的相对补集,记作AB。例如集合A=(1,2,3,4),集合B=(3,4,5,6),那么集合AB=(1,2)。

Algorithm算法模板中集成了集合的差操作,函数名称为set_difference,其作用是计算出两个集合的差集,

int arr1[4]={1,2,3,4};
int arr2[4]={3,4,5,6};
int arr3[4];
int n = set_difference(arr1, arr1+n1, arr2, arr2+n2, arr3)-arr3;
// arr3结果为{1,2}
// n结果为2

4 集合对称差集

集合A与集合B的对称差集定义为属于集合A与集合B,但不属于它们交集AB的元素集合,记为AB。也就是说AB=xxABx∈/AB,即AB=(AB)—(AB)。同样也可以用相对差集的并来表示AB=(AB)∪(BA)。例如上述的两个集合,他们的对称差集为AB=(1,2,5,6)。

Algorithm算法模板中集成了集合对称差集的操作,函数名称为set_symmetric_difference,其作用是计算出两个集合的对称差集,同样的,要求输入的两个集合必须是有序的。

int arr1[4]={1,2,3,4};
int arr2[4]={3,4,5,6};
int arr3[8];
int n = set_symmetric_difference(arr1, arr1+n1, arr2, arr2+n2, arr3)-arr3;
// arr3结果为{1,2,5,6}
// n结果为4

14 字典序函数

1 序列排列

一般地,从n个不同元素中取出m个元素,按照一定的顺序排成一列,叫做从n个元素中取出m个元素的一个排列permutation。特别地,当m=n时,这个排列被称作全排列all permutation,本关卡就是关于n的全排列问题。

2 顺序 next_permutation

给定一个排列序列,Algorithm中的模板函数next_permutation可以产生该排列的下一个序列,输入参数为序列的首地址和尾地址,

do{
       for(int i=0;i<n;i++)cout<<arr[i]<<' ';
       next_permutation(arr,arr+n);
       i++;
   }while(i<m);

3 逆序 prev_permutation

给定一个排列序列,Algorithm中的模板函数prev_permutation可以产生该排列的上一个序列,输入参数为序列的首地址和尾地址,

do{
       for(int i=0;i<n;i++)cout<<arr[i]<<' ';
       prev_permutation(arr,arr+n);
       i++;
   }while(i<m);

38 pair

pair 是c++标准库中的一个模板类,用于存储两个不同类型的对象或值。它通常用于将两个相关的值结合在一起,例如,可以存储键值对也可以在需要返回两个值的函数中使用它

1 头文件

#include<utility>

2 创建和初始化

// 1 使用构造函数创建并初始化
 pair<int,string> pa(42,"hello");
cout<<pa.first<<' '<<pa.second<<endl;

// 2 使用大括号初始化列表
pair<int ,double>pa{520,3.14};

// 3 使用make_pair函数创建并初始化
auto pa = make_pair(5,"hello");

// 4 使用列表
pair<int ,double>pa = {1,3.14}; // 与大括号类似但不相同

3 访问pair的元素:

使用.first和.second成员老访问pair第一个和第二个元素

 pair<int,string> pa(42,"hello");
cout<<pa.first<<' '<<pa.second<<endl;

4 比较pair:

pair可以使用关系运算符来进行比较,比较的是第一个元素,如果第一个元素相等,则比较第二个

5 pair的限制:

pair只能存储两个值,如果需要更多的组合,可以考虑使用tuple或自定义结构体

有关结构体定义,可以看我这篇博客

39 typedef /define/ using区别

using typedef #define 都可用于c++创建别名

1 using

using 是一种现代c++引入的创建类型别名的方式,它提供了更好的了类型安全和可读性

语法:

using new_type_name = existing_type;

示例:

using i = int;  //创建int的别名i
i age = 18;     // 使用i作为int的别名

用途:

创建类型别名

支持模板编译

2 typedef

typedef 是传统的c++方式来创建类型的别名,它在c++11之前是主要选择

语法:

typedef existing_type new_type_name;

示例:

typedef int i;
i age = 18;

用途:

创建类型别名

支持模板编译

3 #define

#define 是预处理器指令,用于创建宏,它并不是真正创建类型别名,而只是进行文本替换

语法:

#define macro_name replacement_text  //注意不加分号

示例:

#define Len 15
int  a =Len*10;  //a的结果是150

用途:

创建常量值或简单文本替换

定义条件编译标志

4 区分和联系

1 类型安全性:

using 提供类型安全性,因为它创建真正的类型别名。
typedef 也提供类型安全性,但不如 using 易读。
#define 不提供类型安全性,因为它只是文本替换。

2 可读性:

using 和 typedef 在可读性方面相似,都可以创建具有描述性的类型别名。
#define 的可读性通常较差,因为它只是文本替换。

3 作用范围:

using 和 typedef 的作用范围受限于块或命名空间。
#define 通常具有更广泛的作用范围,可以影响整个源文件。

4用途:

using 和 typedef 用于创建类型别名,提高代码可读性。
#define 用于创建常量值或简单文本替换,也可以定义条件编译标志

5 建议

在现代C++中,using 是首选的方式,因为它提供了更好的类型安全性和可读性。typedef 仍然是有效的,尤其是在传统的C++代码中。#define 应该谨慎使用,因为它的使用容易导致维护问题和潜在的错误。根据情况选择合适的方式,以提高代码的可维护性和可读性。

40 前缀,中缀,后缀表达式之间的相互转换及计算

1 中缀转前缀

算法思想:表达式中的对象为操作数和运算符,因此需要维护两个栈表:运算符栈和操作数(中间运算结果)栈,具体算法步骤如下。

  • (1) 初始化两个栈:运算符栈S1和储存中间结果的栈S2
  • (2) 从右至左扫描中缀表达式;
  • (3) 遇到操作数时,将其压入S2
  • (4) 遇到运算符时,比较其与S1栈顶运算符的优先级:
  • (4-1) 如果S1为空,或栈顶运算符为右括号),则直接将此运算符入栈;
  • (4-2) 否则,若优先级比栈顶运算符的较高或相等,也将运算符压入S1
  • (4-3) 否则,将S1栈顶的运算符弹出并压入到S2中,再次转到(4-1)与S1中新的栈顶运算符相比较;
  • (5) 遇到括号时:
  • (5-1) 如果是右括号),则直接压入S1
  • (5-2) 如果是左括号(,则依次弹出S1栈顶的运算符,并压入S2,直到遇到右括号为止,此时将这一对括号丢弃;
  • (6) 重复步骤(2)至(5),直到表达式的最左边;
  • (7) 将S1中剩余的运算符依次弹出并压入S2
  • (8) 依次弹出S2中的元素并输出,结果即为中缀表达式对应的前缀表达式。
#include <iostream>
#include <stack>
#include <cstring>
#include <algorithm>
#include<string>
using namespace std;

stack<char> q1;  // 运算符号栈
stack<char> q2;  // 答案栈

// 判断优先级函数
bool y(char a1,char a2){
    if(a1=='+'||a1=='-'){  //优先级判断1
        if(a2==')'||a2 == '+'||a2 == '-')
        return true;
        else return false;
    }
      else if(a1=='*'||a1=='/'||a1=='%')  //优先级判断2
      return true;
    
    return false;
}

//弹出函数
void se(){

                //如果是左括号,开始弹出两个括号之间多有的内容
        while(1){
            if(q1.top()!='('&&q1.top()!=')'){
                q2.push(q1.top());
            }
             if(q1.top()==')'||q1.empty())break;
             q1.pop();  //弹出操作
        } 
        if(q1.top()==')')q1.pop();  //将最后一个右括号也弹出

}

int main(int argc, const char * argv[]) {

       string s1 ;
       cin>>s1;
     
        for(int i = s1.size()-1;i>=0;i--){
            
         if(s1[i]>='0'&&s1[i]<='9')q2.push(s1[i]);
         else{
             if(q1.size()==0||q1.top()==')'||s1[i]==')')q1.push(s1[i]); //如果是空或者右括号,直接压入q1栈
             else if(s1[i]=='('){q1.push(s1[i]);se();}     // 如果是左括号,先压入栈,然后开始找右括号
             else if(y(s1[i],q1.top()))q1.push(s1[i]);     // 如果优先级高,直接压入栈
             else {
                 while(!q1.empty()&&!y(s1[i],q1.top())){   //优先级低 开始一个一个弹出栈
              if(q1.empty()||q1.top()==')'||q1.top()=='(')break;
                 q2.push(q1.top());
                 q1.pop();
             }
             q1.push(s1[i]);  //最后还得把这个操作符入栈
             }
             
         }
        }
        //最后q1栈如果不是空,把剩余的非括号都压入栈
        while(!q1.empty()){
            if(q1.top()!='('&&q1.top()!=')')
            q2.push(q1.top());
            q1.pop();
    
        }

        //遍历q2栈
        while(!q2.empty()){
            cout<<q2.top();
            q2.pop();
        }
      cout<<endl;
    return 0;
}

2 中缀转后缀

与转换为前缀表达式相似,遵循以下步骤:

  • (1) 初始化两个栈:运算符栈S1和储存中间结果的栈S2
  • (2) 从左至右扫描中缀表达式;
  • (3) 遇到操作数时,将其压入S2
  • (4) 遇到运算符时,比较其与S1栈顶运算符的优先级:
  • (4-1) 如果S1为空,或栈顶运算符为左括号(,则直接将此运算符入栈;
  • (4-2) 否则,若优先级比栈顶运算符的高,也将运算符压入S1(注意转换为前缀表达式时是优先级较高或相同,而这里则不包括相同的情况);
  • (4-3) 否则,将S1栈顶的运算符弹出并压入到S2中,再次转到(4-1)与S1中新的栈顶运算符相比较;
  • (5) 遇到括号时:
  • (5-1) 如果是左括号(,则直接压入S1
  • (5-2) 如果是右括号),则依次弹出S1栈顶的运算符,并压入S2,直到遇到左括号为止,此时将这一对括号丢弃;
  • (6) 重复步骤(2)至(5),直到表达式的最右边;
  • (7) 将S1中剩余的运算符依次弹出并压入S2
  • (8) 依次弹出S2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式(转换为前缀表达式时不用逆序)。
//
//  main.cpp
//  step1
//
//  Created by ljpc on 2018/8/31.
//  Copyright © 2018年 ljpc. All rights reserved.
//

#include <iostream>
#include <stack>
#include <cstring>
#include <algorithm>
#include<string>
using namespace std;

stack<char> q1;  // 运算符号栈
stack<char> q2;  // 答案栈

bool y(char a1,char a2){
    if(a1=='+'||a1=='-'){  //优先级判断1
        if(a2=='(')
        return true;
        else return false;
    }
      else if(a1=='*'||a1=='/'||a1=='%') { //优先级判断2
      if(a2 == '+'||a2 == '-')
      return true;
      }
    return false;
}

void se(){

                //如果是左括号,开始弹出两个括号之间多有的内容
        while(1){
            if(q1.top()!=')'&&q1.top()!='('){
                q2.push(q1.top());
            }
             if(q1.top()=='('||q1.empty())break;
             q1.pop();  //弹出操作
        } 
        if(q1.top()=='(')q1.pop();  //将最后一个左括号也弹出

}

int main(int argc, const char * argv[]) {

       string s1 ;
       cin>>s1;
     
        for(int i = 0 ;i<s1.size();i++){
            
         if(s1[i]>='0'&&s1[i]<='9')q2.push(s1[i]);
         else{
             if(q1.size()==0||q1.top()=='('||s1[i]=='(')q1.push(s1[i]); //如果是空或者左括号,直接压入q1栈
             else if(s1[i]==')'){q1.push(s1[i]);se();}     // 如果是左括号,先压入栈,然后开始找右括号
             else if(y(s1[i],q1.top()))q1.push(s1[i]);     // 如果优先级高,直接压入栈
             else {
                 while(!q1.empty()&&!y(s1[i],q1.top())){   //优先级低 开始一个一个弹出栈
              if(q1.empty()||q1.top()=='('||q1.top()==')')break;
                 q2.push(q1.top());
                 q1.pop();
             }
             q1.push(s1[i]);  //最后还得把这个操作符入栈
             }
             
         }
        }
        //最后q1栈如果不是空,把剩余的非括号都压入栈
        while(!q1.empty()){
            if(q1.top()!='('&&q1.top()!=')')
            q2.push(q1.top());
            q1.pop();
    
        }

        //遍历q2栈
        char arr[1000] ={0};
        int l = q2.size();
        int i=0;
        while(!q2.empty()){
            arr[i++] = q2.top();
            q2.pop();
        }
        for(int i=l-1;i>=0;i--)cout<<arr[i];
      cout<<endl;

 
    return 0;
}

3 计算前缀表达式

算法思想:从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 - 运算符 - 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果。可见用计算机计算前缀表达式的值是非常容易的。

例如前缀表达式- * + 3 4 5 6

(1) 从右至左扫描,将6、5、4、3压入堆栈; (2) 遇到+运算符,因此弹出34(注意3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈; (3) 接下来是*运算符,因此弹出75,计算出7*5=35,将35入栈; (4) 最后是-运算符,计算出35-6的值,即29,由此得出最终结果。

#include <iostream>
#include <stack>
#include <cstring>
#include <algorithm>
#include<string>
using namespace std;

stack<string> q;
string s;

// 计算运算结果
string yun(int a1,int a2,char s1){
    if(s1=='+')return  to_string(a1+a2);
    else if(s1=='-')return  to_string(a1-a2);
    else if(s1=='*')return to_string(a1*a2);

}

//压入栈
void se(char s1){
    int q1 = stoi(q.top());
    q.pop();
    int q2 = stoi(q.top());
    q.pop();
    string q3 = yun(q1,q2,s1);
    q.push(q3);
    
}

int main(int argc, const char * argv[]) {

    
     cin>>s;
     for(int i=s.size()-1;i>=0;i--){
        if(s[i]==')'||s[i]=='(')continue;
         else if(s[i]>='0'&&s[i]<='9'){string d ;d+=s[i] ;q.push(d);} //用字符串的形式来储存元素
         else se(s[i]);
     }
     
         cout<<q.top()<<endl;

    
    return 0;
}

4 计算后缀表达式

算法思想:与前缀表达式类似,从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 - 运算符 - 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。

例如后缀表达式3 4 + 5 * 6 -: (1) 从左至右扫描,将34压入堆栈; (2) 遇到+运算符,因此弹出43(注意4为栈顶元素,3为次顶元素,注意与前缀表达式做比较),计算出3+4的值,得7,再将7入栈; (3) 将5入栈; (4) 接下来是*运算符,因此弹出57,计算出7*5=35,将35入栈; (5) 将6入栈; (6) 最后是-运算符,计算出35-6的值,即29,由此得出最终结果。

#include <iostream>
#include <stack>
#include <cstring>
#include <algorithm>
#include<string>
using namespace std;

stack<string> q;
string s;


string yun(int a1,int a2,char s1){
    if(s1=='+')return  to_string(a1+a2);
    else if(s1=='-')return  to_string(a2-a1);
    else if(s1=='*')return to_string(a1*a2);

}

void se(char s1){
    int q1 = stoi(q.top());
    q.pop();
    int q2 = stoi(q.top());
    q.pop();
    string q3 = yun(q1,q2,s1);
    q.push(q3);
    
}


int main(int argc, const char * argv[]) {

   
    
     cin>>s;
     for(int i=0;i<s.size();i++){
        if(s[i]==')'||s[i]=='(')continue;
         else if(s[i]>='0'&&s[i]<='9'){string d ;d+=s[i] ;q.push(d);}
         else se(s[i]);
     }
     
         cout<<q.top()<<endl;
  
    
    return 0;
}

41 背包九讲

1、 0/1背包问题

原题链接:2. 01背包问题 - AcWing题库

题目描述:

有 N件物品和一个容量是 V的背包,每件物品只能使用一次。

第 i 件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

朴素代码

#include<iostream>
using namespace std;

const int N = 1010;

int n,V;//n个物品,背包体积总共V
int dp[N][N];//dp数组,保存每次最大的选项
int v[N],w[N]; //每个物体的体积,和价值
int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i];//输入数据
    for(int i=1;i<=n;i++){ //这一层循环控制从前i个物品中选
        for(int j=0;j<=V;j++){  //这一层控制当前背包的体积
            dp[i][j] = dp[i-1][j];//如果第i个物体不选择,那么就继承i-1的
            //如果当前可用背包体积大于第i个物品的体积,那么可以选第i个物品
           //曲线救国的思想,直接计算选择了第i个物品的不好做,那我们考虑单独计算第i个物品,那么剩下的就是由不选i并且体积为j-v[i]推出来的
            if(j>=v[i])dp[i][j] =max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
        }
    }
    cout<<dp[n][V];
    return 0;

}

优化滚动数组代码

观察dp[ i,j ] = dp[ i-1,j 1] 和 dp[ i,j ] = max(dp[ i-1,j-v[i] ])+w[i ];

我们不难发现 i 层的选取方式只与 i-1 层有关,所以我们可以将 第一维的i-1删掉

但是为什么 j 改成了倒叙开始遍历呢,是因为在原二维的计算方法中,二维使用的都是i-1层的数据,但是换成一维后,我因为j>j-v[i],所以dp[j-v[i]]已经被赋值过了,即dp[ i , j -v[ i ] ]

已经被赋值过了,所以我们现在使用的是i层的dp[ j -v[ i ] ],但是显然这是不合理的,所以我们要倒叙计算,先计算大的,再去计算小的,那么就不存在数据污染这种情况了

#include<iostream>

using namespace std;

const int N = 1010;

int n,V;
int dp[N];
int v[N],w[N];
int main(){
    cin>>n>>V;
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
    
    for(int i=1;i<=n;i++){
        for(int j=V;j>=v[i];j--){
            dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
        }
    }
    cout<<dp[V];
    return 0;
    
    
}

2 完全背包问题

原题链接:3. 完全背包问题 - AcWing题库

题目描述:

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。

第 i种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

朴素算法代码

首先让我们来看看朴素算法的思想

与0/1背包问题类似,我们第i个物品可以选0个,那么dp[ i,j] = dp[i-1,j];

我们也可以选则1个,2个,3个,…,k个

同样直接算并不是很好算,那我们开始曲线救国,先从前i-1种类物品里面选择,然后再加上第i种物品的选择情况,即 dp[ i -1,j-k*v[i] ] +k * w[ i ],我们不难发现,其实选0个可以与后面的合并,也就是k==0的情况

代码如下

#include<iostream>

using namespace std;
const int N=1010;

int n,V;
int dp[N][N];
int v[N],w[N];

int main(){
    cin>>n>>V;
  for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
   
  for(int i=1;i<=n;i++){ //控制选前几件
      for(int j=0;j<=V;j++){  //控制当前的体积
          for(int k=0;k*v[i]<=j;k++){
      //max第一是dp[i][j]的原因是有不同的k产生不同的dp[i][j],所以max找到最大的自己
      //max第二个用的是dp[i-1]是因为,我们单独把第i个种类拿出来单独考虑  
       //当k==0时,dp[i][j]  =max(dp[i][j],dp[i-1][j])  所以不需要分开选择
              dp[i][j] = max(dp[i][j],dp[i-1][j-k*v[i]]+k*w[i]);    
          }
      }
  }
  cout<<dp[n][V];
    
    return 0;
}

方程展开优化法

 #include<iostream>

 using namespace std;
 const int N=1010;

 int n,V;

 int dp[N][N];

int v[N],w[N];

//朴素算法推导优化过程
//将dp[i][j]和dp[i][j-v]展开我们不难发现,dp[i][j-v]与dp[i][j]仅相差一个w[i]
//   dp[i,j] = max(dp[i,j],dp[i-1,j-1v]+1w,dp[i-1,j-2v]+2W,dp[i-1,j-3v]+3w,dp[i-1,j-4v]+4w,....)
//   dp[i,j-v]=max(dp[i,i-v],              dp[i-1,j-2v]+1w,dp[i-1,j-3v]+2w,dp[i-1,j-4v]+3w,....)
//综上可得出新的方程
//   dp[i,j] = max(dp[i,j],dp[i,j-v]+w);
 

int main(){
     cin>>n>>V;
   for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
   
   for(int i=1;i<=n;i++){ //控制选前几件
       for(int j=0;j<=V;j++){  //控制当前的体积
       //优化完后要考虑选与不选两种情况
       //优化前的代码没考虑是因为不选的情况也包含在里面了,就是k==0时的情况
       dp[i][j] = dp[i-1][j];
        if(j>=v[i]) dp[i][j] = max(dp[i][j],dp[i][j-v[i]]+w[i]);
       }
   }
   cout<<dp[n][V];
    
     return 0;
 }

优化为一维

#include<iostream>
using namespace std;
const int N=1010;

int n,V;
int dp[N];
int v[N],w[N];

int main(){
    cin>>n>>V;
  for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
   
  for(int i=1;i<=n;i++){ //控制选前几件
      for(int j=v[i];j<=V;j++){  //控制当前的体积
         //因为这次二维我们使用的是dp[i][j-v[i]],用的是i层而不是i-1层,所以我们不需要倒叙遍历,正序即可
         dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
      }
  }
  cout<<dp[V];
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

海风许愿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值