第二章 数据结构

目录

一、链表

1.1 单链表

1.1.1 数组实现

1.1.2 插入操作 

1.1.3 删除操作 

1.1.4. 题目:单链表

1.2 双链表

1.2.1 数组实现

1.2.2 初始化

1.2.3 插入操作

1.2.4 删除操作

二、栈

2.1 数组实现

2.2 题目:模拟栈

2.3 代码

2.4 单调栈 

2.4.1 题目:单调栈

2.4.2 代码

三、队列

3.1 数组实现

3.2 单调队列

3.2.1 题目:滑动窗口

3.2.2 代码

四、KMP

4.1 题目:KMP字符串

4.2 代码

五、Tire

5.1 题目:Tire字符串统计

5.2 代码

六、并查集

6.1 题目:合并集合

6.2 代码

6.3 题目:连通块中点的数量

6.4 代码

七、堆

7.1 题目:堆排序

7.2 代码

7.3 题目:模拟堆

7.4 代码

八、Hash

8.1 Hash表

8.1.1 题目:模拟散列表

8.1.2 代码

8.2 字符串Hash

8.2.1 题目:字符串哈希

8.2.2 代码

九、C++ STL

9.1 vector

9.1.1 声明 

9.1.2 函数

9.1.3 迭代器

9.2 queue

9.2.1 声明

9.2.2  循环队列 queue

9.2.3 优先队列 priority_queue

9.3 deque

9.4 set

9.4.1 声明

9.4.2 函数

9.4.3 迭代器

9.5 map

9.5.1 声明

9.5.2 函数

9.5.3 迭代器

9.6 algorithm

9.6.1 reverse 翻转

9.6.2 unique 去重

9.6.3 random_shuffle 随机打乱

9.6.4 next_permutation 下一个排序

9.6.5 sort 快速排序

9.6.6 lower_bound/ upper_bound 二分

9.7 cstring

9.7.1 substr 返回子串

9.7.2 c_str 转化为字符串数组

9.7.3 函数


一、链表

数组是一种支持随机访问,但不支持在任意位置插入或删除元素的数据结构。

链表支持在任意位置插入或删除,但只能按顺序依次访问其中的元素。

1.1 单链表

1.1.1 数组实现

const int N = 100010;
//head表示头结点的下标 
//e[i]表示节点i的值 
//ne[i]表示节点i的next指针是多少 
//idx 存储当前已经用到了哪个点 
int head,e[N],ne[N],idx; 

//初始化 
void init(){
	head = -1; //“-1”表示空集 
	idx = 0; //指针从0开始分配 
} 

1.1.2 插入操作 

① 插到头结点

void add_to_head(int x){
	e[idx] = x; //先把值存下来 
	ne[idx] = head; //指向head指针 
	head = idx;
	idx ++; //移到下一个位置 
}

② 插到下标为k的点后面  

void add(int k,int x){
	e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx;
	idx ++;
} 

1.1.3 删除操作 

void remove(int k){
	ne[k] = ne[ne[k]]; //跳过一个点指向下一个点 
}

1.1.4. 题目:单链表

实现一个单链表,链表初始为空,支持三种操作:

(1) 向链表头插入—个数;
(2) 删除第k个插入的数后面的数;
(3) 在第k个插入的数后插入—个数

现在要对该链表进行M次操作,进行完所有操作后,从头到尾输出整个链表。

注意:题目中第k个插入的数并不是指当前链表的第k个数。例如操作过程中一共插入了n个数,则按照插入的时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,……第n个插入的数。

输入格式

第一行包含整数M,表示操作次数。
接下来M行。每行包含—个操作命令。操作命令可能为以下几种:

(1)"H x",表示向链表头插入一个数 x。

(2)"D k",表示删除第k个输入的数后面的数(当k为0时,表示删除头结点)。

(3)"I k x”,表示在第k个输入的数后面插入一个数x(此操作中k均大于0)。

输出格式

共一行,将整个链表从头到尾输出。

数据范围:1 <= M <= 100000,所有操作保证合法。

输入样例:10    H 9    l 1 1    D 1    D 0    H 6    l 3 6    l 4 5    l 4 5    l 3 4    D 6

输出样例:6 4 6 5

#include <iostream>
using namespace std;

const int N = 100010;
int head,e[N],ne[N],idx; 

//初始化 
void init(){
	head = -1; //“-1”表示空集 
	idx = 0; //指针从0开始分配 
} 

//将x插到头结点 
void add_to_head(int x){
	e[idx] = x; //先把值存下来 
	ne[idx] = head; //指向head指针 
	head = idx;
	idx ++; //移到下一个位置 
}

//将x插到下标为k的点后面 
void add(int k,int x){
	e[idx] = x;
	ne[idx] = ne[k];
	ne[k] = idx;
	idx ++;
} 

//将下标为k的点后面的点删掉 
void remove(int k){
	ne[k] = ne[ne[k]]; //跳过一个点指向下一个点 
}

int main(){
	int m;
	cin >> m;
	init(); //初始化 
	while(m --){
		int k,x;
		char op;
		cin >> op;
		if(op == 'H'){ //向链表头插入一个数 x
			cin >> x;
			add_to_head(x); 
		}
		else if(op == 'D'){ //删除第k个输入的数后面的数
			cin >> k;
			if(k == 0) //删除头结点 
				head = ne[head]; //指向头结点指向它指向的点的下一个点 
			remove(k-1); //因为下标从0开始的,所以减1 
		}
		else{ //在第k个输入的数后面插入一个数x 
			cin >> k >> x;
			add(k-1,x); //因为下标从0开始的,所以减1 
		}
	}
	for(int i=head;i != -1;i = ne[i]) 
		cout << e[i] << ' ';
	cout << endl;
	return 0;
} 

