蓝桥杯_数据结构_1 (链表 - 栈与队列 - kmp)

#include <iostream>
using namespace std;

/*
用数组模拟动态链表的原因:每次new节点(结构体)需要一定耗时
1.链表与邻接表
2.栈与队列
3.kmp

*/

826.单链表

实现一个单链表,链表初始为空,支持三种操作:
(1) 向链表头插入一个数;
(2) 删除第k个插入的数后面的数;
(3) 在第k个插入的数后插入一个数
现在要对该链表进行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

【new node 非常慢! 所以一般竞赛、面试也用数组模拟,而不是用指针】
[ typedef struct node{e , ne ,idx} , node[N];结构体数组模拟结点,但是代码变长,仍不是最优写法 ]

记含义:

算法题:追求速度
head 表示存头结点的下标
e[i] 表示下标节点i的值
ne[i] 表示节点i的next指针,值为指向下一个结点值是多少
idx 存储当前已经用到了哪个点(第k个插入的数idx = k-1) (数组长度)
【head-> 3-> 5 -> 7-> 9-> NULL】

head表示头结点下标,刚开始初始化指向-1 , head(null) 还没有指向 (-1表示空)
e[0] = 3 (第一个节点,下标0,地址存放的值)
ne[0]= ② [顺序为链表的第二个结点,结点下标(地址)]
空集用ne[最后结点的next]=-1表示, e和ne用下标关联
idx存储当前用到的地址(相当于一个指针,从第一个结点开始)

插入:),赋x值,元素x指向head->next ,然后头结点指向新结点(数组模拟的lead的next为ne[0] = idx(下一个结点下标)) ,head(指针)指向此插入元素x(元素的结点对应下标idx,再idx++
在这里插入图片描述

①初始化 (头指针)head = -1(空) ; idx = 0(当前无结点,0为第一个插入的结点下标);

②【头插法-算法题80%】e[idx] = x ; ne[idx] = head ; head = idx ; idx++ ;

③将x插到下标是k的点【后面】 (在下标k结点前面插入就传入实参k-1)
e[idx]=x ; ne[idx] = ne[k]; ne[k] = idx ;idx++;

④将下标k后面的节点删除 (删除第k个的结点就传入实参k-1)
ne[k] = ne[ne[k]]; //递归的味道

⑤数组模拟链表循环遍历(i模拟指针从head开始沿着ne[]):
for(int i = head ;i != -1;i = ne[i])




const int N = 100010;

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;  //当前节点idx的next指针 存节点(指向头结点)
	head = idx; //head的next指向idx位置 (head不存值,所以head本身值代表next位置(存放头结点下标位置))
	idx++; //节点用过了,数组中移动到下一个位置
}

//将x插到下标是k的点【后面】 (插入下标k结点前面就传实参k-1即可)
void add(int k,int x) {
	e[idx] = x;
	ne[idx] = ne[k];//idx的next指向k的下一位
	ne[k] = idx;
	idx++; //注意:数组中使用过就不再用,移位 【同时代表长度】
}

//将下标k后面的节点删除(删除第k个的结点就传入实参k-1)
void remove(int k) {
	ne[k] = ne[ne[k]];  // k->next = k->next->next (等效删除)
}//对应算法题可以不用管idx 【这里会浪费空间(k+1的位置),且idx不能表示长度了 -- 题目不需要用到长度,就没关系】

int main() {

	int m;
	cin >> m;
	
	init();
	
	while(m--) {

		int k,x;
		char op;

		cin >> op;//cin会过滤空格等,scanf输入字符串可能出错
		if(op == 'H') {
			cin >> x;
			add_to_head(x);
		} else if(op == 'D') {
			cin >> k;	//若无处理,则删除头结点操作无效 
			if(!k) head = ne[head]; //k == 0时 删除头结点  head指向头结点的下一个节点嵌套ne[存头结点位置head] 
			remove(k - 1);  //delete第k个点
		
		} else {
			cin >> k >> x;  //题目是在第k个位置操作...而函数定义操作k后面的数  , 
			add(k - 1,x); //用k - 1 
		}
	}

	for(int i = head; i != -1; i = ne[i]) cout << e[i] << " ";   //这种输出结束条件为 i != -1 ,-1为最后的节点next->NULL的值 
	cout << endl;
	return 0;
}


827.双链表

