第二章 数据结构(一):链表与邻接表,栈与队列,kmp
包含内容:
1.链表与邻接表;
2.栈与队列;
3.kmp;
struct Node
{
int val; // 值
Node *next; //指针
};// 这种方式不讲
// 因为这种方式,每一次创建一个新的链表的时候,就要new 新的结点
new Node(); // 非常慢
// 动态链表方式慢
- 所以我们采用数组模拟链表的方式
一、链表与邻接表
1. 数组模拟单链表——应用多的是邻接表
邻接表的常用用途是存储树和图
(1)ACWING 826 单链表
- 题目中的要求:k,考虑下标就是k - 1;
// 不要把静态链表想成动态链表
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
// head 表示头结点的下标
// e[i] 表示结点i的值
// ne[i] 表示结点i的next指针是多少
//idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;
// 初始化
void init() {
head = -1; // head指向空集
idx = 0; // 当前idx可以从0开始分配
}
// 将一个结点x插到头结点
void add_to_head(int x) {
// idx 当前可以用的最新点的下标
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]];
}
int main () {
int m;
cin >> m; // 输入操作的次数
init(); // 初始化链表
int k, x;
char op;
while (m--) {
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 {
cin >> k >> x;
add(k - 1, x);
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ';
return 0;
}
2. 数组模拟双链表:优化某些问题
**双链表,两个指针,一个指向前,一个指向后; **
(1)ACWING 827 双链表
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int m; // 操作的个数
int e[N]; // 数值value
int l[N]; // 结点的左指针
int r[N]; // 结点的右指针
int idx; //当前可用的结点
// 初始化
void init () {
// 0表示左端点,1表示右端点
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的左边插入x,不必重写,add(l[k], x);
}
// 删除第k个点
//不是第k个点后的点
void remove(int k) {
l[r[k]] = l[k];
r[l[k]] = r[k];
}
int main () {
cin >> m;
init();
int k, x;
while (m--) {
string op;
cin >> op;
if (op == "L") {
// 题目中在链表最左端插入,实际上是指在 "0 将0当做head,《而不是找一个变量存储0》" 的右边插入x,同理在最右边,是指在1的左边插入x
cin >> x;
add(0, x);
}else if (op == "R"){
cin >> x;
add(l[1], x);
}else if (op == "D"){
// 因为idx 从 2开始 分配,所以k要+1
cin >> k;
remove(k + 1);
} else if (op == "IL"){
cin >> k >> x;
add(l[k + 1], x);
}else {
cin >> k >> x;
add(k + 1, x);
}
}
// 区分一下 单链表的输出,这里为什么会从 r[0] 开始输出? 思考一下!
for (int i = r[0]; i != 1; i = r[i]) cout << e[i] <<' ';
return 0;
}
二、栈与队列
栈:先进后出——可以想象成往一个桶里面放东西,要想取出最下面的东西,就得等上面压着的东西取走才行;
队列:先进先出,可以想象出,食堂窗口排队,先打完饭的人,先从队头走;
- 模拟栈操作
const int N = 1e6 + 10;
int stk[N], tt; // tt, 代表当前栈顶下标
// 插入
stk[ ++ tt] = x;
// 弹出
tt --;
// 判断栈是否为空, 若tt == 0, 则栈为空
if (tt > 0) not empty
else empty
// 求栈顶元素
stk[tt];
- 模拟队列操作
// 在队尾插入元素,在队头弹出元素
int q[N], hh, tt = -1; // hh 指向队头, tt 指向队尾
// 插入
q[tt ++] = x;
// 弹出
hh ++;
// 判断队列是否为空
if (hh <= tt) not empty
else empty
// 取出队头元素
q[hh];
1. ACWING 828 模拟栈
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int stk[N], tt; // tt == 0代表栈为空,栈中无元素
int m;
void push(int x)
{
stk[++tt] = x;
}
bool isEmpty()
{
if (tt <= 0) return true; //是空的
else return false;
}
// 但是题目中保证了操作的合法
void pop()
{
if (isEmpty()) return ; //栈为空,无操作
else --tt;
}
int getTop()
{
return stk[tt];
}
int main ()
{
cin >> m;
string op;
int x;
while (m--)
{
cin >> op;
if (op == "push")
{
cin >> x;
push(x);
}
else if (op == "pop")
{
pop();
}
else if (op == "query")
{
printf ("%d\n", getTop());
}
else
{
if (isEmpty()) printf ("YES\n");
else printf ("NO\n");
}
}
return 0;
}
2. 单调栈:ACWING 830 单调栈
单调栈是在栈“先进后出”的性质上,再新增一个特性:从栈顶到栈底的元素是严格递增(或者递减的)呈单调;
常见的应用:在一组序列中,找出每个数左边第一个比此数小的数,不存在则为-1;
- 暴力做法:
for (int i = 0; i < n; ++i)
{
for (j = i - 1; j >= 0; --j)
{
if (a[i] > a[j])
{
就说明找到了左边第一个比a[i]小的数
break;
}
}
}
在暴力做法的过程中,在i往右走的过程中,可以用一个栈来存储i左边的所有元素;
最开始栈是空的,i指针每往右边移动一个位置,就往栈中加入一个元素,当指针到i这个位置时,栈中有如下元素:
每一次寻找的时候,都是从栈顶开始找,找到第一个比i所指向的数要小的数;
(1)ACWING 830. 单调栈
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n;
int stk[N], tt;
int main ()
{
cin >> n;
// 读入n个数字
// stk tt从0开始
for (int i = 0; i < n; ++i)
{
int x;
cin >> x;
// 如果 栈是不空的,且栈顶元素大于等于未输入当前的元素 ,说明栈顶元素不会用到,删掉
while (tt && stk[tt] >= x) tt--;
// 做完后,若栈不空,说明此栈顶元素就是i左边最近的比之小的元素,把他输出
if (tt) cout << stk[tt] << ' ';
else cout << -1 << ' ';
// 完了之后,要将x插入栈中
stk[++tt] = x;
}
}
- 会发现循环内,每个元素只会进栈一次,出栈一次,时间复杂度还是O(n)
- 提升cin, cout 的速度;
3. ACWING 829. 模拟队列
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int m;
// hh 指向队头, tt 指向队尾
int q[N], hh, tt = -1;
void push_one(int x)
{
q[++tt] = x;
}
bool isEmpty()
{
if (hh > tt) return true; // 是空的
else return false;
}
// 题目保证操作合法
void pop_one()
{
if (isEmpty()) return;
else hh++;
}
int getTop()
{
return q[hh];
}
int main ()
{
cin >> m;
string op;
int x;
while (m--)
{
cin >> op;
if (op == "push")
{
cin >> x;
push_one(x);
}
else if (op == "pop")
{
pop_one();
}
else if (op == "query")
{
cout << getTop() << endl;
}
else
{
if (isEmpty()) cout << "YES" << endl;
else cout << "NO" <<endl;
}
}
return 0;
}
4. 单调队列的应用:ACWING 154. 滑动窗口
- 输出滑动窗口长度为3的最大与最小值;
- 总结:
单调栈和单调队列的问题:先用栈或者队列来暴力的模拟一下问题,先把朴素算法的思路整理清楚;
再看朴素算法中,栈和队列里面哪些元素是没有用的;
将这些没有用的元素全部删掉,再找找是否有单调性,若剩下的元素有单调性就可以做优化,取最值就可以取两个端点了,找一个值就可以用二分等等方法;
有单调性后再考虑优化;
多重背包可以用单调队列优化;
判断队头的下标是否超出了【i - k + 1. i】,超出了就把队头删掉,队列中存储的是下标不是值;
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
int n;
int k;
int a[N], q[N]; // a : 存储原来的值 ;q : 存储单调队列
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)
{
// 队列中存储的是下标;
// 判断队列是不是空的,并且终点是i ,起点是i - k + 1
// > q[hh], 说明q[hh] 出了窗口,hh ++;
// 用if就行,因为每次窗口只往后移动一位,所以每次最多只有一个数是不在窗口内的,无需while
if (hh <= tt && i - k + 1 > q[hh]) hh++; // 队头出队,移动窗口
// 如果我新插入的数字,比队尾的数字要小的话,那队尾就没有用了,那就把这个数字删掉
while (hh <= tt && a[q[tt]] >= a[i]) tt--; // 删除原队尾的数字
// 将当前值,放入q中
q[++tt] = i;
// 当窗口长度不足k不用输出 取队头 从小到大
if (i >= k - 1) printf ("%d ", a[q[hh]]);
}
printf("\n");
// 询找最大值,同理
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;
//当窗口长度不足k不用输出 取队头 从小到大
if (i >= k - 1) printf ("%d ", a[q[hh]]);
}
return 0;
}
//解决队首已经出窗口的问题;
//解决队尾与当前元素a[i]不满足单调性的问题;
//将当前元素下标加入队尾;
//如果满足条件则输出结果;
//上面四个步骤中一定要先3后4,因为有可能输出的正是新加入的那个元素;
三、kmp 【噩梦】
1. ACWING 831 KMP字符串
// 可以防止重复比对,已经比对过的直接错位挪一下,从那个不匹配的地方继续比较
//s[i] 匹配 p[j + 1] 往前错一位
//正在匹配那一段,从不匹配字符开始往前数,找最大的后缀,能对应到模板串从头往后数,然后再错开,就能避免重复比较
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;
const int M = 100010;
int n, m;
char p[M], s[N];
int ne[M]; // next数组
int main () {
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;
}
for (int i = 1, j = 0; i <= m; i++) {
// 若j没有退回起点(没有重新开始匹配),并且s[i] != p[j + 1],
//那么模板串,最少往后多少,可以使得最大前缀 = 后缀 ,next[j] 位置
// 若j不能往下走,j就往前退一步 j = ne[j],看能否再往前走
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;
}