欢迎访问我的主页https://hjh645.github.io/
感谢大家!~
数据结构 1
一、链表与邻接表
用数组模拟链表
struct Node {
int val;
Node *next;
};
new Node(); //非常耗时
- 单链表:写邻接表(存储图、树)
- 双链表:优化某些问题
1.1 单链表
head ->
head ->
用两个数组,一个用来存当前指针的值是多少(e[]),一个用来存它下一个结点的位置(ne[])
#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;
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]];
}
题目
实现一个单链表,链表初始为空,支持三种操作:
(1) 向链表头插入一个数;
(2) 删除第k个插入的数后面的数; (删除下标是k-1的点的后面一个点)
(3) 在第k个插入的数后插入一个数 (在下标是k-1的点后面插入一个点)
现在要对该链表进行M次操作,进行完所有操作后,从头到尾输出整个链表。
注意:题目中第k个插入的数并不是指当前链表的第k个数。例如操作过程中一共插入了n个数,则按照插入的时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,…第n个插入的数。
输入格式
第一行包含整数M,表示操作次数。
接下来M行,每行包含一个操作命令,操作命令可能为以下几种:
(1) “H x”,表示向链表头插入一个数x。
(2) “D k”,表示删除第k个输入的数后面的数(当k为0时,表示删除头结点)。
(3) “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
==========
这里要注意第k个结点的下标是k-1。
下面只有main函数不一样
#include<iostream>
using namespace std;
const int M = 100010;
int head, idx, e[M], ne[M];
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()
{
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];
remove(k - 1);
}
else
{
cin >> k >> x;
add(k - 1, x);
}
}
for(int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
return 0;
}
双链表
额外开两个数组,一个l[N]存的是左边结点的坐标,一个r[N]存的是右边结点的坐标。
使用0表示head结点,1表示tail结点
#include <iostream>
using namespace std;
const int N = 100010;
int m;
int e[N], l[N], r[N], idx;
// 初始化
void init() {
r[0] = 1, l[1] = 0; // 0表示头结点,1表示尾结点,头结点的右边是尾结点,尾结点左边是头结点
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++ ;
}
// 在k的左边插入结点,直接调用 add(l[k], x)
// 删除下标为k的结点
void remove(int k) {
r[l[k]] = r[k];
l[r[k]] = l[k];
}
二、栈与队列
栈:先进后出
队列:先进先出
栈
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt; // tt 表示栈顶
//插入
stk[ ++ tt] = x;
//弹出
tt --;
// 判断栈是否为空
if(tt > 0) 不空;
else 空;
// 栈顶
stk[tt];
队列
#include <iostream>
using namespace std;
const int N = 100010;
// 在队尾插入元素,对头弹出元素
int q[N], hh, tt = -1; // hh 表示对头, tt 表示队尾
// 插入
q[++tt] = x;
// 弹出
hh ++;
//判断是否为空
if(hh <= tt) 非空;
else 空;
// 取队头
q[hh];
单调栈
思想:如果有一个数比另一个数下标更小,值更大,那么这个数永远不会被用到。
保证栈是单调递增的
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int stk[N], tt;
int main() {
ios::sync_with_stdio(false); //可以加速
scanf("%d", &n); //scanf 比 cin快,输入输出比较多就用scanf printf
for(int i = 0; i < n; i++) {
int x;
scanf("%d", &x);
while (tt & stk[tt] >= x) tt--; // 如果tt不为0(即栈不为空),把所有比x更大的数弹出
if (tt) printf("%d ", stk[tt]); //栈顶即为所求元素
else printf("-1 ");
stk[ ++ tt] = x; //把x插入栈顶
}
}
单调队列
求滑动窗口里的最大值/最小值
用队列来维护窗口
暴力做法:遍历窗口中的每个值求最大值/最小值
优化:如果窗口中,有一个数比另一个数的值更大,并且在那个数的左边,那么这个数就可以从队列中删掉。这样就可以得到一个单调上升的队列。
如果开了02优化/O3优化,stl和数组的速度是差不多的,如果没开(99%情况下),那么stl比数组慢一些。
#include <iostream>
using namespace std;
const int N = 1000010;
int n;
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 ++ ) {
// 判断队头是否已经滑出窗口
// hh<=tt就说明不是空的,
// i-k+1是队列的第一个元素的位置
// 如果队列第一个元素的位置大于当前的头结点的位置,那当前的头结点应该出队列
// 用if不用while,因为1次只会有一个元素要出去。
if (hh <= tt && i - k + 1 > q[hh]) hh ++;
// 如果当前要插入队列的元素,也就是a[i],比队列的末尾更小,那么队列的末尾就要出队
// 一直将所有比将要插入的元素更大的末尾元素出队,保证要插入的元素比末尾元素更大
// 那么当前的头结点就是最小的结点的位置
while (hh <= tt && a[q[tt]] >= a[i]) tt--;
q[ ++ tt] = i; // 将当前元素插入队尾,实际上是当前元素的位置插入队尾
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
puts("");
// 找窗口的最大值,和最小值的代码相同,只有一个符号变了
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;
if (i >= k - 1) printf("%d ", a[q[hh]]);
}
puts("");
return 0;
}
三、KMP
暴力算法怎么做
S[N], p[N]
for(int i = 1; i <= n; i ++ ) {
bool flag = true;
for (int j = 1; j <= m; j ++ ) {
if (s[i] != p[j]) {
flag = false;
break;
}
}
}
如何优化
找匹配的字符串中,1j的段和i-j+1i的段相同,并且长度最长的串。
KMP算法教程可以参考https://www.bilibili.com/video/BV1jb411V78H/ 以及https://www.bilibili.com/video/BV1M5411j7Xx
(下面是从视频的评论中拿过来的)
要求那个next数组(有些叫next-val数组):
必须先求模式串S 每一个字符前面的那个字符串的最大公共前后缀长度,将这一系列长度存成一个数组,求出来的每个长度其实就是和模式串每一个对应位置上做比较的下标
例如:模式串是ABACABC的最长公共前后缀长度数组为:我们将最长公共前后缀长度记作LCPSF,现在从模式串第一个字符A开始,A的前面字符串为null,所以A之前的子串的LCPSF是0;来到B,B的前面字符串是A,A是单独的字符不存在公共前后缀,所以长度也是0;来到A,A前面的子串是AB,LCPSF为0;来到C,C前面的子串是ABA,LCPSF为1;来到A,A前面的子串是ABAC,LCPSF为0;来到B,B之前子串为ABACA,LCPSF为1;来到C,C前面子串为ABACAB,LCPSF为2;到此这个最长公共前后缀数组就出来了=【0,0,0,1,0,1,2】将这个数组从第二个值开始每个值加1=【0,1,1,2,1,2,3】就是将要和子串对应位置比较的下标
关于指针回溯求next的理解
每次求next【i】,可看作前缀与后缀的一次匹配,在该过程中就可以用上之前所求的next,若匹配失败,则像模式串与父串匹配一样,将指针移到next【j-1】上。
求next过程实际上是dp(动态规划),只与前一个状态有关:
若不匹配,一直往前退到0或匹配为止
若匹配,则将之前的结果传递:
因为之前的结果不为0时,前后缀有相等的部分,所以j所指的实际是与当前值相等的前缀,可视为将前缀从前面拖了过来,就不必将指针从前缀开始匹配了,所以之前的结果是可以传递的。
#include <iostream>
using namespace std;
const int N = 10010, M = 100010;
int n, m;
char p[N], s[M];
int ne[N]; // next[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过程
// s[i]是当前匹配的,试图和s[i]匹配的是p[j+1]
for (int i = 1, j = 0; i <= m; i ++ ) {
// j没有退回起点,并且s[i]和p[j+1]不匹配(j前面的都匹配了)
// 则将j赋值为ne[j],即,ne[j]前面的都匹配了,从ne[j]+1开始重新匹配
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++; // 匹配成功,j往后移
if (j == n) {
printf("%d ", i - n);
j = ne[j];
}
}
return 0;
}