实现一个双链表,双链表初始为空,支持5种操作:
(1) 在最左侧插入一个数;
(2) 在最右侧插入一个数;
(3) 将第k个插入的数删除;
(4) 在第k个插入的数左侧插入一个数;
(5) 在第k个插入的数右侧插入一个数
现在要对该链表进行M次操作,进行完所有操作后,从左到右输出整个链表。
注意:题目中第k个插入的数并不是指当前链表的第k个数。例如操作过程中一共插入了n个数,则按照插入的时间顺序,这n个数依次为:第1个插入的数,第2个插入的数,…第n个插入的数。
输入格式
第一行包含整数M,表示操作次数。
接下来M行,每行包含一个操作命令,操作命令可能为以下几种:
(1) “L x”,表示在链表的最左端插入数x。
(2) “R x”,表示在链表的最右端插入数x。
(3) “D k”,表示将第k个插入的数删除。
(4) “IL k x”,表示在第k个插入的数左侧插入一个数。
(5) “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

双链表不用head【区别于单链表】
因为一般算法题中,单链表的head要用来存一个有效数据(指向下一个结点的地址),
而双链表不用,所以头尾(左右端点)用0 ,1 表示即可【为算法题服务的最常用模板】
在这里插入图片描述

记:

L[0](头)= 0,R[0] = 1(尾) //初始化左右指针
【右指针往左走,初始化在最右端,右端点记为idx=1
左指针往右走,初始化在最左端,左端点记为idx = 0】

删除第k个点 R[L[k]] =L[k] ; L[R[k]] = R[k]
(让第k结点的左指针指向右指针的下一个结点下标, k的右指针指向左指针指向的下一个节点的下标地址,
此时k脱离链表,没有指针指向k)





const int N = 100010;
int m;
int e[N],l[N],r[N],idx;  //l->next(前驱) , r->next (后继) e存元素值 ,idx当前结点 

//初始化 (两个无值的边界结点) 
void init(){
	
	//0表示左端点,1表示右端点
	l[1] = 0,r[0] = 1; //初始化,右指针r在最右端,l在最左端,左端点idx=0 
	idx = 2; //注意0,1已经用过了,idx初始值为2
	
}

//在下标是k的点的右边,插入x      
//①idx右 = k右 ②idx左 = k (先③k右(即k+1)的左 = idx) (后④k右 = idx) 
void add(int k,int x) // *在k的左边插入x,传入参数k-1 , 就调用参数为 l[k] == (k - 1)         
{
	e[idx] = x;//赋值 
	r[idx] = r[k];  // idx.r->next = k.r->next  idx右边指向k+1 == k.r->next
	l[idx] = k;	//idx.l->next = k   idx左边指向k    
	l[r[k]] = idx; //k.r->next->l->next = (k+1).l->next = idx         
	r[k] = idx; //k.r->next = idx
}

//删除第k个点 
void remove()
{
	r[l[k]] = r[k]; //k.l->next->r->next(即(k-1).r->next) = k.r->next = (k+1)
	l[r[k]] = l[k]; //同理(k+1).l->next = k.l->next = (k-1)
}

int main()
{
	cin>>m;
	
	init();
	
	while(m--)
	{
		int k,int x;
		string op; cin>>op;
		if(op=="L")  
		{
			cin>>x;
			add(0,x);    
		}
		if(op=="R")
		{
			cin>>x;
			add(l[1],x);
		}
		if(op=="D")
		{
			cin>>x;
			remove(x+1);
		}
		if(op=="IL")
		{
			cin >> k >> x ;
			add(l[k+1],x);
		}
		if(op=="IR")
		{
			int k,x; cin>>k>>x;
			add(k+1,x);
		}
	}
	for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";
	return 0;
}



/*栈 
const int N = 100010
int stk[N] , tt;

//入栈 
stk[++tt ] = x;
if(tt > 0) not empty 
else empty  

//弹出 
tt-- ;
*/

