第二章 数据结构(一):链表与邻接表,栈与队列,kmp

第二章 数据结构(一):链表与邻接表,栈与队列,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;
}

KMP 待补充!!!!!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值