目录
9.6.6 lower_bound/ upper_bound 二分
一、链表
数组是一种支持随机访问,但不支持在任意位置插入或删除元素的数据结构。
链表支持在任意位置插入或删除,但只能按顺序依次访问其中的元素。
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 / end | deque的头/尾迭代器 | 与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();