week2 数据结构
链表
算法模板
单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
void insert(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}
// 将头结点删除,需要保证头结点存在
void remove()
{
head = ne[head];
}
双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
例子
example 1 : 单链表
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 k k k 个插入的数后面的数;
- 在第 k k k 个插入的数后插入一个数。
现在要对该链表进行 M M M 次操作,进行完所有操作后,从头到尾输出整个链表。
#include <iostream>
using namespace std;
const int N = 100010;
// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 将x插到头结点
void add_to_head(int x)
{
e[idx] = x, ne[idx] = head, head = idx ++ ;
}
// 将x插到下标是k的点后面
void add(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = 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')
{
cin >> x;
add_to_head(x);
}
else if (op == 'D')
{
cin >> k;
if (!k) head = ne[head];
else remove(k - 1);
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
example 2 : 双链表
实现一个双链表,双链表初始为空,支持 5 5 5 种操作:
- 在最左侧插入一个数;
- 在最右侧插入一个数;
- 将第 k k k 个插入的数删除;
- 在第 k k k 个插入的数左侧插入一个数;
- 在第 k k k 个插入的数右侧插入一个数
现在要对该链表进行 M M M 次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第 k k k 个插入的数并不是指当前链表的第 k k k 个数。
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
// 在节点a的右边插入一个数x
void insert(int a, int x)
{
e[idx] = x;
l[idx] = a, r[idx] = r[a];
l[r[a]] = idx, r[a] = idx ++ ;
}
// 删除节点a
void remove(int a)
{
l[r[a]] = l[a];
r[l[a]] = r[a];
}
int main()
{
cin >> m;
// 0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
while (m -- )
{
string op;
cin >> op;
int k, x;
if (op == "L")
{
cin >> x;
insert(0, x);
}
else if (op == "R")
{
cin >> x;
insert(l[1], x);
}
else if (op == "D")
{
cin >> k;
remove(k + 1);
}
else if (op == "IL")
{
cin >> k >> x;
insert(l[k + 1], x);
}
else
{
cin >> k >> x;
insert(k + 1, x);
}
}
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
cout << endl;
return 0;
}
栈
算法思想
先进后出
算法模板
// tt表示栈顶
int stk[N], tt = 0;
// 向栈顶插入一个数
stk[ ++ tt] = x;
// 从栈顶弹出一个数
tt -- ;
// 栈顶的值
stk[tt];
// 判断栈是否为空
if (tt > 0)
{
}
例子
example 1 : 模拟栈
实现一个栈,栈初始为空,支持四种操作:
- push x – 向栈顶插入一个数 x x x;
- pop – 从栈顶弹出一个数;
- empty – 判断栈是否为空;
- query – 查询栈顶元素。
现在要对栈进行 M M M 个操作,其中的每个操作 3 3 3和操作 4 4 4 都要输出相应的结果。
#include<iostream>
#include<string.h>
using namespace std;
const int N=100010;
int stk[N],tt;
int n;
int main()
{
cin>>n;
while(n--)
{
string op;
int x;
cin>>op;
if(op=="push")
{
cin>>x;
stk[++tt]=x;
}
else if(op=="query")
{
cout<<stk[tt]<<endl;
}
else if(op=="pop")
{
tt--;
}
else if(op=="empty")
{
if(tt>0)
cout<<"NO"<<endl;
else
cout<<"YES"<<endl;
}
}
}
队列
算法思想
先进先出
算法模板
熟练掌握:能够非常快的默写出来
-
普通队列
// hh 表示队头,tt表示队尾 int q[N], hh = 0, tt = -1; // 向队尾插入一个数 q[ ++ tt] = x; // 从队头弹出一个数 hh ++ ; // 队头的值 q[hh]; // 判断队列是否为空 if (hh <= tt) { }
-
循环队列
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;
// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;
// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh != tt)
{
}
例子
example 1 : 模拟队列
实现一个队列,队列初始为空,支持四种操作:
- push x – 向队尾插入一个数 x x x;
- pop – 从队头弹出一个数;
- empty – 判断队列是否为空;
- query – 查询队头元素。
现在要对队列进行 M M M 个操作,其中的每个操作 3 3 3 和操作 4 4 4 都要输出相应的结果。
#include<iostream>
#include<string.h>
using namespace std;
const int N=100010;
int q[N],hh=0,tt=-1;//hh为队头,tt为队尾
int n;
int main()
{
cin>>n;
while(n--)
{
string op;
int x;
cin>>op;
if(op=="push")
{
cin>>x;
q[++tt]=x;,
}
else if(op=="query")
{
cout<<q[hh]<<endl;
}
else if(op=="pop")
{
hh++;
}
else if(op=="empty")
{
if(hh-1==tt)
cout<<"YES"<<endl;
else
cout<<"NO"<<endl;
}
}
return 0;
}
单调栈
算法思想
-
先想朴素算法
-
删掉一些永远也用不到的不必要的值
-
看剩下的元素是是否具有单调性
-
优化(头、尾、二分········)
在单调栈中,每一个元素只有一次入栈与出栈,所以最终的复杂度是 O ( n ) O(n) O(n)
算法模板
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
例子
example 1 : 单调栈
给定一个长度为 N N N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 − 1 −1 −1。
eg : 输入 3 4 2 7 5 输出 -1 3 -1 2 2
把元素一个一个放入栈中,如果后面的元素比前面的元素小,将前面的元素将一直用不到,可以把它从栈中弹出,直到遇到比其小的元素,停止并将其输出,再将后面的元素压入栈中
#include<iostream>
using namespace std;
const int N = 100010;
int stk[N],tt;
int n;
int x;
int main()
{
cin>>n;
while(n--)
{
cin>>x;
while(tt && stk[tt]>=x) tt--;//删除不必然的元素
if(tt==0)
cout<<"-1"<<" ";
else//利用单调性输出
cout<<stk[tt]<<" ";
stk[++tt]=x;
}
return 0;
}
单调队列
算法思想
与单调栈相同
- 先想朴素算法
- 删掉一些永远也用不到的不必要的值
- 看剩下的元素是是否具有单调性
- 优化(头、尾、二分········)
算法模板
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
q[ ++ tt] = i;
}
例子
example 1 : 滑动窗口
给定一个大小为 n ≤ 1 0 6 n≤10^6 n≤106 的数组。
有一个大小为 k k k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k k k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7]
,
k
k
k 为
3
3
3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=1000100;
int n,k;
int a[N],q[N];
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]) hh++;
//如果新的数比队尾还小,那旧数就可以删掉了,只要新数还在,旧数就不可能输出
while(hh<=tt&&a[q[tt]]>=a[i]) tt--;
//把新数放进队列,形成单调递增的队列
q[++tt]=i;
//只要i过了窗口大小就可以输出了
if(i>=k-1) printf("%d ",a[q[hh]]);
}
printf("\n");
//下面求对窗口最大数,同理,形成单调递减队列
hh=0,tt=-1;
for(int i=0;i<n;i++){
if(hh<=tt&&i-k+1>q[hh]) hh++;
while(hh<=tt&&a[q[tt]]<=a[i]) tt--;
q[++tt]=i;
if(i>=k-1) printf("%d ",a[q[hh]]);
}
printf("\n");
return 0;
}
Trie 树
算法思想
Tire 树又称字典树,高效地存储和查找字符串集合的数据结构
算法模板
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点 -----数组模拟的指针
// cnt[]存储以当前节点结尾的单词数量
// idx 当前使用的下标
// 插入一个字符串
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
cnt[p] ++ ;
}
// 查询字符串出现的次数
int query(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
例子
example 1 : Tire 字符串统计
维护一个字符串集合,支持两种操作:
- I x 向集合中插入一个字符串 x x x;
- Q x询问一个字符串在集合中出现了多少次。
共有 N N N 个操作,输入的字符串总长度不超过 105 105 105,字符串仅包含小写英文字母。
#include<iostream>
#include<string.h>
using namespace std;
const int N = 100010;
int son[N][26],cnt[N],idx;
int n;
void insert(string str)
{
int p=0;
for(int i=0;str[i];i++)
{
int u = str[i]-'a';
if(son[p][u]==0) son[p][u]=++idx;
p=son[p][u];
}
cnt[p]++;
}
int query(string str)
{
int p=0;
for(int i=0;str[i];i++)
{
int u=str[i]-'a';
if(son[p][u]==0)
return 0;
else
p=son[p][u];
}
return cnt[p];
}
int main()
{
cin>>n;
while(n--)
{
char op;
string str;
cin>>op;
if(op=='I')
{
cin>>str;
insert(str);
}
else if(op=='Q')
{
cin>>str;
cout<<query(str)<<endl;
}
}
return 0;
}
并查集
算法思想
- 将两个集合合并
- 询问两个元素是否在一个集合当中
优化后的算法复杂度接近 O ( 1 ) O(1) O(1)
基本原理
每个集合用一棵树来表示,树根的编号就是整个集合的编号,每个节点存储它的父节点,p[x]表示x的父节点
问题1:如何判断树根:if(p[x]==x)
问题2:如何求 x 的编号:while(p[x]!=x) x=p[x]
路径压缩:把路径上所有的节点都指向根节点
问题3:如何合并两个集合:px是x的集合编号,py是y的集合编号 p[x]=y
代码模板
1、朴素并查集
int p[N]; //存储每个点的祖宗节点
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
2、维护size的并查集
int p[N], size[N];
//p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
size[i] = 1;
}
// 合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
例子
example 1 : 合并集合
一共有 n n n 个数,编号是 1 ∼ n 1∼n 1∼n,最开始每个数各自在一个集合中。
现在要进行 m m m 个操作,操作共有两种:
- M a b,将编号为 a a a 和 b b b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
- Q a b,询问编号为 a a a 和 b b b 的两个数是否在同一个集合中;
#include<iostream>
using namespace std;
const int N= 100010;
int p[N];
int n,m;
int find(int x)
{
if(x!=p[x]) p[x]=find(p[x]);
return p[x];
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
{
p[i]=i;
}
while(m--)
{
char op;
cin>>op;
if(op=='M')
{
int a,b;
cin>>a>>b;
p[find(a)]=find(b);
}
else if(op=='Q')
{
int a,b;
cin>>a>>b;
if(find(a)==find(b))
{
cout<<"Yes"<<endl;
}
else
{
cout<<"No"<<endl;
}
}
}
return 0;
}
example 2 : 连通块中点的个数
给定一个包含 n n n 个点(编号为 1 ∼ n 1∼n 1∼n)的无向图,初始时图中没有边。
现在要进行 m m m 个操作,操作共有三种:
- C a b,在点 a a a和点 b b b 之间连一条边, a a a 和 b b b 可能相等;
- Q1 a b,询问点 a a a和点 b b b 是否在同一个连通块中, a a a 和 b b b 可能相等;
- Q2 a,询问点 a a a 所在连通块中点的数量;
#include<iostream>
#include<string.h>
using namespace std;
const int N=100010;
int n,m;
int p[N];
int s[N];
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
p[i] = i;
s[i] = 1;
}
while(m--)
{
string op;
cin>>op;
int a,b;
if(op=="C")
{
cin>>a>>b;
if(find(a)==find(b))
continue;
s[find(b)] += s[find(a)];
p[find(a)] = find(b);
}
else if(op=="Q1")
{
cin>>a>>b;
if(find(a)==find(b))
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
else if(op=="Q2")
{
cin>>a;
cout<<s[find(a)]<<endl;
}
}
return 0;
}
堆
算法思想
用来维护一个集合,可以快速地取得最大值或最小值
代码模板
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[N], ph[N], hp[N], size;
// 交换两个点,及其映射关系
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;
if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
例子
example 1 : 堆排序
输入一个长度为 n n n 的整数数列,从小到大输出前 m m m 小的数。
#include<iostream>
using namespace std;
const int N = 100010;
int n,m;
int h[N];
int length;
void down(int u)
{
int t = u;
if (u * 2 <= length && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= length && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if(t!=u)
{
swap(h[t],h[u]);
down(t);
}
}
void up(int u)
{
while(u>0&&h[u]<h[u/2])
{
swap(h[u],h[u/2]);
u=u/2;
}
}
void makeheap()
{
for(int i=n/2;i>0;i--)
{
down(i);
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>h[i];
}
length=n;
makeheap();
while(m--)
{
cout<<h[1]<<" ";
h[1]=h[length];
down(1);
length--;
}
return 0;
}
哈希表(散列表)
算法思想
把一个大空间映射到小空间
代码模板
-
拉链法
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] = idx ++ ; } // 在哈希表中查询某个数是否存在 bool find(int x) { int k = (x % N + N) % N; for (int i = h[k]; i != -1; i = ne[i]) if (e[i] == x) return true; return false; }
-
开放寻址法
int h[N];
// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int find(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}
例子
example 1 : 模拟散列表
维护一个集合,支持如下几种操作:
- I x,插入一个数 x x x;
- Q x,询问数 x x x 是否在集合中出现过;
现在要进行 N N N 次操作,对于每个询问操作输出对应的结果。
1、拉链法
#include<iostream>
#include<cstring>
using namespace std;
const int N=100003;
int h[N];
int e[N],ne[N],idx;
int n;
void insert(int x)
{
int k=(x%N+N)%N;
e[idx]=x;
ne[idx]=h[k];
h[k]=idx++;
}
bool query(int x)
{
int k=(x%N+N)%N;
for(int i=h[k];i!=-1;i=ne[i])
{
if(e[i]==x)
return true;
}
return false;
}
int main()
{
cin>>n;
memset(h,-1,sizeof h);
while(n--)
{
char op;
cin>>op;
if(op=='I')
{
int a;
cin>>a;
insert(a);
}
else
{
int a;
cin>>a;
if(query(a)==true)
{
cout<<"Yes"<<endl;
}
else
{
cout<<"No"<<endl;
}
}
}
}
2、开放寻址法
#include<iostream>
#include<cstring>
using namespace std;
const int N=200003,null=0x3f3f3f3f;
int h[N];
int n;
int find(int x)
{
int k=(x%N+N)%N;
while(h[k]!=null&&h[k]!=x)
{
k++;
if(k==N) k=0;
}
return k;
}
int main()
{
cin>>n;
memset(h,null,sizeof h);
while(n--)
{
char op;
cin>>op;
if(op=='I')
{
int a;
cin>>a;
h[find(a)]=a;
}
else
{
int a;
cin>>a;
if (h[find(a)] == null) puts("No");
else puts("Yes");
}
}
}
字符串哈希
算法思想
主要内容为前缀哈希法。把一个字符串看成一个 P P P进制的数,每一个字符就是该 P P P进制数的某一位
特点:1、不能映射为 0 0 0
2、不存在冲突( P P P取 131 131 131, Q Q Q取 2 64 2^{64} 264)
可以利用前缀哈希算法算出所有子串的哈希值,从而可以在 O ( 1 ) O(1) O(1)时间内快速判断两个字符串是否相等。
代码模板
//核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
//小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}
// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
例子
example 1 : 字符串哈希
给定一个长度为 n n n 的字符串,再给定 m m m个询问,每个询问包含四个整数 l 1 , r 1 , l 2 , r 2 l_1,r_1,l_2,r_2 l1,r1,l2,r2,请你判断 [ l 1 , r 1 ] [l1,r1] [l1,r1] 和 [ l 2 , r 2 ] [l2,r2] [l2,r2]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
#include<iostream>
#include<string.h>
using namespace std;
typedef unsigned long long ULL;
const int N=100010,P=131;
int n,m;
char str[N];
ULL h[N],p[N];
ULL get(int l,int r)
{
return h[r]-h[l-1]*p[r-l+1];
}
int main()
{
cin>>n>>m;
p[0]=1;
for(int i=1;i<=n;i++)
{
cin>>str[i];
p[i]=p[i-1]*P;
h[i]=h[i-1]*P+str[i];
}
while(m--)
{
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(get(l1,r1)==get(l2,r2))
cout<<"Yes"<<endl;
else
cout<<"No"<<endl;
}
return 0;
}