/*队列
int q[N] ,hh,tt = -1;
//插入队尾 
q[ ++tt ] = x;

//队头弹出 
hh++;

//判断是否为空
if(hh < tt ) not empty
else empty 

//取队头元素
q[hh] 

单调栈(抽象但题型很少) 【左边第一个较小的数】

【单调栈/队列-最常解决的问题:给定序列,求每一个数的左边的离它最近的比它大/小的数(满足某种条件)
单调栈 (单调上升)
题目描述 :
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 -1。
输入输出 :
输入
5
3 4 2 7 5
输出
-1 3 -1 2 2

此题暴力 for (int i = 0;i <=n;i++) for(j = i - 1;j>=0;j–){找到左边第一个小于自身的元素;break} // j开始指向左边第一个元素,开始判断

【找左边的第一个小的元素,就把比右边大的元素删掉(因为有更小的数),肯定不是答案-没有用 - 得到单调栈 】
栈和队列里的存的不是元素值,而是元素下标

记:(在栈顶插入,弹出)

【插入】stk[++ tt] = x ;
【弹出删除】tt - - ;
【判断是否为空】if(tt > 0) not empty ; else empty;
【取栈顶】stk[tt] ; (tt初始值0,第一个入栈元素下标为1)




const int N = 100010;
int n;
int stk[N],tt;

int main()
{
	//cin.tie(0); 加速cin读取 
	//ios::sync_with_stdio(false);
	//cin >> n;
	scanf("%d",&n); 
	for(int i = 0;i < n;i++) //每个元素最多出栈一次,进栈一次:2n, 复杂度O(n)
	{
		int x;
		scanf("%d",&x);
		while(tt && stk[tt] >= x ) tt--;  //保证单调性,大的数放在前面  
		if(tt) cout << stk[tt] << " "; //栈不为空 (遍历到最后) tt != 0时打印
		else cout << -1 << " ";	//栈空了还未找到 
		
		stk[ ++tt] = x;		//加入x,保证前面的数小于x
	}
	
	return 0;
}

单调队列 【滑动窗口】

滑动窗口
问题描述:
给定一个大小为n ≤ 106的数组。
有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到k个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为[1 3 -1 -3 5 3 6 7],k为3。
窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7
输入格式:
输入包含两行。
第一行包含两个整数n nn和k kk,分别代表数组长度和滑动窗口的长度。
第二行有n nn个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式:
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
数据范围:
1 ≤ n , k ≤ 100000
输入样例:(元素长度,窗口大小)
8 3
1 3 -1 -3 5 3 6 7
输出样例:(第一行输出最小值,第二行输出最大值【窗口大小为3,即3个中的最小】)
-1 -3 -3 -3 3 3
3 3 5 5 6 7

每次滑动一格 : 【一进一出】队尾加进一个新元素,队头出队一个元素
q[tt]存放a数组的下标,取值用a[q[tt]] ,因此数组排序不变,队列可以循环使用数组判断

做题:
①先朴素算法模拟 :一进一出维护窗口大小k , 遍历窗口O(k)找最大最小
②优化:把没有用的元素删去,看是否具有单调性【最大最小值分开求】 (最小值:前面的数比后面的数大,则前面一定没有用,删掉,则变成单调递增,最小值一定在窗口的最左边,头指针)
最大值相反,构造单调递减,删去比前面大的元素,此时队头为最大值

记:(在队尾插入元素,在队头弹出元素)
int q[tt],hh,tt = -1; //初始值个人习惯

【插入】q[++tt] = x ;
【弹出】hh++ ;
【判断是否为空】if(hh <= tt) empty ; else empty;
【取队头元素】q[hh] , 队尾也可以取:q[tt]




const int N = 1000010;
int a[N],q[N];
int n,k;
 
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++)
	{
		//判断对头是否已经滑出窗口
		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("");
	
	//求最大值 - 从单调上升变成单调递减  条件改成 a[q[tt]] <= a[i]
	hh = 0,tt = -1;
	for(int i = 0;i < n;i++)
	{
		//判断对头是否已经滑出窗口(hh头指针-->向右滑动)
		if(hh <= tt && i - k + 1 > q[hh]) hh++;  //队列不为空 && 窗口位置不越界 滑到 a[n-k-1]为最后一次滑动 【弹出,向右滑动】
		
		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,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串 P 在模式串 S 中多次作为子串出现。
求出模板串 P 在模式串 S 中所有出现的位置的起始下标。
输入格式
第一行输入整数 N,表示字符串 P 的长度。
第二行输入字符串 P。
第三行输入整数 M,表示字符串 S 的长度。
第四行输入字符串 S。
输出格式
共一行,输出所有出现位置的起始下标(下标从 0 开始计数),整数之间用空格隔开。
数据范围
1≤N≤105
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2

先想
1.暴力(朴素)做法:s短串,p串 ,双重循环 for(int i = 0;i < n;i++) bool flag = true;
for(j = 1;j <= m;j++) {if(s[i] != p[j]) 如果不相等 flag = false ,break退出}

记:【p短n,s长m ,从1开始存 ,下标位于 [1,n] 和 [1,m] 】
【具体看代码:next数组过程与kmp非常类似,代码仅把s[i]换成自身p[i]即可】
【ne数组构造过程和kmp匹配过程类似,不相同不断回退:
for(i = 2,j = 0;i < n;i++),j=ne[j]//i = 1第一位失败不用退不用退,若判断if(q[i]==q[j+1]相等) j++ ,
, ne数组存放相同的前后缀长度(即回退下标),循环每轮求得 ne[i] = j】
【kmp过程】
模板串自身前缀相等的位置标记,在匹配失败回退时,只需退到相等前缀位置
再次判断下一位,不行再退j = ne[j]
for(int i = 1,j= 0 ; i <= m ; i ++ )
{
while( j&& s[i] != p[j + 1] ) j = ne[j] //退无可退 或者不匹配,p依据前后缀回退
if( s[i] == p[j + 1] ) j++; // i随着循环增加 , 比较下一位

if(j == n) //匹配成功
{按题目要求}
}

预处理出数组next数组 :
子串匹配失败时,要至少向后移动多少,才能和主串(已经匹配过的位置)匹配上
前面的位置和匹配失败的 到移动的位置与主串对应位相等 ,同时也与之前已经匹配的位置相等(等效)
模板串自身的后缀和前缀相等的长度最大值==即可以回溯的位置
用next[j] : 逐位匹配的下一位判断,若 s[i] != p[j+1] 时,j回退到next[j],next[j]数组维护前后缀的最长相等长度
即指向匹配模式串的j指针在不相等时,依据ne[j]数组不断回退,如果回退到0,即从0开始(退回模式串起点)

在这里插入图片描述


/*字符串匹配BF 
暴力+剪枝优化 朴素解法 
int s[N],p[N];//s较短串,p较长串
for(int i = 0;i < n;i++)
{
	bool flag = true;//是否成功
	for(int j = 1;j <= m;j++)
	{
		if(s[i] != p[j])
		{
			flag = false;
			break;
		}
	}
} 
*/