1.2 双链表

1.2.1 数组实现

1.2.2 初始化

1.2.3 插入操作

① 右边插入

//在下标为k的点的右边插入x 
void add(int k,int x){
	e[idx] = x;
	//改四条边,故有四个指针操作 
	r[idx] = r[k]; //右边指向 
	l[idx] = k; //左边指向 
	l[r[k]] = idx,r[k] = idx; //先改左指针 	
}

② 左边插入

//在下标为k的点的左边插入x 
//等价于在k的左边的那个点的右边插入x
add(l[k],x); //在l[k]的右边插入 

1.2.4 删除操作

void remove(int k){
	r[l[k]] = r[k]; //k点左边的右边等于k的右边
	l[r[k]] = l[k];	//k点右边的左边等于k的左边 
}

二、栈

栈是一种“后进后出”的线性数据结构。

栈只有一端能够进出元素,我们一般称这一端为栈顶,另一端为栈底。

添加或删除栈中元素时,我们只能将其插入到栈顶或从栈顶取出。

2.1 数组实现

int stk[N],tt; //tt表示栈顶下标,初始为0
stk[++tt] = x; //在栈顶插入数据 
tt --; //弹出栈顶元素 
if(tt > 0) //判断栈是否为空
stk[tt]; //取出栈顶元素 

2.2 题目:模拟栈

实现一个栈,栈初始为空,支持四种操作:

(1) "push x",向栈顶插入—个数x;

(2) "pop",从规顶弹出一个数;

(3)"empty",判断栈是否为空;

(4)"query",查询栈顶元素。

现在要对栈进行M个操作,其中的每个操作3和操作4都要输出相应的结果。

输入格式

第一行包含整数M,表示操作次数。接下来M行,每行包含一个操作命令。操作命令为"push x", "pop”, "empty", "query" 中的一种。

输出格式

对于每个"empty"和"query"操作都要输出一个查询结果,每个结果占一行。其中,"empty"操作的查询结果为"YES"或"NO","query"操作的查询结果为一个整数,表示栈顶元素的值。

数据范围:1 <= M <= 100000,1 <= x <= 10^9,所有操作保证合法。

输入样例:10    push 5    query    push 6    pop    query    pop    empty    push 4    query    empty

输出样例:5    5    YES    4    NO

2.3 代码

#include <iostream>
#include <string>
using namespace std;
const int N = 1e5 + 100;
int a[N],n = 0;
int main()
{
	int m;
	cin >> m;
	while(m --){
		string order;
		int x;
		cin >> order;
		if(order == "push"){
			cin >> x;
			a[++n] = x;
		}	
		else if(order == "query")
			cout << a[n] << endl;
		else if(order == "pop"){
			if(n > 0)	n --;
        }
		else{
			if(n <= 0)
				cout << "YES" << endl;
			else
				cout << "NO" << endl;
		}	
	}
	return 0;
}

2.4 单调栈 

2.4.1 题目:单调栈

给定一个长度为N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出-1。

输入格式

第—行包含整数N,表示数列长度。第二行包含N个整数,表示整数数列。

输出格式

共一行,包含N个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出-1。

数据范围:1 <= N <= 10^5,1  <= 数列中元素 <= 10^9。

输入样例:5    3 4 2 7 5

输出样例:-1 3 -1 2 2

2.4.2 代码

#include <iostream>
using namespace std;
 
const int N = 100010;
int n,stk[N],tt;
int main(){
	//scanf和printf比cin和cout要快很多!!! 
	ios::sync_with_stdio(false); //可以加快cin和cout的读取速度 
	cin >> n;
	for(int i=0;i < n;i ++){
		int x;
		cin >> x;
		while(tt && stk[tt]>=x)	//栈非空,栈顶元素大
			tt --;
		if(tt)
			cout << stk[tt] << " ";
		else
			cout << -1 << " ";	 
		stk[++tt] = x; //必须先自增 
	}
	return 0;
}

三、队列

队列是一种”先进先出“的线性数据结构。

一般来讲,元素从右端(队尾)进入队列,从左端(队头)离开队列。

3.1 数组实现

//在队尾插入元素,在队头弹出元素
int q[N],hh,tt = -1; //hh初始化为0,tt初始化为-1 
//插入
q[++ tt] = x;
//弹出 
hh ++;
//判断队列是否为空 
if(hh <= tt) 
//取出队头元素
q[hh];
//去除队尾元素
q[tt]; 

3.2 单调队列

3.2.1 题目:滑动窗口

给定一个大小为 10^6 的数组。有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边,您只能在窗门中看到 k 个数字,每次滑动窗口向右移动一个位置。

输入格式

输入包含两行。第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。第二行有n个整数,代表数组的具体数值。同行数据之间用空格隔开。

输出格式

输出包含两个。第—行输出,从左至右,每个位置滑动窗口中的最小值。第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:8 3    1 3 -1 -3 5 3 6 7

输出样例:-1 -3 -3 -3 3 3    3 3 5 5 6 7

3.2.2 代码

#include <iostream>
using namespace std;

