数据结构
1.链表
- 对于工程所使用的链表就不说了可以去看青岛大学王卓老师的
- 单链表
// 创建链表
const int N = 1e6 + 10
// e[N] 表示value -> 数据 我是以int 类型举例, 你可以用其他的比如struct, double
// ne[N] 表示next的位置
// idx 相当于指针
// head 表示头指针
// 以-1结尾
int head, e[N], ne[N], idx;
void init() {
idx = 0;
// 头指针指向空 即为 -1
head = -1;
}
// 头插入
void add_to_head(int x) {
e[idx] = x;
ne[idx] = head;
head = idx++;
}
// 中间插入 在插入的第k个下标后插入
void add(int k, int x) {
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
// 删除k 后面一个数
// k与上面一样
void remove(int k) {
ne[k] = ne[ne[k]];
}
对于上面k是什么?
下面图片给予解释
!](…/…/Snipaste_2023-05-09_19-47-31.png)
这题上面有
输入数据:
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
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
// 下面函数基本上就是对于单调栈的相关操作
int head, e[N], ne[N], idx;
int n, k, x;
char a;
void init() {
head = -1;
idx = 0;
}
void add_to_head(int x) {
e[idx] = x;
ne[idx] = head;
head = idx++;
}
void add(int k, int x) {
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx++;
}
void remove(int k) {
ne[k] = ne[ne[k]];
}
int main() {
// 初始化链表
init();
// n 个测试数据
cin >> n;
while(n--) {
// 判断第一个字符是什么
cin >> a;
// 对应相关操作
switch(a) {
case 'H':
cin >> x;
add_to_head(x);
break;
case 'I':
cin >> k >> x;
add(k - 1, x);
break;
case 'D':
cin >> k;
if (!k) head = ne[head];
else remove(k - 1);
break;
}
}
// 遍历链表
for (int i = head; i != -1; i = ne[i]) {
printf("%d ", e[i]);
}
printf("\n");
system("pause");
return 0;
}
2.双链表
- 就是可以从前往后也可以从后往前
const int N = 1e6 + 10;
// l[N] 存放左边的next
// r[N] 存放右边的next
int e[N], l[N], r[N], idx;
// 你可能会发现与上面不同的是head没有了
// 那么头指针去哪里了呢?
// 我们这里偷了个懒 把l方向的head 用0号下标来表示, r 方向的head用1号下标来表示
void init() {
r[0] = 1;
l[1] = 0;
// 0 的右边为1
// 1 的左边为0
}
// 在第k个插入的元素后添加x
void add(int k, int x) {
e[idx] = x;
r[idx] = r[k];
l[idx] = l[k];
l[r[k]] = idx;
r[k] = idx++;
// 最后两个不可反过来
}
// 这里为什么只有有插入, 而且没有做插入呢?
// 你可以想想
// 输出第k个插入元素
void remove(int k) {
l[r[k]] = l[k];
r[l[k]] = r[k];
}
- 对于左插入只需调用add(l[k], x); 即为右插入
!](…/…/Snipaste_2023-05-09_20-05-25.png)
测试数据:
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
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n;
int e[N], r[N], l[N], idx;
char a;
int k, x;
void init() {
r[0] = 1;
l[1] = 0;
idx = 2;
}
void add(int k, int x) {
e[idx] = x;
r[idx] = r[k];
l[idx] = k;
l[r[k]] = idx;
r[k] = idx++;
}
void remove(int k) {
l[r[k]] = l[k];
r[l[k]] = r[k];
}
int main() {
init();
scanf("%d", &n);
while(n--) {
cin >> a;
switch(a) {
case 'L':
cin >> x;
add(0, x);
break;
case 'R':
cin >> x;
add(l[1], x);
break;
case 'D':
cin >> k;
remove(k + 1);
break;
case 'I':
cin >> a;
if (a == 'L') {
cin >> k >> x;
add(l[k + 1], x);
}else {
cin >> k >> x;
add(k + 1, x);
}
}
}
// 为什么这个遍历与单链表不一样???
for (int i = r[0]; i != 1; i = r[i]) {
printf("%d ", e[i]);
}
return 0;
}
链表补充
- 单链表变成循环链表
// 对一个元素进行闭环操作
// 不需要head
ne[idx] = idx;
// 这是我自己想的不确定对于不对
// 如果是不初始化head 在最后一个元素来了后进入下一个就是head, 但e[head] 没有东西
// 所以不妨直接删除head
- 双链表变成循环链表
// 其实与单链表是类似的处理
// 对第一个元素
r[idx] = idx;
l[idx] = idx;
// 这样应该就可以了, (不确定对错)
3.栈(stack)
const int N = 1e6 + 10;
// sta 表示一个栈
// tt 表示一个类似指针 指向栈顶的下标
int sta[N], tt;
// 插入
void push(int x) {
sta[tt++] = x;
}
// 删除
void pop() {
tt--;
}
// 取出栈顶元素
int top() {
return sta[tt - 1];
}
// 判断是否为空
bool isempty() {
return tt == 0;
}
// 栈的长度
int size() {
return tt;
}
给个例子:
!](…/…/Snipaste_2023-05-09_20-15-05.png)
输入数据:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出数据:
5
5
YES
4
NO
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int sta[N], tt, n, t;
string s;
// 是否为空
bool isempty() {
return tt == 0;
}
// 增加元素
void add(int x) {
sta[tt++] = x;
}
// 删除元素
void pop() {
tt--;
}
// 长度
int size() {
return tt;
}
// 栈顶元素
int top() {
return sta[tt - 1];
}
int main()
{
scanf("%d", &n);
while(n --) {
cin >> s;
if (s == "push") {
scanf("%d", &t);
add(t);
}else if (s == "pop") {
pop();
}else if (s == "empty") {
if (isempty()) {
printf("YES\n");
}else {
printf("NO\n");
}
}else {
printf("%d\n", top());
}
}
system("pause");
return 0;
}
4.双端队列
queue只是deque的一部分, 对于模拟队列来说可以模拟双端队列来实现
对于priority_queue来说是没法直接模拟的
可以通过模拟队列加sort来实现不过时间复杂度比较高
可以换成堆来实现相对来说时间复杂度就低了许多
const int N = 1e6 + 10;
// 用que来表示队列
// tt 表示队尾
// hh 表示对头
// 为什么要令他们等于N / 2 呢?
// 因为要push_front()
int que[N], tt = N / 2, hh = N / 2;
// 尾插
void push_back(int x) {
que[tt++] = x;
}
// 头插
void push_front(int x) {
que[hh--] = x;
}
// 尾删
void pop_back() {
tt--;
}
// 头删
void pop_front() {
hh++;
}
// 判断对否为空
bool isempty() {
return tt == hh;
}
// 队的长度
int size() {
return tt - hh;
}
// 输出尾顶部元素
int back() {
return que[tt - 1];
}
// 输出头部元素
int front() {
return que[hh];
}
!](…/…/Snipaste_2023-05-09_20-29-57.png)
输入:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出:
NO
6
YES
4
参考代码:
#include <iostream>
#include<deque>
using namespace std;
const int N = 1e6 + 10;
int que[N], hh, tt;
int n, t;
string s;
void push(int x) {
que[tt++] = x;
}
void pop() {
hh++;
}
bool isempty() {
return hh == tt;
}
int front() {
return que[hh];
}
int main()
{
cin >> n;
while ( n--) {
cin >> s;
if (s == "push") {
cin >> t;
push(t);
}else if (s == "pop") {
pop();
}else if (s == "empty") {
if (isempty()) {
printf("YES\n");
}else {
printf("NO\n");
}
}else {
printf("%d\n", front());
}
}
system("pause");
return 0;
}
5.栈的应用->单调栈
!](…/…/Snipaste_2023-05-09_21-15-11.png)
- 暴力做法O(n^2)
for (int i = 0; i < n; i++) {
int j = i - 1;
for (; j >= 0; j--) {
if (q[j] < q[i]) {
break;
}
}
if (j >= 0) {
printf("%d ", q[j]);
}else {
printf("-1 ");
}
}
n = 1e5 所以会time limit error
那么这么优化呢?
不妨把暴力过程写出来, 来看哪些数据一定是没有用的, 那么我们就可以直接去除
!](…/…/20230509213124.png)
!](…/…/20230509213116.png)
最终, 我们发现了可以将之前的数据保存到一个单调递增的栈中
// 参考代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, sta[N], tt, q[N];
void push(int x) {
sta[tt++] = x;
}
void pop() {
tt--;
}
int top() {
return sta[tt - 1];
}
bool isempty() {
return tt == 0;
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", q + i);
}
for (int i = 0; i < n; i++) {
// 如果 栈顶的元素比q[i] 大就将栈顶元素删除
while(!isempty() && top() >= q[i]) pop();
// 如果栈空了就说明栈中元素均大于等于q[i], 在q[i] 前面没有小于q[i] 的值
if (isempty()) {
printf("-1 ");
}else {
printf("%d ", top());
}
// 将q[i] 加入到栈中
push(q[i]);
}
system("pause");
return 0;
}
6.队的应用->单调队列
!](…/…/Snipaste_2023-05-09_21-37-33.png)
与上题类似
首先用暴力做法, 然后再爆力的基础上, 进行优化
// 暴力
int l = 0, r = k - 1;
for (int i = r; i < n; i++) {
int mi = inf;// inf = 2^31 - 1
for (int j = l; j <= r; j++) {
mi = min(mi, q[j]);
}
printf("%d ", mi);
l++, r++;
}
时间复杂度O(nk)// k 没有说多少默认1e6
!](…/…/20230510083501.jpg)
// 参考代码
// 注意: que保存的是q[i] 中的i 不是q[i]
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, q[N], k;
int que[N], tt, hh;
// 保存下标
void push(int x) {
que[tt++] = x;
}
// 尾部删除
void pop_back() {
tt--;
}
// 头删除
void pop_front() {
hh++;
}
bool isempty() {
return hh == tt;
}
int size() {
return tt - hh;
}
// 查队头
int front() {
return q[que[hh]];
}
// 查队尾
int back() {
return q[que[tt - 1]];
}
int main() {
scanf("%d%d", &n, &k);
for (int i = 0; i < n; i++) {
scanf("%d", q + i);
}
for (int i = 0; i < n; i++) {
// 这就是为什么要保存i, i - k + 1 > que[hh] // 可以判断长度是否>= k 不然无法判断
if (tt > hh && i - k + 1 > que[hh]) pop_front();
// 这里一定要用尾删除, 所以无法用que来保存q[i]
// 为什么不能用头删除
// 8 3
// 1 3 -1 -3 5 3 6 7
// 后面会出现5 你可以去试一试
while(!isempty() && back() >= q[i]) pop_back();
push(i);
// 当i >= k - 1 也就是第一个滑动窗口满了的时候, 后面开始输出
if (i >= k - 1) printf("%d ", front());
}
puts("");
// 后面是写大于的
// 思想一样
hh = tt = 0;
for (int i = 0; i < n; i++) {
if (tt > hh && i - k + 1 > que[hh]) pop_front();
while(!isempty() && back() <= q[i]) pop_back();
push(i);
if (i >= k - 1) printf("%d ", front());
}
system("pause");
return 0;
}
7. KMP 算法
简介:
kmp 算法主要应用与有两个字符串之间匹配的问题上面
假设存在字符串s1 = “abababab”, s2 = “aba” 求所有s1中出现子串s2的初始下标
首先还是先暴力做法:
for (int i = 0; s1[i]; i++) {
if (s1[i] == s2[0]) {
int j = 1, l = i + 1;
for (; s2[j]; j++, l++) {
if (s2[j] != s1[l]) {
break;
}
}
if (s2[j] == '\0') {
printf("%d ", i);
}
}
}
时间复杂度O(nm)
一般n, m 范围为10^5 会超时
优化:
那么如果s2 的前i个和后i个字符串一样的
字符串"abcabc"
前3个与后3个是一样的
那么当匹配到最后一个字符‘c’后 我们可以将i 向后移动3格而不是一格这就是kmp算法的优化
我们定义:
ne[]数组存放就是以i 结尾向后移动的格子数量比如上面的ne[5] = 3;
!](…/…/20230512152314.png)
对应代码:
for (int i = 1, j = 0; p[i]; i++ ) {
// 这个理解起来有点难
// 下面有解释
while(j && p[i] != q[j + 1]) j = ne[j];
// while() 循环退出两种情况
if (p[i] == q[j + 1]) j++;
// 匹配完成
// 为什么是i - k 下面有解释
if (j == m) {
printf("%d", i - k);
// 最后一个元素他可以让i 向前多少
// 可以不写, 但是这样可以减少时间
j = ne[j];
}
}
// 乍一看这个代码好像也是O(n ^ 2) 其实是O(n) 证明我也不会
// 求ne代码
// 就是q对q自己求子串的过程
// ne[1] = 0 这不用过多解释, 所以从二开始
for (int i = 2, j = 0; q[i]; i++) {
while(j && q[i] != q[j + 1]) j = ne[j];
if (q[i] == q[j + 1]) j++;
ne[i] = j;
}
!](…/…/20230512155013.jpg)
!](…/…/002.png)
8.trie树
trie树可以用来存放字符串出现次数和插入一个字符串
// N 代表所有字符串中 字符的数量不超过N, 不是字符串的数量不超过N
const int N = 1e6 + 10;
// 假设字符串全为小写字母
// son表示存放单词的数组, 其实是一个链表(我之前不是这么想的, 会出现一个bug在后面会说)
// idx 与链表的差不多;
// cnt[idx]记录的是以 idx 结尾出现的字符串的个数
// 如果没有要个数可以用bool
int son[N][26], idx, cnt[N];
// 因为全为小写字母 所以只需要26就行
// 最保险的做法是 son[N][128]
// 当然所需要的空间更大
假设现在有字符串“abcdef" “abceff” “abfgh” 需要插入到trie中
先写代码:
void insert(char * str) {
int p = 0;
for(int i = 0; str[i]; i++) {
int u = str[i] - 'a';
// 因为idx 初始化为0 所以一定要用++idx 而不能用idx++
// 如果没有son[p][u] 我就开辟一条路来
if (!son[p][u]) son[p][u] = ++idx;
// p = son[p][u] 相当于链表了
p = son[p][u];
}
// 因为p他是惟一的 所以cnt[p]是唯一的
cnt[p] ++;
}
// 这也可以解释N 就比如每个字符串中字符对应不等idx就会一种加一
解释:
!](…/…/Snipaste_2023-05-12_16-26-49.png)
!](…/…/Snipaste_2023-05-12_16-26-58.png)
是不是觉得很奇怪???
这是啥?
我之前想直接son[26][26]不就行了何必这么麻烦
但是有bug
比如插入 abc
你查询abcc 发现是有的
接下来就是查询操作了
int query(char str[]) {
int p = 0;
for (int i = 0; str[i]; i++) {
int u = str[i] - 'a';
// 就是说str[i] 下一个字符没得了
// 那肯定没有对应的字符串了
// return 0;
if (!son[p][u]) return 0;
p = son[p][u];
}
// 返回cnt[p] 就是以p结尾的字符串
return cnt[p];
}
!](…/…/Snipaste_2023-05-12_16-43-08.png)
输入:
5
I abc
Q abc
Q ab
I ab
Q ab
输出:
1
0
1
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int son[N][26], idx, cnt[N];
int n;
char str[N];
void insert() {
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() {
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() {
cin >> n;
while(n --) {
// 这是一个技巧
// 读入%s来接收%c 可以去除空格
char op[2];
cin >> op >> str;
if (op[0] == 'I') {
insert();
}else {
cout << query() << endl;
}
}
return 0;
}
9.并查集
什么是并查集呢?
[请自行百度](算法学习笔记(1) : 并查集 - 知乎 (zhihu.com))
并查集最最重要的就是find
// find函数为返回x所在的集合的顶部
// 同时也使用了路径压缩
// 定义每个集合的root的p[root]为其下标
int find(int x) {
if (x != p[x]) p[x] = find(p[x]);
return p[x];
}
!](…/…/01.png)
- 如何将一个集合和另外一个集合变成一个集合
// 假设要合并 a b 所在集合
p[find(a)] = find(b);
// 就是将 a 的root 的头改为 b 的root的头
!](…/…/Snipaste_2023-05-12_17-07-46.png)
输入:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出:
Yes
No
Yes
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int q[N];
int n, m;
int find(int x) {
if (x != q[x]) q[x] = find(q[x]);
return q[x];
}
int main() {
cin >> n >> m;
// 题目说一开始这n个点在不同集合
// 全部为i = q[i]即可, 说明他们在不同集合
// 这里我之前以为他们全部在为0 的集合里, 后面发现不对
// 因为我没有看初始化
for (int i = 0; i < n; i++) {
q[i] = i;
}
while(m --) {
char op[2];
int a, b;
cin >> op >> a >> b;
if (op[0] == 'M') {
q[find(a)] = find(b);
}else {
if (find(a) == find(b)) {
puts("Yes");
}else {
puts("No");
}
}
}
return 0;
}
同时, 还可以用并查集维护一些元素
比如: 集合里面元素的个数, 集合元素到root的距离等?
10.堆
主要介绍手写堆而不是priority_queue() (这是c++的)
手搓堆最最最最最…最主要的就是down() and up() 一个是上浮, 一个是下沉
堆 一定是完全二叉树
哪什么是完全二叉树呢?
请自行百度
还有一个概念就是满二叉树
[请自行百度](百度一下,你就知道 (baidu.com))
// 授之以鱼不如授之以渔, 请去百度
这里是以小根堆举例:
最小堆(小根堆):根结点的键值是所有堆结点键值中最小者。
就是:他的左子树和右子树均大于等于自己本身
你可以猜猜右子树是啥?
// 用 heap[]数组来存放堆
// cnt 表示已经用了多少的元素
// u 是下标
void down(int u) {
// t 是他本身, 左子树root, 右子树root的最小值的下标
int t = u;
// cnt 是从1开始 如果是0 2 * 0 = 0 那么根的左子树和root在同一点所以从一开始
// 左子树下标为 2u
// 有子树下标为 2u + 1 在完全二叉树应该有讲
if (u * 2 <= cnt && heap[u * 2] < heap[u]) t = u * 2;
if (u * 2 + 1 <= cnt && heap[u * 2 + 1] < heap[u]) t = u * 2 + 1;
// if u != t 说明左右子树其中有一个更小
if (u != t) {
swap(heap[u], heap[t]);
// 在调用t 开始的down
down(t);
}
}
// 不多做解释
// 比较简单
void up(int u) {
while (u >> 1 && heap[u >> 1] > heap[u]) {
swap(heap[u >> 1], heap[u]);
u >>= 1;
}
}
!](…/…/Snipaste_2023-05-11_17-59-16.png)
// 创建堆
cin >> n;
for (int i = 0; i < n; i++) {
cin >> heap[i];
}
// 这里就可以创建堆
// 时间复杂度O(n)
// n >> 1 就是倒数第二层 从倒数第二层开始去依次down()
// down 可以将该子树变成最小堆
for (int i = (n >> 1); i; i--) {
down(i);
}
!](…/…/Snipaste_2023-05-12_18-16-25.png)
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m;
// sz 就是cnt
// 本来想写size 你可以试一试为啥不行
int h[N], sz;
void down(int u) {
int t = u;
if (u * 2 <= sz && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= sz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t) {
swap(h[u], h[t]);
down(t);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n ; i++) {
scanf("%d", h + i);
}
sz = n;
// 创建堆
for (int i = n / 2; i; i--) {
down(i);
}
while(m --) {
printf("%d ", h[1]);
h[1] = h[sz];
sz--;
down(1);
}
return 0;
}
ap[u >> 1], heap[u]);
u >>= 1;
}
}
[外链图片转存中...(img-6bFRW9zM-1683897425195)]!](../../Snipaste_2023-05-11_17-59-16.png)
```c
// 创建堆
cin >> n;
for (int i = 0; i < n; i++) {
cin >> heap[i];
}
// 这里就可以创建堆
// 时间复杂度O(n)
// n >> 1 就是倒数第二层 从倒数第二层开始去依次down()
// down 可以将该子树变成最小堆
for (int i = (n >> 1); i; i--) {
down(i);
}
[外链图片转存中…(img-i6g0tKtF-1683897425195)]!](…/…/Snipaste_2023-05-12_18-16-25.png)
参考代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m;
// sz 就是cnt
// 本来想写size 你可以试一试为啥不行
int h[N], sz;
void down(int u) {
int t = u;
if (u * 2 <= sz && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= sz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t) {
swap(h[u], h[t]);
down(t);
}
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n ; i++) {
scanf("%d", h + i);
}
sz = n;
// 创建堆
for (int i = n / 2; i; i--) {
down(i);
}
while(m --) {
printf("%d ", h[1]);
h[1] = h[sz];
sz--;
down(1);
}
return 0;
}