const int N = 10010,M = 100010;

int n,m;
char p[N],s[N];//模板主串,模式串  【p在s(长)中寻找匹配的片段】
int ne[N];//回溯数组(最长前后缀数组) 

int main()
{	//p长度n ,s长度m
	cin >> n >> p + 1 >> m >> s + 1;  // 模板主串长度 ,输入主串**(下标从1开始)** ,模式串长度 ,输入模式串(从1开始)
	
	
	//求next[j]过程(类似kmp匹配的过程)    //i = 1的时候没有前后缀,只有一个元素 ,不能包括自己(不用回退,i=2开始) 
	for(int i = 2,j = 0;i <= n;i ++)  
	{	//【回退条件:(j == 0不能再退了,最终等待i循环完退出) && (不匹配时需回退) 
		while( j && p[i] != p[j + 1]) j = ne[j];  //【条件和kmp匹配过程类似,自身找到相同第一个位置,j=ne[j],再判断if(相等) j++ , ne数组存放相同的前后缀长度,循环每轮求得 ne[i] = j】
		                                          //若j = 0就从头开始,不用回退了,执行判断语句即可
		if(p[i] == p[j + 1]) j++;     
		ne[i] = j;//第i个位置的最长前后缀相等长度 ,长度为j
	} 
	
	//kmp匹配过程 O(n),只看一个变量判断复杂度,i是m次(while循环j回溯等效至少每次减1,即最多执行m次)    // s(长)为 m 
	for(int i = 1,j = 0;i <= m;i++)//下标i=1开始匹配 ,next的j = 0开始存储回退下标next[0] = 0, next[1] = 0
	{	//【回退条件:(j != 0) && (不匹配时判断回退位置)
		while(j && s[i] != p[j + 1]) j = ne[j]; //若j = 0就从头开始,不用回退了,执行判断语句即可
		if( s[i] == p[j + 1] ) j++;//模式串已经匹配到的长度位置 
		
		if(j == n)//匹配成功  
		{	//【此处按题目要求】输出(所有)匹配成功的主串的第一位的位置下标
		//注意:我们存的是从1开始,但题目下标从0开始,所以第一位下标为i-n 
			printf("%d ",i - n);  //输出匹配成功的主串第一位的位置  ,即主串当前位置i减去模式串长度n加上1得到第一位下标 ,即i - n + 1   ,再减去1 ,等效从0开始存储的下标值
 			j = ne[j];//题目要求所有位置,成功后回退到第一个位置 p[(j =  0) + 1] 
			//j得到回退值,新一轮从头开始移动,s下标i仍继续增加 
		} 
	}
	
	return 0; 
}



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值