const int N = 1000010;
int a[N],q[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 ++ ){
		if(hh<=tt && i-k+1>q[hh]) //队列非空,起点i-k+1 
			hh ++; //队头滑出窗口了 
	    while (hh <= tt && a[q[tt]]>=a[i])
			tt --; //队尾元素大,出队尾 
    	q[++tt] = i; //i有可能是最小值,队列已空,i加进去就是队头元素 
		if(i >= k-1)	//窗口满k才会输出 
			printf("%d ",a[q[hh]]);
	}
	puts(""); //换行,什么都不输出就只是换行 
	
	//求滑动窗口的最大值
	hh = 0, tt = -1; //再次初始化 
	for (int i = 0; i < n; i ++ ){
		if(hh<=tt && i-k+1>q[hh]) //队列非空,起点i-k+1 
			hh ++; //队头滑出窗口了 
	    while (hh <= tt && a[q[tt]]<=a[i])
			tt --; //队尾元素小,出队尾 
    	q[++tt] = i; //i就是最大值且队列为空时,需先存后输出 
		if(i >= k-1)	//窗口满k才会输出 
			printf("%d ",a[q[hh]]); //队头是最大值 
	}
	return 0; 
}

四、KMP

KMP算法,又称模式匹配算法,能够在线性时间内判定字符串 A [ 1~N ] 是否为字符串 B [ 1~M ] 的子串,并求出字符串A在字符串B中各次出现的位置。

初始化next [1] = j = 0,按照 i = 2~N 的顺序依次计算 next [N]。  

4.1 题目:KMP字符串

给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。模板串Р在模式串S中多次作为子串出现。求出模板串P在模式串S中所有出现的位置的起始下标。

输入格式

第—行输入整数N,表示字符串P的长度。第二行输入字符串P。第三行输入整数M,表示字符串S的长度。第四行输入字符串M。

输出格式

共一行,输出所有出现位置的起始下标(下标从0开始计数),整数之间用空格隔开。

数据范围:1 <= N <= 10^4,1 <= M <= 10^5。

输入样例:3    aba    5    ababa

输出样例:0    2

4.2 代码

#include <iostream>
using namespace std;

const int N = 10010,M = 100010;
int n,m;
char p[N],s[M];
int ne[N]; //ne[1] = 0 

int main(){
	cin >> n >> p+1 >> m >> s+1; //下标从1开始,故有p+1和s+1 
	
	//求nxte数组
	for (int i = 2, j = 0; i <= m; i ++ ){
    	while (j && p[i] != p[j + 1])
			j = ne[j];
    	if (p[i] == p[j + 1])
			j ++;
    	ne[i] = j; //ne[1]初始定义为0,i从2开始 
	}
	
	//KMP匹配过程 
	for (int i = 1, j = 0; i <= m; i ++ ){ //j总往前错一位 
    	while (j && s[i]!=p[j + 1]) //j不为0即j没有退回起点,并且不能匹配 
			j = ne[j]; //退一步重新定位匹配 
    	if (s[i] == p[j + 1]) //匹配成功 
			j ++ ; //到下一个位置 
		if(j == n){ //匹配成功 
			printf("%d ",i-n); 
			j = ne[j];
		}
	}
	return 0;
}

五、Tire

Tire(字典树)是一种用于实现字符串快速检索的多叉树结构,高校地存储和查找字符串集合的数据结构。

Tire的每个节点都拥有若干个字符指针,若要插入或检索字符串时扫描到一个字符c,就沿着当前节点的c字符指针,走向该指针指向的节点。

5.1 题目:Tire字符串统计

维护一个字符串集合,支持两种操作:
1. “I x” 向集合中插入一个字符串 x;
2. “Q x” 询问一个字符串在集合中出现了多少次。
共有N个操作,输入的字符率总长度不超过 10^5,字符串仅包含小写英文字母。

输入格式

第一行包含整数N,表示操作数。接下来N行,每行包含一个操作指令,指令为 “I x” 或 “Q x” 中的一种。

输出格式

对于每个询问指令 “Q x”,都要输出一个整数作为结果,表示x在集合中出现的次数。每个结果占一行。

数据范围:1 <= N <= 2*10^4

输入样例:5    I abc    Q abc    Q ab    I ab    Q ab

输出样例:1    0    1

5.2 代码

#include <iostream> 
using namespace std;

const int N = 100010;
int son[N][26]; //仅包含小写字母,故每个节点最多向外连26条边
int cnt[N]; //以当前这个点结尾的个数 
int idx; //存储当前用到的下标,下标为0的点既是根节点,又是空节点 
char str[N]; //定义为全局变量 

void insert(char str[]){
	int p = 0;
	for(int i=0;str[i];i ++){ //字符串结尾是'/0' 
		int u = str[i]-'a'; //26个字母映射成 0~25
		if(!son[p][u]) //节点不存在 
			son[p][u] =  ++idx; //下标自增 1 
		p = son[p][u]; //走到下一个点 
	}
	cnt[p] ++; //p是最后一个点 
}

int query(char str[]){
	int p = 0;
	for(int i=0;str[i];i ++){ //字符串结尾是'/0' 
		int u = str[i]-'a'; //26个字母映射成 0~25
		if(!son[p][u]) //节点不存在 
			return 0; //返回0 
		p = son[p][u]; //走到下一个点 
	}
	return cnt[p]; //返回以p结尾的字符串数量 
}

int main(){
	int n;
	scanf("%d",&n);
	while(n --){
		char op[2]; //操作 
		scanf("%s%s",op,str);
		if(op[0] == 'I')
			insert(str);
		else
			printf("%d\n",query(str)); 
	} 
	return 0;
} 

六、并查集

并查集(Disjoint-Set)是一种可以动态维护若干个不重叠的集合,并支持合并与查询的数据结构。并查集包括两个基本操作:

1.Get,查询一个元素属于哪一个集合。

2.Merge,把两个集合合并成一个大集合。 

定义集合的表示方法采用“代表元法”,即为每个集合选择一个固定的元素,作为整个集合的“代表”。定义归属关系的表示方法,使用一个树形结构存储每个集合,树上的每个节点都是一个元素,树根是集合的代表元素。

