单链表
单链表主要用来实现邻接表以存储树和图,算法题中我们常用数组来模拟链表,实现方式为用两个数组来存值和下一个点,指向下一个点则通过下标来实现
#include <iostream>
#include <cstring>
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()
{
init();
int n;
cin >> n;
while (n -- )
{
char op;
cin >> op;
int k, x;
if(op == 'H')
{
cin >> x;
add_to_head(x);
}
else if(op == 'I')
{
cin >> k >> x;
add(k - 1, x);
}
else
{
cin >> k;
if(!k) head = ne[head];
else remove(k - 1);
}
}
for(int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
return 0;
}
双链表
与单链表相比我们需要存储节点的左右节点
链表插入:
删除某个节点:
#include <iostream>
using namespace std;
const int N = 100010;
int e[N], l[N], r[N], idx;
//初始化
void init()
{
r[0] = 1;
l[1] = 0;
idx = 2;
}
//在下标是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;
idx ++;
}
//删除第k个点
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main()
{
init();
int n;
cin >> n;
while (n -- )
{
string op;
cin >> op;
int k, x;
if(op == "R")
{
cin >> x;
add(l[1], x);
}
else if(op == "L")
{
cin >> x;
add(0, x);
}
else if(op == "IR")
{
cin >> k >> x;
add(k + 1, x);
}
else if(op == "D")
{
cin >> k;
remove(k + 1);
}
else
{
cin >> k >> x;
add(l[k + 1], x);
}
}
for(int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
return 0;
}
栈
栈是一种先进后出的数据结构,插入和弹出都在栈顶
数组模拟栈:
实现一个栈,栈初始为空,支持四种操作:
1.push x
– 向栈顶插入一个数 x;
2.pop
– 从栈顶弹出一个数;
3.empty
– 判断栈是否为空;
4.query
– 查询栈顶元素。
现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
#include <iostream>
using namespace std;
const int N = 100010;
int stack[N], tt;
void push(int x)
{
stack[++ tt] = x;//插入元素
return;
}
int query()
{
return stack[tt];//返回栈顶元素
}
void pop()
{
tt --;//将栈顶弹出
}
bool empty()
{
return tt;//判断是否为空,tt初始值是0
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
while (n -- )
{
string op;
cin >> op;
if(op == "push")
{
int x;
cin >> x;
push(x);
}
else if(op == "query")
{
cout << query() << endl;
}
else if(op == "pop")
{
pop();
}
else
{
if(empty()) cout << "NO" << endl;
else cout << "YES" << endl;
}
}
return 0;
}
队列
队列是先进先出,最先放进队列的元素在队头最先被弹出,最后放进的元素在队尾
数组模拟队列:
实现一个队列,队列初始为空,支持四种操作:
1.push x
– 向队尾插入一个数 x;
2.pop
– 从队头弹出一个数;
3.empty
– 判断队列是否为空;
4.query
– 查询队头元素。
现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
#include <iostream>
using namespace std;
const int N = 100010;
int q[N];
//hh为队头,tt为队尾
int hh, tt = -1;
void push(int x)
{
q[++ tt] = x;//向队尾插入元素
return;
}
int query()
{
return q[hh];//返回队头元素
}
void pop()
{
hh ++;//弹出队头元素
}
bool empty()
{
return hh <= tt;//判断是否为空
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
while (n -- )
{
string op;
cin >> op;
if(op == "push")
{
int x;
cin >> x;
push(x);
}
else if(op == "query")
{
cout << query() << endl;
}
else if(op == "pop")
{
pop();
}
else
{
if(empty()) cout << "NO" << endl;
else cout << "YES" << endl;
}
}
return 0;
}
单调栈和单调队列常用于优化问题,一般考虑朴素解法使用栈和队列,考虑栈和队列中是否有哪些元素是从来没用过的,再考虑剩下的元素是否有单调性,若具有单调性则极值就会在栈顶或者队头取到,从而将问题优化
单调栈
单调栈常见模型:找出每个数左边离它最近的最大/最小的数
#include <iostream>
using namespace std;
const int N = 100010;
int stack[N];
int q[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
for(int i = 0; i < n; i ++) cin >> q[i];
int tt = 0;
for(int i = 0; i < n; i ++)
{
while(tt && stack[tt] >= q[i]) tt --;
if(!tt) cout << "-1" << ' ';
else cout << stack[tt] << ' ';
stack[++ tt] = q[i];
}
return 0;
}
单调队列
单调队列常见模型:滑动窗口最大/最小值
#include <iostream>
using namespace std;
const int N = 1000010;
int a[N],q[N];
int hh, tt = -1;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, k;
cin >> n >> k;
for(int i = 0; i < n; i ++) cin >> a[i];
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) cout << a[q[hh]] << ' ';
}
cout << endl;
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) cout << a[q[hh]] << ' ';
}
return 0;
}
K M P KMP KMP
朴素的字符串匹配是依次枚举模式串
s
s
s中的每个字符,判断以该字符开头是否可以与模板串
p
p
p依次匹配。我们考虑匹配过程中的某种失败情况和最后成功的结果之间的关系。若两种情况下有重复部分,则以匹配失败的前一个字符的后缀与以该字符结尾的字符串前缀两者应相等,因此我们可以直接从前缀部分开始继续匹配,跳过一定不可能匹配成功的部分。
K
M
P
KMP
KMP字符串模式匹配主要是求模板串的
n
e
x
t
next
next数组,也就是以每个字符结尾的后缀与字符串开头重复部分最长是多少
#include <iostream>
#include <string>
using namespace std;
const int N = 1000010;
char s[N],p[N];
int ne[N];
int main()
{
int n, m;
cin >> n >> p + 1 >> m >> s + 1;
//求next数组
for(int i = 2, j = 0; i <= n; i ++)
{
while(j && p[i] != p[j + 1]) j = ne[j];
if(p[i] == p[j + 1]) j ++;
ne[i] = j;
}
//KMP匹配
for(int i = 1, j = 0; i <= m; i ++)
{
while(j && s[i] != p[j + 1]) j = ne[j];
if(s[i] == p[j + 1]) j ++;
if(j == n)
{
cout << i - n << ' ';
j = ne[j];
}
}
return 0;
}
T i r e Tire Tire
T
i
r
e
Tire
Tire是一种高效的存储和查找字符串集合的数据结构
#include <iostream>
#include <string>
using namespace std;
const int N = 100010;
int son[N][26], cnt[N], idx;//下标是0的点既是根节点,又是空节点
char str[N];
//插入字符串,构造Tire树
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] ++;
}
//在tire树中查找
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;//若没有对应的节点返回0
p = son[p][u];
}
return cnt[p];//返回对应节点的字符串数
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
while(n --)
{
char op;
cin >> op >> str;
if(op == 'I') insert(str);
else cout << query(str) << endl;
}
return 0;
}
并查集
并查集主要用于处理集合之间的问题,比如连通块问题
t
i
p
s
tips
tips:当我们构造完集合时,一定要再调用
f
i
n
d
find
find函数进行路径压缩,将路径上所有点都指向祖宗节点
#include <iostream>
using namespace std;
const int N = 100010;
int p[N];
int find(int x)
{
//找到x的祖宗节点并将路径上的所有点都指向祖宗节点
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++) p[i] = i;
while (m -- )
{
char op;
int a, b;
cin >> op >> a >> b;
if(op == 'M') p[find(a)] = find(b);
else
{
if(find(a) == find(b)) cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
堆
堆是一种满足根节点小于/大于左右儿子节点的完全二叉树
如何手写一个堆?
1.插入一个数
2.求集合当中的最小值
3.删除最小值
4.删除任意一个元素
5.修改任意一个元素
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int h[N], s;
void down(int x)
{
int t = x;
if(2 * x <= s && h[t] > h[2 * x]) t = 2 * x;//与右儿子比较
if(2 * x + 1 <= s && h[t] > h[2 * x + 1]) t = 2 * x + 1;//与左儿子比较
if(t != x)
{
swap(h[x],h[t]);
down(t);
}
}
void up(int x)
{
int t = x;
if(x / 2 && h[x / 2] > h[t]) t = x / 2;//与父节点比较
if(x != t)
{
swap(h[x], h[t]);
up(t);
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> h[i];
s = n;
for(int i = n / 2; i; i --) down(i);
while (m -- )
{
cout << h[1] << ' ';
h[1] = h[s --];
down(1);
}
return 0;
}
哈希表
哈希表可以看成是一种映射,之前的离散化是一种特殊的保序哈希,根据处理冲突的方式不同,可以分为开放寻址法和拉链法
开放寻址法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
int h[N];
//对于x返回其对应的哈希映射之后的位置
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()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
memset(h, 0x3f, sizeof h);
while(n --)
{
char op[2];
int x;
cin >> op >> x;
int k = find(x);
if(op[0] == 'I') h[k] = x;
else
{
if(h[k] != null) cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
拉链法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003;
int h[N];
int e[N], ne[N], idx;
//在对应的位置拉链
int insert(int x)
{
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++;
}
//从当前位置开始遍历链
void query(int x)
{
int k = (x % N + N) % N;
for(int i = h[k]; i != -1; i = ne[i])
{
if(e[i] == x)
{
cout << "Yes" << endl;
return;
}
}
cout << "No" << endl;
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n;
cin >> n;
memset(h, -1, sizeof h);
while(n --)
{
char op[2];
int x;
cin >> op >> x;
if(op[0] == 'I') insert(x);
else
{
query(x);
}
}
return 0;
}
字符串哈希
字符串哈希是将字符串当作一个
p
p
p进制的数,求出所有前缀的哈希值。当需要求出某一子串
[
L
,
R
]
[L,R]
[L,R]的哈希值时,计算
h
[
R
]
−
h
[
L
−
1
]
∗
P
R
−
L
+
1
h[R]-h[L-1]*P^{R-L+1}
h[R]−h[L−1]∗PR−L+1即为子串的哈希值
#include <iostream>
#include <cstring>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
ULL h[N], p[N];
char str[N];
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
cin >> str + 1;
p[0] = 1;
for(int i = 1; i <= n; i ++)
{
p[i] = p[i - 1] * P; //记录P的幂
h[i] = h[i - 1] * P + str[i]; //求前缀哈希值,保证不出现0
}
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;
}