链表
数组模拟法
单链表
模板题分析
- 单链表需要用到的数组:
head
存储链表头,e[]
存储节点的值,ne[]
存储节点的next指针,idx
表示当前用到了哪个节点 - 初始化
void init(){
head = -1;
idx = 0;
}
- 将x点插到下表是k点后面:
void insert(int k,int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
- 将头结点删除,需要保证头结点存在
void remove(){
head = ne[head];
}
模板题代码
//P30单链表
//链表
/*
struct Node{
int val;
Node *next;
};
*/
//竞赛中最常用的是邻接表
//邻接表用的最多的是 存储图 和 树
//双链表用来优化某些问题
#include <iostream>
using namespace std;
const int N = 100010;
//head表示头结点的下标
//e[i]表示节点i的值
//ne[i]表示节点i的next指针是多少,或者说下一个节点i的地址是什么
//idx存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
//初始化
void init() {
head = -1;
idx = 0;
}
//将x插入到头结点:
//1.将x的next指针指向head节点指向的下一个位置
//2.将head指针删掉,然后指向x节点
void add_to_head(int x) {
e[idx] = x; //把x这个值先存下来
ne[idx] = 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') {
cin >> x;
add_to_head(x);
} else if (op == 'D') {
cin >> k;
if (!k)
head = ne[head];
else
remove(k - 1);//前几次错因:没加else
} else {
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i])
cout << e[i] << ' ';
cout << endl;
return 0;
}
双链表
模板题分析
- 双链表需要用到的数组:
e[]
表示节点的值,l[]
表示节点的左指针,r[]
表示节点的右指针,idx
表示当前用到了哪个节点 - 双链表初始化:
void init() {
//0表示左端点,1表示右端点
r[0] = 1;
l[0] = 0;
idx = 2;//因为0和1已经被占用了
}
- 在第k点的右边插入x:
void add_to_right(int k, int x) {
e[idx] = x;//将x存入当前下标的数值数组中
r[idx] = r[k];//当前下标的后继为k的后继
l[idx] = k;//当前下标的前驱为k
l[r[k]] = idx;//k的后继节点的前驱为idx
r[k] = idx++;//k的后继节点为idx,idx++
}
在第k点的左边插入x,等价于在l[k]的右边插入x,也就是add_to_right(l[k],x)
- 删除第k个点:
void remove(int k) {
r[l[k]] = r[k];//k的前驱的后继为k的后继
l[r[k]] = l[k];//k的后继的前驱为k的前驱
}
模板题代码
//P31双链表
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
//e用来存储数值,
//l[]用来存储前驱(左边数的下标),
//r[]用来存储后继(右边数的下标)
//idx用来记录当前下标
int e[N], l[N], r[N], idx;
//初始化
void init() {
//0表示左端点,1表示右端点
r[0] = 1;
l[0] = 0;
idx = 2;//因为0和1已经被占用了
}
//在第k点的右边插入x
void add_to_right(int k, int x) {
e[idx] = x;//将x存入当前下标的数值数组中
r[idx] = r[k];//当前下标的后继为k的后继
l[idx] = k;//当前下标的前驱为k
l[r[k]] = idx;//k的后继节点的前驱为idx
r[k] = idx++;//k的后继节点为idx,idx++
}
//在k点的左边插入x,等价于在l[k]的右边插入x,也就是add_to_right(l[k],x);
void add_to_left(int k, int x) {
add_to_right(l[k], x);
}
//删除第k个点
void remove(int k) {
r[l[k]] = r[k];//k的前驱的后继为k的后继
l[r[k]] = l[k];//k的后继的前驱为k的前驱
}
//在链表的最左端插入数x
void add_to_head(int x) {
add_to_right(0, x);//在0后面插入x
}
//在链表的最右端插入数x
void add_to_tail(int x) {
add_to_right(l[1], x);
}
int main() {
int m;
init();
cin >> m;
int k, x;
while (m--) {
string op;
cin >> op;
if (op == "L") {
cin >> x;
add_to_head(x);
} else if (op == "R") {
cin >> x;
add_to_tail(x);
} else if (op == "D") {
cin >> k;
remove(k + 1);
} else if (op == "IL") {
cin >> k >> x;
add_to_left(k + 1, x);
} else {
cin >> k >> x;
add_to_right(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];
- 判断栈是否为空:如果
tt > 0
,则表示不为空
模板题分析
模板题代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010;
int stk[N], tt;
//向栈顶插入一个数x
void push(int x) {
stk[++tt] = x;
}
//从栈顶弹出一个数x
void pop() {
tt--;
}
//判断栈是否为空
void empty() {
if (tt > 0)
cout << "NO" << endl;
else
cout << "YES" << endl;
}
//查询栈顶元素
void query() {
cout << stk[tt] << endl;
}
int main() {
int m;
cin >> m;
string op;
while (m--) {
cin >> op;
if (op == "push") {
int x;
cin >> x;
push(x);
} else if (op == "pop") {
pop();
} else if (op == "empty") {
empty();
} else if (op == "query") {
query();
}
}
return 0;
}
单调栈
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
模板题分析
- 当栈里不空,并且栈里元素大于当前这个数时
tt--
- 如果栈里元素不空,输出当前这个数左边第一个比它小的数;如果栈里元素是空的,输出
-1
- 最后把x插到栈里面去
stk[++tt] = x
模板题代码
//P34单调栈
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt;
int main() {
cin.tie(0);
ios::sync_with_stdio(false);
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;//最后把x插到栈里面去
}
return 0;
}
队列
模拟队列
内容
hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;
- 向队尾插入一个数
q[ ++ tt] = x;
- 从队头弹出一个数
hh ++ ;
- 队头的值
q[hh];
- 判断队列是否为空,如果
hh <= tt
,则表示不为空
模板题分析
在队尾插入元素,在队头弹出元素
int q[N],hh,tt = -1;
hh表示对头,tt表示队尾
- 插入
q[++tt] = x;
- 弹出
hh++;
- 判断队列是否为空
if(hh<tt)not empty;
else empty;
- 取出队头元素
q[hh]
模板题代码
#include <iostream>
using namespace std;
const int N = 100010;
int q[N], tt = 0, hh = 1;//别忘了初始化
//1.向队尾插入一个数x
void push(int x) {
q[++tt] = x;
}
//2.从队头弹出一个数
void pop() {
hh++;
}
//3.判断队列是否为空
void empty() {
if (hh <= tt)
cout << "NO" << endl;
else
cout << "YES" << endl;
}
//4.查询队头元素
void query() {
cout << q[hh] << endl;
}
int main() {
int m;
cin >> m;
string op;
while (m--) {
cin >> op;
if (op == "push") {
int x;
cin >> x;
push(x);
} else if (op == "pop") {
pop();
} else if (op == "empty") {
empty();
} else {
query();
}
}
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;
}
模板题代码
//P35滑动窗口
#include <iostream>
using namespace std;
const int N = 1000010;
//a是原数组,q是单调队列
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++) {
//判断队头是否已经滑出窗口
//用下标来判断,队列q里存的是下标
//当前终点是i,起点是i-k+1
if (hh <= tt && i - k + 1 > q[hh])
hh++;//说明hh已经出了窗口了
while (hh <= tt && a[q[tt]] >= a[i])
tt--;
//把当前这个数插到队列里去
q[++tt] = i;
//特判
if (i >= k - 1)
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])
hh++;
while (hh <= tt && a[q[tt]] <= a[i])
tt--;
q[++tt] = i;
if (i >= k - 1)
printf("%d ", a[q[hh]]);
}
puts("");
return 0;
}
KMP
模板题分析
next[i]表示以i为终点的后缀和从1开始的前缀相等,并且后缀的长度最长
i是遍历s的所有字母,想要和s数组匹配的是p[j+1]
模板题代码
#include <iostream>
using namespace std;
const int N = 100010, M = 1000010;
int n, m;
char p[N], s[M];
int ne[N];
int main() {
cin >> n >> p + 1 >> m >> s + 1;//下标都从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匹配过程
//i是遍历s的所有字母
//想要和s数组匹配的是p[j+1]
for (int i = 1, j = 0; i <= m; i++) {//
while (j && s[i] != p[j + 1])//当j没有退回起点,s和p数组不能匹配了
j = ne[j];
if (s[i] == p[j + 1])//如果s与j已经匹配了
j++;//j就可以移到下一个位置
if (j == n) {//说明匹配成功
printf("%d ", i - n);
j = ne[j];
}
}
return 0;
}
Trie树
能快速查找一组字符串
ITCX7121寒假第二周-J Trie字符串统计
模板题分析
字符串插入:
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];
}
模板题代码
//P27Tire字符串统计
#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26], cnt[N], idx;//下标是0的点,既是根节点,又是空节点
//son[N][26]存的是每个节点的所有儿子是什么
//idx是当前所在的节点
char str[N];
//插入操作
void insert(char str[]) {
int p = 0;//从根节点开始
for (int i = 0; str[i]; i++) {//字符串末尾是0
int u = str[i] - 'a';//把小写字母映射成0~25
if (!son[p][u])//如果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];//返回以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;
}
并查集
简介
一. 并查集:
- 将两个集合合并
- 询问两个元素是否在一个集合当中
二. 基本原理:
每个集合用一颗树来表示,树根的编号就是整个集合的编号,每个节点存储它的父节点,p[x]表示x的父节点
三. 问题
4. 如何判断树根:if(p[x]==x)
5. 如何求x的集合编号:while(p[x]!=x)x = p[x]
6. 如何合并两个集合:p[x]是x的集合编号,p[y]是y的集合编号,p[x] = y
优化:压缩路径
模板题1
分析
优化:压缩路径:
int find(int x) { //返回x的祖宗节点+压缩路径
if (p[x] != x)//如果此时这个点x不是根节点
p[x] = find(p[x]);//就让x的父节点等于x的祖宗节点
return p[x];
}
- 合并操作:让a的祖宗节点的父亲等于b的祖宗节点
p[find(a)] = find(b)
- 询问操作:判断
find(a) == find(b)
就行了
代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N];//p表示每个元素它的父节点是谁
int find(int x) { //返回x的祖宗节点+压缩路径
if (p[x] != x)//如果此时这个点x不是根节点
p[x] = find(p[x]);//就让x的父节点等于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 {//询问a,b是否在同一个集合里
if (find(a) == find(b))//如果a的祖宗节点等于b的祖宗节点
puts("Yes");//那么输出Yes
else
puts("No");//否则输出No
}
}
return 0;
}
模板题2
分析
size[]数组:表示每一个集合的大小,每一个集合里元素的个数,就是存储根节点所拥有的子节点数
- 合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
将节点数加到新根节点数上
p[find(a)] = find(b);
a的根节点的根节点等于b的根节点 - 询问点a和点b是否在同一个连通块中:判断
find(a) == find(b)
- 询问点a所在连通块中点的数量:
sizee[find(a)]
代码
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
//p存储每个点
//size表示每一个集合的大小,每一个集合里元素的个数,就是存储根节点所拥有的子节点数
//只保证根节点的size有意义
int p[N], sizee[N];
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;
sizee[i] = 1;//最开始的时候每一个集合里只有自己一个点
}
while (m--) {
char op[3];
int a, b;
scanf("%s", op);
if (op[0] == 'C') {//连边,就是合并
scanf("%d%d", &a, &b);
if (find(a) == find(b))//如果a和b已经在同一个集合里了,就continue跳出
continue;
sizee[find(b)] += sizee[find(a)];//将节点数加到新根节点数上
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", sizee[find(a)]);
}
}
return 0;
}
堆
堆排序
模板题分析
关键写down函数
void down(int u) {
int t = u;//t存的是三个点中的最小的那个节点编号
if (u * 2 <= sizee && h[u * 2] < h[t])//如果左儿子存在,并且左儿子小于父亲h[t]
t = u * 2;//就让t = 左儿子
if (u * 2 + 1 <= sizee && h[u * 2 + 1] < h[t])//如果右儿子存在,并且右儿子小于父亲h[t]
t = u * 2 + 1;//就让t = 右儿子
if (u != t) {//如果根节点u不是三个点中的最小值t,
swap(h[t], h[u]);//就交换他们
down(t);
}
}
模板题代码
//P26堆排序
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N], sizee;//h是heap堆
void down(int u) {
int t = u;//t存的是三个点中的最小的那个节点编号
if (u * 2 <= sizee && h[u * 2] < h[t])//如果左儿子存在,并且左儿子小于父亲h[t]
t = u * 2;//就让t = 左儿子
if (u * 2 + 1 <= sizee && h[u * 2 + 1] < h[t])//如果右儿子存在,并且右儿子小于父亲h[t]
t = u * 2 + 1;//就让t = 右儿子
if (u != t) {//如果根节点u不是三个点中的最小值t,
swap(h[t], h[u]);//就交换他们
down(t);
}
}
//void up(int u) {
// while (u / 2 && h[u / 2] > h[u]) {
// 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]);
sizee = n;
for (int i = n / 2; i; i--)
down(i);
while (m--) {
printf("%d ", h[1]);//每次输出堆顶,也就是最小值
h[1] = h[sizee];//在删除最小值
sizee--;
down(1);
}
return 0;
}
模拟堆
模板题分析
手写一个堆:
- 插入一个数:heap[++size] = x;up(size);
- 求集合当中的最小值:heap[1];
- 删除最小值:heap[1] = heap[size];size–;down(1);
- 删除任意一个元素:heap[k] = heap[size];size–;down(k);up(k);
- 修改任意一个元素:heap[k] = x;down(k);up(k);
模板题代码
//注意:要将size写成sizee
#include <iostream>
#include <algorithm>
#include <string.h>
using namespace std;
const int N = 100010;
//p是下标(pointer),h是堆(heap)
//ph就是从下标映射到堆里面去
//ph[k]存的是第k个插入的数的下标是什么,也就是在堆里面的下标是什么
//hp是从堆里面映射到下标
//hp是堆里面的某一个点是第几个插入的点
int h[N], ph[N], hp[N], sizee;
//在堆里面交换两个元素
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 <= sizee && h[u * 2] < h[t])
t = u * 2;
if (u * 2 + 1 <= sizee && 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 / 2] > h[u]) {
heap_swap(u / 2, u);
u /= 2;
}
}
int main() {
int n, m = 0;
scanf("%d", &n);
while (n--) {
char op[10];
int k, x;
scanf("%s", op);
if (!strcmp(op, "I")) {//1.插入一个数x
scanf("%d", &x);
sizee++;//堆里面多一个元素
m++;//当前是第几个插入的数
//第m个插入的数一开始在堆里面size这个位置,就是最后一个位置
//堆里面size这个数一开始对应的是m
ph[m] = sizee, hp[sizee] = m;
h[sizee] = x;
up(sizee);//生成新堆
} else if (!strcmp(op, "PM"))//2.输出当前集合中的最小值
printf("%d\n", h[1]);//就是堆顶
else if (!strcmp(op, "DM")) {//3.删除当前集合中的最小值
heap_swap(1, sizee);//就是把最后一个元素放到第一个元素的位置上去,堆交换
sizee--;//删掉最后一个元素
down(1);//生成新堆
} else if (!strcmp(op, "D")) {//4.删除第k个插入的数
scanf("%d", &k);
k = ph[k];//让k找到它在堆里面的位置
heap_swap(k, sizee);//删除就是把最后一个元素与k进行堆交换
sizee--;//删掉最后一个元素
down(k), up(k);//生成新堆
} else {//5.修改第k个插入的数,将其变为x
scanf("%d%d", &k, &x);
k = ph[k];//先找到第k个插入的数是啥
h[k] = x;//将堆中第k个插入的数修改为x
down(k), up(k);//生成新堆
}
}
return 0;
}
补充函数知识点
- strcmp函数:
int strcmp(const char*str1,const char *str2);
比较两个字符串的大小
int strcmp(str1,str2);
str1>str2
时,返回大于0的数字
str1=str2
时,返回0
str1<str2
时,返回小于0的数字 - strcat函数:
char *strcat(char*destination,const char *source);
追加拷贝,追加到目标空间后面,目标空间必须足够大,能容下源字符串的内容
通俗来讲就是字符串拼接,将后面那个字符串拼接到前面那个字符串后面 - strcpy函数:
char*strcpy(char*destination,const char*source)
strcpy
是覆盖拷贝,将全覆盖拷贝到destination,会把’\0’也拷过去,且destination的空间必须>=source
的空间 - strlen函数
strlen
函数返回的是在字符串中’\0’前面出现的字符的个数
int strlen(const char*str)
哈希
哈希表
拉链法
分析
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;
}
完整代码
//哈希表
//拉链法
#include <iostream>
#include <cstring>
using namespace std;
/*
做哈希的时候,数组长度或者说mod的数N一般要取成质数,
并且这个质数要尽可能离2的整次幂远,这样取冲突的概率最小
先算一下大于100000的第一个质数是什么,就把N取成刚算的数
#include <iostream>
using namespace std;
int main() {
for (int i = 100000;; i++) {
bool flag = true;
for (int j = 2; j * j <= i; j++) {
if (i % j == 0) {
flag = false;
break;
}
if (flag) {
cout << i << endl;
break;
}
}
}
return 0;
}//结果:i= 100003;
*/
const int N = 100003;
//h数组开个槽,就是创建一个链表h[k]
int h[N], e[N], ne[N], idx;
void insert(int x) {//把x插到h[k]上
//x可能是负数,在C++中负数模N还是负数,x%N+N就一定是一个正数,所以x就要模N加N再模N
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool find(int x) {//在k这个链表h[k]中找一下有没有x
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)//e用来存数值
return true;
return false;
}
int main() {
int n;
scanf("%d", &n);
memset(h, -1, sizeof h);//把槽h[N]清空,空指针用-1来表示
while (n--) {
char op[2];
int x;
scanf("%s%d", op, &x);//尽量用%s读字符
if (op[0] == 'I')//todo
insert(x);
else {
if (find(x))
puts("Yes");
else
puts("No");
}
}
return 0;
}
开放寻址法
分析
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;
}
完整代码
//哈希表
//开放寻址法
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
//0x3f3f3f3f是个大于1e9的数,一定不在x的范围内
int h[N];
//核心操作find
//如果x在哈希表中存在,那么返回x在哈希表中的位置,
//如果x在哈希表中不存在,那么返回x应该在哈希表中存储的位置
int find(int x) {
int k = (x % N + N) % N;
//当前位置不等于空,也就是坑位上有人,并且这个坑位上的人不等于x,那就应该去看下一个坑位
while (h[k] != null && h[k] != x) {
k++;
if (k == N)//说明已经看完了最后一个坑位
k = 0;//就循环看第一个坑位
}
return k;
}
int main() {
int n;
scanf("%d", &n);
//按字节来memset,h是int型数组,有4个字节,每一个字节都是0x3f,每个数就是4个0x3f,
//因此初始化成0x3f
memset(h, 0x3f, sizeof h);//数组初始化,把h数组初始化成0x3f
while (n--) {
char op[2];
int x;
scanf("%s%d", op, &x);
int k = find(x);
if (*op == 'I')
h[k] = x;
else {
if (h[k] != null)
puts("Yes");
else
puts("No");
}
}
return 0;
}
字符串哈希
模板题分析
快速判断两个字符串是否相等,可以用字符串前缀哈希这个做法
核心思想:将字符串看成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];
}
模板题代码
//P28字符串哈希
#include <iostream>
using namespace std;
typedef unsigned long long ULL;
const int N = 100010, P = 131;
//P一般取131或13331,代表P进制
int n, m;
char str[N];
ULL h[N], p[N];
//h数组表示某一个值前缀哈希值
//p数组用来表示乘了p的多少次方
ULL get(int l, int r) { //计算l~r区间的哈希值的公式
return h[r] - h[l - 1] * p[r - l + 1];
}
int main() {
scanf("%d%d%s", &n, &m, str + 1);
p[0] = 1;//表示p的0次方等于1
for (int i = 1; i <= n; i++) {
p[i] = p[i - 1] * P;
h[i] = h[i - 1] * P + str[i]; //字符串前缀哈希值
}
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;
}
kmp做法可以用来求一个字符串的循环节,字符串前缀哈希不可以
其他都可以用字符串前缀哈希
STL简介
vector
变长数组(数组长度可以动态变化) 倍增思想
size()返回元素个数
empty()返回是否为空
clear()清空
front()返回vector第一个数/back()返回vector最后一个数
push_back向vector最后插入一个数/pop_back把vector最后一个数删掉
begin()是vector的第0个数/end()是vector的最后一个数的后面那个数 迭代器
vector支持随机寻址
vector的三种遍历方式:
for(int i = 0;i<a.size();i++)cout<<a[i]<<' ';
for(vector<int>::iterator i = a.begin();i!a.end();i++)cout<<*i<<' ';
//iterator<int>iterator可以直接写成auto,a.begin()就是a[0],a.end()就是a[size()]
for(auto x : a)cout<<x<<' ';
vector 支持比较运算,按字典序
pair<int,int>
first 第一个元素,second 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字 (字典序)
string 字符串
size()/length()返回字符串长度
empty()
clear()
'+'号做字符串拼接
substr()返回某一个子串
c_str()返回string对应数组的头指针
queue 队列
size()
empty()
push()往队尾插入一个元素
front()返回队头元素
back()返回队尾元素
pop()弹出队头元素
priority_queue 优先队列 (堆)
默认是大根堆
push()往堆里插入一个元素
top()返回堆顶元素
pop()弹出堆顶元素
定义成小根堆的方式:
priority_queue<int,vector<int>,greate<int>>q;
stack 栈
size()
empty()
push()往栈顶插入一个元素
top()返回栈顶元素
pop()弹出栈顶元素
deque 双端队列
队头队尾都可以插入弹出,且支持随机寻址
size()
empty()
clear()
front()返回第一个元素/back()返回最后一个元素
push_back()向最后插入一个元素/pop_back()弹出最后一个元素
push_front()向队首插入一个元素/pop_front()从队首弹出一个元素
begin()/end()迭代器
//特点:慢,用的不多
set,map,multiset,multimap
基于平衡二叉树(红黑树)实现 动态维护有序序列
size()
empty()
clear()
begin()/end() 支持++ --操作 返回前驱和后继
set/multiset:set里没有重复元素,multiset里可以有重复元素
insert() 插入一个数
find()查找一个数
count()返回某一个数的个数
erase():
1.输入是一个数x,删除所有x 时间复杂度是o(k+logn) k是x的个数
2.输入是一个迭代器,删除这个迭代器
lower_bound()/upper_bound():
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap://存的是一个映射
insert()插入的数是一个pair
erase()输入的参数是pair或者迭代器
find()
[]可以像数组一样用,时间复杂度是o(logn)
unordered_set,unordered_map,unordered_multiset,unordered_multimap
基于哈希表实现,无序
和上面类似,增删改查的时间复杂度是o(1)
不支持lower_bound()/upper_bound()和迭代器的++ --
bitset 压位
bitset<10000>s;
~,&,|,^
>>,<<
==,!=
[]
PS:
any()返回有多少个1
count()判断是否至少有一个1
none()判断是否全为0
set()把所有位置变成1
set(k,v)将第k位变成v
reset()把所有位变成0
flip()把所有位取反,等价于~
flip(k)把第k位取反
系统为某一程序分配空间时,所需时间,与空间大小无关,与申请次数有关