整个并查集实际上是一个森林(若干棵树),可以维护一个数组fa来记录这个森林,用fa[x]保存x的父节点。特别地,令树根的fa值为它自己。

合并两个集合时,只需连接两个树根(令其中一个树根为另一个树根的子节点,即fa[root1] = root2)。在查询元素的归属时,需要从该元素开始通过fa存储的值不断递归访问父节点,直至到达树根。

为提高查询效率,并查集引入了路径压缩思想。即在每次执行Get操作的同时,把访问过的每个节点(也就是所查询元素的全部祖先)都直接指向树根。每次Get操作的均摊复杂度为O(logN)。

6.1 题目:合并集合

一共有n个数,编号是1-n,最开始每个数各自在一个集合中。现在要进行m个操作,操作共有两种:
1.“M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
2.“Q a b”,询问编号为a和b的两个数是否在同一个集合中;

输入格式

第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“M a b”或“Q a b”中的一种

输出格式

对于每个询问指令“Q a b”,都要输出一个结果,如果a和b在同一集合内,则输出“Yes”,否则输出“No”。每个结果占一行。

数据范围:1 <= n, m <= 10^5 

输入样例:4 5    M 1 2    M 3 4    Q 1 2    Q 1 3    Q 3 4

输出样例:Yes    No    Yes

6.2 代码

#include <iostream>
using namespace std;

const int N = 100010;
int n,m;
int p[N]; //记录每个元素的父节点 

//返回x的祖宗节点+路径压缩 
int find(int x){
	if(p[x] != x)
		p[x] = find(p[x]); //父节点等于祖宗节点 
	return p[x]; 
} 

int main(){
	scanf("%d%d",&n,&m); //读入点的数量和操作数 
	for(int i=1;i <= n;i ++)
		p[i] = i; //所有节点的父节点赋值成自己 
	while(m --){
		char op[2];
		int a,b;
		//按字符串读入,可忽略空格和回车,更易过某些测试样例 
		scanf("%s%d%d",op,&a,&b); //数组名即为该数组的首地址
		if(op[0] == 'M') //合并集合 
			p[find(a)] = find(b); //a的祖宗节点的父亲等于b的祖宗节点 
		else{ //查询集合 
			if(find(a) == find(b))
				puts("Yes"); 
			else puts("No");
		} 
	}
	return 0;
}

6.3 题目:连通块中点的数量

给定一个包含n个点(编号为1-n)的无向图,初始时图中没有边。现在要进行m个操作,操作共有三种:
1.“C a b”,在点a和点b之间连一条边,a和b可能相等;
2.“Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等;
3.“Q2 a”,询问点a所在连通块中点的数量;

输入格式

第一行输入整数n和m。接下来m行,每行包含一个操作指令,指令为“C a b”,“Q1 a b”或“Q2 a”中的一种。

输出格式

对于每个询问指令“Q1 a b”,如果a和b在同一个连通块中,则输出”Yes”,否则输出"No”。对于每个询问指令“Q2 a”,输出一个整数表示点a所在连通块中点的数量。每个结果占一行。

数据范围:1 <= n,m <= 10

输入样例:5 5    C 1 2    Q1 1 2    Q2 1    C 2 5    Q2 5

输出样例:Yes    2    3

6.4 代码

#include <iostream>
using namespace std;

const int N = 100010;
int n,m;
int p[N]; //记录每个元素的父节点 
int size[N]; //表示每一个集合的大小,即集合中点的数量 

//返回x的祖宗节点+路径压缩 
int find(int x){
	if(p[x] != x)
		p[x] = find(p[x]); //父节点等于祖宗节点 
	return p[x]; 
} 

int main(){
	scanf("%d%d",&n,&m); //读入点的数量和操作数 
	for(int i=1;i <= n;i ++){
		p[i] = i; //所有节点的父节点赋值成自己,即有n个集合 
		size[i] = 1; //最开始每个集合里面只有一个点 
	}
	while(m --){
		char op[5];
		int a,b;
		//按字符串读入,可忽略空格和回车,更易过某些测试样例 
		scanf("%s",op); //读入操作,数组名即为该数组的首地址
		if(op[0] == 'C'){//连接两个点 
			scanf("%d%d",&a,&b); 
			if(find(a) == find(b)) //两点相等的情况 
				continue; //不继续后面的操作 
			size[find(b)] += size[find(a)]; //a归属于b,更新b集合的点数 
			p[find(a)] = find(b); //a的祖宗节点的父亲等于b的祖宗节点 
		}
		else if(op[1] == '1'){ //判断第二个字符,查询两个点是否连通
			scanf("%d%d",&a,&b); 
			if(find(a) == find(b))
				puts("Yes"); 
			else	puts("No");
		} 
		else{ //询问某个点所在集合中点的总数 
			scanf("%d",&a); 
			printf("%d\n",size[find(a)]); 
		}
	}
	return 0;
}

七、堆

二叉堆是一种支持插入、删除、查询最值的数据结构。

它其实是一棵满足“堆性质”的完全二叉树,树上每个节点带有一个权值。若树中的任意一个节点的权值都小于等于其父节点的权值,则称该二叉树满足“大根堆性质”,满足“大根堆性质”的完全二叉树就是“大根堆”。若树中任意一个节点的权值都大于等于其父节点的权值,则称该二叉树满足“小根堆性质”,满足“小根堆性质”的完全二叉树就是“小根堆”。

根据完全二叉树性质,我们可以采用层次序列存储方式,直接用一个数组来保存二叉堆。

层序序列存储方式,就是逐层从左到右为树中的节点依次编号,把此编号作为节点在数组中存储的位置(下标)。

7.1 题目:堆排序

输入一个长度为n的整数数列,从小到大输出前m小的数。

