目录
正文
单链表
在算法题目中,我们需要快速的得到答案,因此,单链表在实现的过程中使用数组实现,记录下一个位置。
题目:
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 k 个插入的数后面的数;
- 在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 11 个插入的数,第 22 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x
,表示向链表头插入一个数 x。D k
,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。I k x
,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
输出格式
共一行,将整个链表从头到尾输出。
数据范围
1 ≤ M ≤ 100000
所有操作保证合法。
输入样例:
10
H 9
I 1 1
D 1
D 0
H 6
I 3 6
I 4 5
I 4 5
I 3 4
D 6
输出样例:
6 4 6 5
笔记
操作 D 0
理解错了,卡了一会儿
我误解为了重置单链表,但实际上只是删除头结点指向的元素
#include <iostream>
using namespace std;
const int N = 100010;
int head, e[N], ne[N], idx;
void init()
{
head = -1;
idx = 0;
}
void add_head(int x)
{
e[idx] = x;
ne[idx] = head;
head = idx;
idx ++ ;
}
void delete_k(int k)
{
ne[k] = ne[ne[k]];
}
void insert_k(int k, int x)
{
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx ++ ;
}
int main()
{
int m;
scanf("%d", &m);
init();
while (m -- )
{
char op[2];
scanf("%s", op);
if (*op == 'H')
{
int x;
scanf("%d", &x);
add_head(x);
}
else if (*op == 'D')
{
int k;
scanf("%d", &k);
if (k) delete_k(k - 1);
else head = ne[head];
}
else
{
int k, x;
scanf("%d%d", &k, &x);
insert_k(k - 1, x);
}
}
int p = head;
while (p != -1)
{
printf("%d ", e[p]);
p = ne[p];
}
return 0;
}
双链表
实现一个双链表,双链表初始为空,支持 55 种操作:
- 在最左侧插入一个数;
- 在最右侧插入一个数;
- 将第 k 个插入的数删除;
- 在第 k 个插入的数左侧插入一个数;
- 在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 11 个插入的数,第 22 个插入的数,…第 n 个插入的数。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
L x
,表示在链表的最左端插入数 x。R x
,表示在链表的最右端插入数 x。D k
,表示将第 k 个插入的数删除。IL k x
,表示在第 k 个插入的数左侧插入一个数。IR k x
,表示在第 k 个插入的数右侧插入一个数。
输出格式
共一行,将整个链表从左到右输出。
数据范围
1 ≤ M ≤ 100000
所有操作保证合法。
输入样例:
10
R 7
D 1
L 3
IL 2 10
D 3
IL 2 7
L 8
R 9
IL 4 7
IR 2 2
输出样例:
8 7 7 3 2 9
笔记
刚开始操作符用的是 char
改为用 string 读,最后解决了读入的问题
#include <iostream>
using namespace std;
const int N = 100010;
int e[N], l[N], r[N], idx;
void init()
{
l[0] = -1, r[0] = 1, l[1] = 0, r[1] = -1, idx = 2;
}
void add_left(int x)
{
e[idx] = x, r[idx] = r[0], l[idx] = 0, l[r[0]] = idx, r[0] = idx ++ ;
}
void add_right(int x)
{
e[idx] = x, r[idx] = 1, l[idx] = l[1], r[l[1]] = idx, l[1] = idx ++ ;
}
void delete_k(int k)
{
r[l[k]] = r[k], l[r[k]] = l[k];
}
void insert_left(int k, int x)
{
e[idx] = x, l[idx] = l[k], r[idx] = k, r[l[k]] = idx, l[k] = idx ++ ;
}
void insert_right(int k, int x)
{
e[idx] = x, l[idx] = k, r[idx] = r[k], l[r[k]] = idx, r[k] = idx ++ ;
}
int main()
{
int m;
scanf("%d", &m);
init();
while (m -- )
{
string op;
cin >> op;
if (op == "L")
{
int x;
scanf("%d", &x);
add_left(x);
}
else if (op == "R")
{
int x;
scanf("%d", &x);
add_right(x);
}
else if (op == "D")
{
int k;
scanf("%d", &k);
delete_k(k + 1);
}
else if (op == "IL")
{
int k, x;
scanf("%d%d", &k, &x);
insert_left(k + 1, x);
}
else
{
int k, x;
scanf("%d%d", &k, &x);
insert_right(k + 1, x);
}
}
int p = r[0];
while (p != 1)
{
printf("%d ", e[p]);
p = r[p];
}
}
看了题解以后,发现往 k 的左边插入,其实就是往 k 的左结点的右边插入,可以省去很多定义的操作
#include <iostream>
using namespace std;
const int N = 100010;
int e[N], l[N], r[N], idx;
void init()
{
l[0] = -1, r[0] = 1, l[1] = 0, r[1] = -1, idx = 2;
}
void delete_k(int k)
{
r[l[k]] = r[k], l[r[k]] = l[k];
}
void insert(int k, int x)
{
e[idx] = x, l[idx] = k, r[idx] = r[k], l[r[k]] = idx, r[k] = idx ++ ;
}
int main()
{
int m;
scanf("%d", &m);
init();
while (m -- )
{
string op;
cin >> op;
if (op == "L")
{
int x;
scanf("%d", &x);
insert(0, x);
}
else if (op == "R")
{
int x;
scanf("%d", &x);
insert(l[1], x);
}
else if (op == "D")
{
int k;
scanf("%d", &k);
delete_k(k + 1);
}
else if (op == "IL")
{
int k, x;
scanf("%d%d", &k, &x);
insert(l[k + 1], x);
}
else
{
int k, x;
scanf("%d%d", &k, &x);
insert(k + 1, x);
}
}
int p = r[0];
while (p != 1)
{
printf("%d ", e[p]);
p = r[p];
}
}
栈
实现一个栈,栈初始为空,支持四种操作:
push x
– 向栈顶插入一个数 x;pop
– 从栈顶弹出一个数;empty
– 判断栈是否为空;query
– 查询栈顶元素。
现在要对栈进行 M 个操作,其中的每个操作 33 和操作 44 都要输出相应的结果。
输入格式
第一行包含整数 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
笔记
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int stk[N], tt = -1;
void push(int x)
{
stk[ ++ tt] = x;
}
void pop()
{
tt -- ;
}
void query()
{
printf("%d\n", stk[tt]);
}
void empty()
{
if (tt == -1) printf("YES\n");
else printf("NO\n");
}
int main()
{
int m;
scanf("%d", &m);
while (m -- )
{
string op;
cin >> op;
if (op == "push")
{
int x;
scanf("%d", &x);
push(x);
}
else if (op == "pop") pop();
else if (op == "query") query();
else empty();
}
return 0;
}
队列
实现一个队列,队列初始为空,支持四种操作:
push x
– 向队尾插入一个数 x;pop
– 从队头弹出一个数;empty
– 判断队列是否为空;query
– 查询队头元素。
现在要对队列进行 M 个操作,其中的每个操作 33 和操作 44 都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M 行,每行包含一个操作命令,操作命令为 push x
,pop
,empty
,query
中的一种。
输出格式
对于每个 empty
和 query
操作都要输出一个查询结果,每个结果占一行。
其中,empty
操作的查询结果为 YES
或 NO
,query
操作的查询结果为一个整数,表示队头元素的值。
数据范围
1 ≤ M ≤ 100000,
1 ≤ x ≤ 10^9
所有操作保证合法。
输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
笔记
三目运算符小心越界,最好打个括号
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int m;
int que[N], tt, hh;
int main()
{
cin >> m;
while (m -- )
{
string op;
cin >> op;
if (op == "push")
{
int x;
cin >> x;
que[tt ++ ] = x;
}
else if (op == "pop") hh ++ ;
else if (op == "query") cout << que[hh] << endl;
else cout << ((hh == tt)? "YES" : "NO") << endl;
}
return 0;
}
KMP
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P 在字符串 S 中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1 ≤ N ≤ 10^5
1 ≤ M ≤ 10^6
输入样例:
3
aba
5
ababa
输出样例:
0 2
笔记
ne[N]
表示的是每次匹配失败,最少应该把子串往前移动几位
#include <iostream>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int n, m, ne[N];
char p[N], s[M];
int main()
{
cin >> n >> p + 1 >> m >> s + 1;
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;
}
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) printf("%d ", i - n), j = ne[j];
}
return 0;
}
Trie
维护一个字符串集合,支持两种操作:
I x
向集合中插入一个字符串 x;Q x
询问一个字符串在集合中出现了多少次。
共有 N 个操作,所有输入的字符串总长度不超过 105105,字符串仅包含小写英文字母。
输入格式
第一行包含整数 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
笔记
son[N][26]
中存储的是子结点的编号
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int son[N][26], cnt[N], idx;
char str[N];
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];
}
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;
}
并查集
并查集中的操作都是近似 O ( 1 ) O(1) O(1) 的。
用到了路径优化,做法是每次合并时将结点指向根节点。
题目:
一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
M a b
,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;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
笔记
题解中的 find(x)
很精妙
返回的是祖先结点
同时压缩路径,递归的让路径上的每个节点都指向祖先结点
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int p[N];
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 0; 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);
else
{
if (find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
堆
输入一个长度为 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
笔记
四则运算的优先级高于位运算
建堆时只需要对前
n
/
2
n/2
n/2 的数据进行 down
操作,且该操作的时间复杂度不是
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 而是
O
(
n
)
O(n)
O(n)(具有严格数学证明)
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int hp[N], idx;
void down(int x)
{
int t = x;
if ((x << 1) <= idx && hp[x << 1] < hp[t]) t = x << 1;
if ((x << 1 | 1) <= idx && hp[x << 1 | 1] < hp[t]) t = x << 1 | 1;
if (t != x)
{
swap(hp[t], hp[x]);
down(t);
}
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i ++ ) scanf("%d", &hp[i]);
idx = n;
for (int i = n / 2; i; i -- ) down(i);
while (m -- )
{
printf("%d ", hp[1]);
hp[1] = hp[idx];
idx -- ;
down(1);
}
return 0;
}
哈希表
维护一个集合,支持如下几种操作:
I x
,插入一个整数 x;Q x
,询问整数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。
输入格式
第一行包含整数 N,表示操作数量。
接下来 N 行,每行包含一个操作指令,操作指令为 I x
,Q x
中的一种。
输出格式
对于每个询问指令 Q x
,输出一个询问结果,如果 x 在集合中出现过,则输出 Yes
,否则输出 No
。
每个结果占一行。
数据范围
1 ≤ N ≤ 10^5
−10^9 ≤ x ≤ 10^9
输入样例:
5
I 1
I 2
I 3
Q 2
Q 5
输出样例:
Yes
No
笔记
开放寻址法
如果当前位置上有人,则找下一个位置,直到找到空的位置(解决冲突的某一种办法)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 200003, null = 0x3f3f3f3f;
int h[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()
{
int n;
scanf("%d", &n);
memset(h, 0x3f, sizeof h);
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;
}
链地址法:
h[N]
存储的是链表的头结点
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003;
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 query(int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (x == e[i]) return true;
return false;
}
int main()
{
int n;
scanf("%d", &n);
memset(h, -1, sizeof h);
while (n -- )
{
char op[2];
int x;
scanf("%s%d", op, &x);
if (*op == 'I') insert(x);
else
{
if (query(x)) puts("Yes");
else puts("No");
}
}
return 0;
}
of h);
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;
}
链地址法:
h[N]
存储的是链表的头结点
#include <iostream>
#include <cstring>
using namespace std;
const int N = 100003;
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 query(int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (x == e[i]) return true;
return false;
}
int main()
{
int n;
scanf("%d", &n);
memset(h, -1, sizeof h);
while (n -- )
{
char op[2];
int x;
scanf("%s%d", op, &x);
if (*op == 'I') insert(x);
else
{
if (query(x)) puts("Yes");
else puts("No");
}
}
return 0;
}