输入格式

第一行包含整数n和m。第二行包含n个整数,表示整数数列。

输出格式

共一行,包含m个整数,表示整数数列中前m小的数。

数据范围:1 < =m <= n <= 10^5,1<= 数列中元素 <= 10^9

输入样例:5 3    4 5 1 3 2

输出样例:1 2 3 

7.2 代码

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

const int N =100010;
int n,m;
int h[N], size;

void down(int u){
	int t = u; //用t存3个点中的最小值的下标 
	if(u*2<=size && h[u*2]<h[t]) //左儿子存在并且其值小于h[t] 
		t = u*2; //等于左儿子
	if(u*2+1<=size && h[u*2+1]<h[t])
		t = u*2+1; 
	if(u != t){ //根节点u不是最小值
		swap(h[u],h[t]); //与叶节点t交换
		down(t); //再排序t的位置,使得满足“小根堆性质” 
	}
}

void up(int u){
	while(u/2 && h[u/2]>h[u]){ //父节点非0且其值小于叶节点 
		swap(h[u/2],h[u]); //交换两数 
		u /= 2; //值更新为父节点 
	}
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i <= n;i ++)
		scanf("%d",&h[i]);
	size = n; //堆的大小,下标从1开始 
	for(int i=n/2;i;i --) //时间复杂度小于O(n) 
		down(i);
	while(m --){
		printf("%d ",h[1]); //输出堆顶元素
		//删除堆顶元素
		h[1] = h[size];
		size --;
		down(1); 
	}
	return 0;
}

7.3 题目:模拟堆

维护一个集合,初始时集合为空,支持如下几种操作:
1.“I x”,插入一个数x;
2.“PM”,输出当前集合中的最小值;
3.“DM”,删除当前集合中的最小值(当最小值不唯一时,删除最早插入的最小值);
4.“D k”,删除第k个插入的数;
5.“C k x”,修改第k个插入的数,将其变为x;
现在要进行N次操作,对于所有第2个操作,输出当前集合的最小值。

输入格式

第一行包含整数N。按下来N行,每行包含一个操作指令,操作指令为“I x”,“PM”,“DM”,“D k”或“C k x”中的一种。

输出格式

对于每个输出指令“PM”,输出一个结果,表示当前集合中的最小值。每个结果占一行。

数据范国:1 <= N <= 10^5

输入样例:8    I -10    PM    I -10    D 1    C 2 8    I 6    PM    DM

输出样例:-10    6

7.4 代码

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

const int N =100010;
int h[N],size;
int ph[N]; //存储第k个元素插入的下标 
int hp[N]; //存储堆内第k个点的插入位序

void heap_swap(int a,int b){
	swap(ph[hp[a]],ph[hp[b]]);
	swap(hp[a],hp[b]);
	swap(h[a],h[b]);
} 
 
void down(int u){
	int t = u; //用t存3个点中的最小值的下标 
	if(u*2<=size && h[u*2]<h[t]) //左儿子存在并且其值小于h[t] 
		t = u*2; //等于左儿子
	if(u*2+1<=size && h[u*2+1]<h[t])
		t = u*2+1; 
	if(u != t){ //根节点u不是最小值
		heap_swap(u,t); //与叶节点t交换
		down(t); //再排序t的位置,使得满足“小根堆性质” 
	}
}

void up(int u){
	while(u/2 && h[u/2]>h[u]){ //父节点非0且其值小于叶节点 
		heap_swap(u/2,u); //交换两数 
		u /= 2; //值更新为父节点 
	}
}

int main(){
	int n,m = 0;
	scanf("%d",&n);
	while(n --){
		string op;
		cin >> op;
		int k,x;
		if(op == "I"){ //插入一个数 
			scanf("%d",&x);
			size ++;
			m ++;
			ph[m] = size; //第m个插入的元素在堆内的位置
			hp[size] = m; //堆内元素的插入位序 
			h[size] = x; 
			up(size); //向上对堆排序 
		}
		else if(op == "PM") //输出当前集合中的最小值 
			printf("%d\n",h[1]);
		else if(op == "DM"){ //删除当前集合中的最小值
			heap_swap(1,size);
			size --;
			down(1);
		}
		else if(op == "D"){ //删除第k个插入的数
			scanf("%d",&k);
			k = ph[k];
			heap_swap(k,size);
			size --;
			down(k),up(k); //最多执行一个函数 
		}
		else{ //修改第k个插入的数
			scanf("%d%d",&k,&x);
			k = ph[k]; //找到其对应到堆内的下标 
			h[k] = x; //直接修改其值 
			down(k),up(k); //排序保持堆的有序性 
		} 
	}
	return 0;
}

八、Hash

8.1 Hash表

Hash表又称为散列表,一般由Hash函数(散列函数)与链表结构共同实现。

与离散化思想类似,当我们要对若干复杂信息进行统计时,可以用Hash函数把这些复杂信息映射到一个容易维护的值域内。因为值域变简单、范围变小,有可能造成两个不同的原始信息被Hash函数映射为相同的值,所以需要处理这种冲突情况。

有一种称为“开散列”的解决方案是,建立一个邻接表结构,以Hash函数的值域作为表头数组head,映射后的值相同的原始信息被分到同一类,构成一个链表接在对应的表头之后,链表的节点上可以保存原始信息和一些统计数据。

8.1.1 题目:模拟散列表

维护一个集合,支持如下几种操作:
1.“I x”,插入一个数x;
2.“Q x”,询问数x是否在集合中出现过;
现在要进行N次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数N,表示操作数量。接下来N行,每行包会一个操作指令,操作指令为“I x”,“Q x”中的一种。

输出格式

对于每个询问指令“Qx”,输出一个询问结果,如果x在集合中出现过,则输出“Yes”,否则输出“No”。每个结果占一行。

数据范围:1 <= N <= 10^5,-10^9 <= z <= 10^9

输入样例:5    I 1    I 2    I 3    Q 4    Q 5

输出样例:Yes    No

8.1.2 代码

在dev-c++中不能使用cstring,因为它没有MFC库,故以下代码都没有运行。

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

//选一个比较大的质数取模,冲突概率最小
const int N = 100003; //大于100000的最小质数  
int h[N],e[N],ne[N],idx; //数组含义与单链表的相同 

//拉链法
 
void insert(int x){
	int k = (x%N + N) % N; //使负的余数变正数 
	e[idx] = x; //存储值 
	ne[idx] = h[k]; //新点的指针指向h[k](存的第一个链表节点的下标) 
	h[k] = idx ++; //指向新插入的点 
}

bool find(int x){
	int k = (x%N + N) % N; //映射到0~N,C++中负数模上N是负数 
	for(int i=h[k];i != -1;i = ne[i])
		if(e[i] == x)
			return true; 
	return false;
}  

int main(){
	int n;
	scanf("%d",&n);
	memset(h,-1,sizeof h); //空指针用-1表示 
	while(n --){
		string op;
		int x;
		cin >> op >> x;
		if(op == "I")
			insert(x);
		else{
			if(find(x))
				puts("Yes");
			else
				puts("No");
		}
	}
	return 0;
}
#include <cstring>
#include <iostream>
using namespace std;

const int N = 200003; //大于200000(得要是输入数据数量的两倍)的最小质数 
const int null = 0x3f3f3f3f; //该数大于10^9 
int h[N];

//开放寻址法

int find(int x){
	int k = (x%N + N) % N; //映射到0~N,C++中负数模上N是负数 
    while (h[k] != null && h[k] != x){
        k ++ ; //往下走 
        if (k == N)
			k = 0; //从头开始循环找 
    }
    return k; //k为下标或者应该存储的位置	
}  

int main(){
	int n;
	scanf("%d",&n);
	//h是int类型数组,四个字节,memset按字节
	memset(h,0x3f,sizeof h); 
	//每个字节都是0x3f,每个数就是0x3f3f3f3f 
	while(n --){
		string op;
		int x;
		cin >> op >> x;
		int k = find(x);
		if(op == "I")
			h[k] = x;
		else{
			if(h[k] != null)
				puts("Yes");
			else
				puts("No");
		}
	}
	return 0;
}

8.2 字符串Hash

字符串Hash函数把一个任意长度的字符串映射成一个非负整数,并且其冲突概率几乎为0。 

取一固定值P,把字符串看作P进制数,并分配一个大于0的数值,代表每种字符。一般来说,我们分配的数值都远小于P。例如,对于小写字母构成的字符串,可以令 a = 1,b = 2,…,z = 26。取一固定值M,求出该P进制数对M的余数,作为该字符串的Hash值。


一般来说,我们取P=131或P=13331,此时Hash值产生冲突的概率极低,只要Hash值相同,我们就可以认为原字符串是相等的。通常我们取 M=2⁶⁴,即直接使用unsigned long long类型存储这个Hash值,在计算时不处理算术溢出问题,产生溢出时相当于自动对2⁶⁴
取模,这样可以避免低效的取模(mod)运算。

8.2.1 题目:字符串哈希

给定一个长度为n的字符串,再给定m个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1] 和 [l2,r2] 这两个区间所包含的字符串子串是否完全相同。字符串中只包含大小写英文字母和数字。

输入格式

第一行包含整数n和m,表示字符串长度和询问次数。笫二行包含一个长度为n的字符串,字符串中只包含大小写英文字母和数字。接下来m行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。注意,字符串的位置从1开始编号。

输出格式

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出“Yes”,否则输出“No”。

数据范围:1 <= n, m <= 10^5

输入样例:8 3    aabbaabb    1 3 5 7    1 3 6 8    1 2 1 2

输出样例:Yes    No    Yes

8.2.2 代码

#include <iostream>
using namespace std;

typedef unsigned long long ULL;
const int N = 100010;
const int P = 131;
int n,m;
char str[N];
int h[N],p[N]; //h数组存储前k个字符的hash值,p数组存储P的多少次方 

ULL get(int l,int r){
	return h[r]-h[l-1]*p[r-l+1]; //直接套用公式 
}

int main(){
	scanf("%d%d%s",&n,&m,str+1); //字符串从1开始存储 
	p[0] = 1; //即p的0次方
	for(int i=1;i <= n;i ++){
		p[i] = p[i-1] * P;
		h[i] = h[i-1]*P + str[i]; //自动转换类型,char变int 
	} 
	while(m --){
		int l1,r1,l2,r2;
		scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
		if(get(l1,r1) == get(l2,r2))
			puts("Yes");
		else
			puts("No");
	}
	return 0;
}

九、C++ STL

9.1 vector

vector可理解为变长数组,它的内部实现基于倍增思想。

vector支持随机访问,即对于任意的下标 0 ≤ i < n,可以像数组一样用 [ i ] 取值。但它不是链表,不支持在任意位置 0(1) 插入。为了保证效率,元素的增删一般应该在末尾进行。

支持比较运算,按字典序排序。

vector<int> a(4,3),b(3,4);
if(a < b) 
    puts("a < b");

9.1.1 声明 

#include<vector> //头文件
vector<int>a; //相当于一个长度动态变化的int数组
vector<int>b(n); //定义一个长度为n的int数组
vector<int>c(n,x); //定义一个长度为n且值均为x的int数组
//相当于第一维长233,第二维长度动态变化的int数组,
vector<int>b[233]; //即定义了233个int数组
struct rec{…};
vector<rec>c; //自定义的结构体类型也可以保存在vector中

9.1.2 函数

① size / empty

size函数返回vector的实际长度(包含的元素个数),empty函数返回一个bool类型,表明vector是否为空。二者的时间复杂度都是 O(1)。

所有的STL容器都支持这两个方法,含义也都相同。 

② clear

clear函数把vector清空。

③ begin / end

begin函数返回指向vector中第一个元素的迭代器。例如a是一个非空的vector,则*a. begin()与a[0]的作用相同。


所有的容器都可以视作一个“前闭后开”的结构,end函数返回vector的尾部,即第n个元素再往后的“边界”。*a. end()与a[n]都是越界访问,其中n=a. size()。

//下面三份代码都遍历了vector<int>a,并输出它的所有元素。
for (int i = 0;i < a.size();i ++)
    cout << a[i] << endl;

for (vector<int>::iterator it = a.begin();it != a.end();it ++)
    cout << *it << endl;

for (auto it = a.begin();it != a.end();it ++) //auto自动判断变量类型
    cout << *it << endl;

for(auto x : a)
    cout << x << endl;

④ front / back

front函数返回vector的第一个元素,等价于*a. begin()和a[0]。
back函数返回vector的最后一个元素,等价于*--a. end()和a[a. size()-1]。

⑤ push_back / pop_back

a.push_back(x) 把元素x插入到vector a的尾部。

a.pop_back() 删除vector a的最后一个元素。

9.1.3 迭代器

迭代器就像STL容器的“指针”,可以用星号 “*” 操作符解除引用。
一个保存int的vector的迭代器声明方法为:vector<int>::iterator it;

vector的迭代器是“随机访问迭代器”,可以把vector的迭代器与一个整数相加减,其行为和指针的移动类似。可以把vector的两个迭代器相减,其结果也和指针相减类似,得到两个迭代器对应下标之间的距离。

9.2 queue

头文件queue主要包括循环队列queue和优先队列priority_queue两个容器。

9.2.1 声明

queue<int> q;
q = queue<int>(); //相当于是vector的clear函数
struct rec{…};
queue<rec> q;
priority_queue<int> q;
priority_queue< pair<int,int> > q;

pair<>是C++内置的二元组,尖括号中分别指定二元组的第一元、第二元的类型。可以用make_pair 函数创建二元组,用成员变量first访问第一元、second访问第二元。在比较大小时,以第一元为第一关键字、第二元为第二关键字(字典序)。

//构造及初始化
pair<int,string> p;
p = make_pair(10,"abc");
p = {20,"abc");

9.2.2  循环队列 queue

方法描述示例时间复杂度
push入队(从队尾) q.push(element); 

O(1)

pop出队(从队头)q.pop();O(1)
front队头元素int x =q. front();O(1)
back队尾元素int y =q. back();O(1)

9.2.3 优先队列 priority_queue

priority_queue 可理解为一个大根二叉堆,默认是大根堆。

方法 描述示例时间复杂度
push把元素插入堆q.push(x);O(logn)
pop删除堆顶元素q.pop();O(logn)
top查询堆顶元素(最大值)int x=q.top();O(1)

priority_queue 实现小根二叉堆。对于int等内置数值类型,可以把要插入的元素的相反数放入堆中。等从堆中取出元素时,再把它取相反数变回原来的元素。这样就相当于把小的放在堆顶。

priority_queue<int> heap;
heap.push(-x);

9.3 deque

双端队列deque是一个支持在两端高效插入或删除元素的连续线性存储空间。它就像是vector和queue的结合。与vector相比,deque在头部增删元素仅需要O(1)的时间,与queue相比,deque像数组一样支持随机访问。

方法描述示例时间复杂度
[ ]随机访问与vector类似O(1)
begin / enddeque的头/尾迭代器与vector迭代器类似O(1)

front / back

队头 / 队尾元素与queue类似O(1)

push_back

从队尾入队q.push_back(x);O(1)
push_front从队头入队q.push_front(y);O(1)
push_front从队头出队q.push_front();O(1)
pop_back从队尾出队q.pop_back();O(1)
clear清空队列q.clear();O(n)

9.4 set

头文件set主要包括set和multiset两个容器,分别是“有序集合”和“有序多重集”,即前者的元素不能重复,而后者可以包含若干个相等的元素。set和multiset的内部实现是一棵红黑树(平衡树的一种),它们支持的函数基本相同。

9.4.1 声明

set<int> s;
struct rec{…};
set<rec> s;
multiset<double> s;
//与优先队列一样,set和multiset存储的元素必须定义“小于号”运算符

9.4.2 函数

① size / empty / clear

与vector类似,分别为元素个数,是否为空、清空。前两者的时间复杂度为O(1)。

② begin / end

返回集合的首、尾迭代器,时间复杂度为O(1)。

s.begin()是指向集合中最小元素的迭代器。

s.end()是指向集合中最大元素的下一个位置的迭代器。换言之,就像vector一样,是一个“前闭后开”的形式。因此--s.end()是指向集合中最大元素的迭代器。

③ insert

s.insert(x)把一个元素x插入到集合s中,时间复杂度为O(logn)。

在set中,若元素已存在,则不会重复插入该元素,对集合的状态无影响。

④ find

s.find(x)在集合s中查找等于x的元素,并返回指向该元素的迭代器。若不存在,则返回s.end()。时间复杂度为O(logn)。

⑤ lower_bound / upper_bound

这两个函数的用法与find类似,但查找的条件略有不同,时间复杂度O(logn)。

s.lower_bound(x)查找>=x的元素的最小的一个,并返回指向该元素的迭代器。

s.upper_bound(x)查找>x的元素中最小的一个,并返回指向该元素的迭代器。

⑥ erase

设it是一个迭代器,s.erase(it)从s中删除迭代器it指向的元素,时间复杂度为O(logn)。

设x是一个元素,s.erase(x)从s中删除所有等于x的元素,时间复杂度为O(k+logn),其中k为被删除的元素个数。

⑦ count

s.count(x)返回集合s中等于x的元素个数,时间复杂度为O(k+logn),k为元素x的个数。

9.4.3 迭代器

set和multiset的迭代器称为“双向访问迭代器”,不支持“随机访问”,支持星号(*)解除引用,仅支持“++”和“--”两个与算术相关的操作。

设 it 是一个迭代器,例如 set<int> : : iterator it;若把 it++,则 it 将会指向“下一个”元素。这里的“下一个”是指在元素从小到大排序的结果中,排在 it 下一名的元素。同理,若把 it--,则 it 将会指向排在“上一个”的元素。

请注意,执行“++”和“--”操作的时间复杂度都是O(logn)。执行操作前后,务必仔细检查,避免迭代器指向的位置超出首、尾迭代器之间的范围。

9.5 map

map容器是一个键值对key-value的映射。

其内部实现是一棵以key为关键码的红黑树。map的key和value可以任意类型,其中key必须定义“小于号”运算符。

9.5.1 声明

声明方法为:map<key_type,value_type> name;

map<long long,bool> vis;
map<string,int> hash;
map< pair<int,int>,vector<int> > test;

在很多时候,map容器被当作Hash表使用,建立从复杂信息key(如字符串)到简单信息value(如一定范围内的整数)的映射。

map<string,int> a;
a["abc"] = 1; //[]的时间复杂度是O(logn)
cout << a["abc"]; //输出1

9.5.2 函数

① size / empty / clear / begin / end

与set类似,分别为元素个数、是否为空、清空、首迭代器、尾迭代器。

② insert / erase 

与set类似,分别为插入、删除。insert的参数是pair<key_type,value_type>,erase的参数可以是key或者迭代器。

map<int,int> h;
h.insert(make_pair(1,2)),h.insert(make_pair(2,3));
map<int,int>::iterator it = h.begin();
pair<int,int> p = *it;
h.erase(it),h.erase(2);
cout << p.first << ' ' << p.second << endl;

③ find

 h.find(x)在变量名为h的map中查找key为x的二元组,并返回指向该二元组的迭代器。若不存在,返回h.end()。时间复杂度为O(logn)。

9.5.3 迭代器

map的迭代器与set一样,也是“双向访问迭代器”。对map的迭代器解除引用后,将得到一个二元组pair<key_type,value_type>。

9.6 algorithm

9.6.1 reverse 翻转

//翻转一个vector
reverse(a.begin(),a.end());
//翻转一个数组,元素存放在下标1~n
reverse(a+1,a+n+1);

9.6.2 unique 去重

 返回去重之后的尾迭代器(或指针),仍然为前闭后开,即这个尾迭代器是去重之后末尾元素的下一个位置,该函数常用于离散化,利用迭代器(或指针)的减法,可计算出去重后的元素个数m。

//把一个vector去重
int m = unique(a.begin(),a.end())-a.begin();
//把一个数组去重,元素存放在下标1~n
int m = unique(a+1,1+n+1) - (a+1);

9.6.3 random_shuffle 随机打乱

用法与reverse相同。

9.6.4 next_permutation 下一个排序

把两个迭代器(或指针)指定的部分看作一个排列,求出这些元素构成的全排列中,字典序排在下一个的排列,并直接在序列上更新。另外,若不存在排名更靠后的排列,则返回 false,否则返回 true 。同理,也有 prev_permutation 函数。

//下面的程序按字典序输出1~n的n!种全排列:
for(inti=1;i<=n;i++)
    a[i]=i
do {
    for ( int i = 1;i < n;i++) 
        cout < < a[i] << ‘ ‘;
    cout << a[n] << endl;
} while(next_permutation(a+1,a+n+1));

9.6.5 sort 快速排序

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

//把一个int数组(元素存放在下标1~n)从大到小排序,传入比较函数
inta[MAX_SIZE];
bool cmp(int a,int b){return a >b;}
sort(a+1,a+n+1,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());

9.6.6 lower_bound/ upper_bound 二分

lower_bound 的第三个参数传入一个元素x,在两个迭代器(或指针)指定的部分上执行二分查找,返回指向第一个大于等于x的元素的位置的迭代器(或指针)。
upper_bound 的用法和 lower_bound 大致相同,唯一的区别是查找第一个大于x 的元素。当然,两个迭代器(或指针)指定的部分应该是提前排好序的。
在有序int数组(元素存放在下标1~n)中查找大于等于x的最小整数的下标:
int i= lower_bound(a+1,a+n+1,x)-a;
在有序vector<int>中查找小于等于x的最大整数(假设一定存在):
int y=*-- upper_bound(a. begin( ),a. end( ),x);

9.7 cstring

9.7.1 substr 返回子串

str.substr(x,y); //x是子串起始位置,y是子串长度

string a = "abc";
cout << a.substr(1); //输出abc
cout << a,substr(1,2) //输出bc
cout << a.substr(1,10); //输出abc

9.7.2 c_str 转化为字符串数组

c_str()就是将C++的string转化为C的字符串数组,c_str()生成一个const char * 指针,指向字符串的首地址。

string str = "qwe";
printf("%s\n",str.c_str());

9.7.3 函数

string str = "abc";
str.size();
str.length(); 
str.empty();
str.clear();
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值