模拟栈
栈是一种常用的数据结构
他的逻辑是先进后出
就比如我们几个人走到一个死胡同,先进去的人只能在里面等后面进去的人出去才能出来
这里用数组实现一个静态的栈
Acwing:828.模拟栈
实现一个栈,栈初始为空,支持四种操作:
push x – 向栈顶插入一个数 x ;
pop – 从栈顶弹出一个数;
empty – 判断栈是否为空;
query – 查询栈顶元素。
现在要对栈进行 M个操作,其中的每个操作 3 和操作 4都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式
对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示栈顶元素的值。
数据范围
1≤M≤100000,
1≤x≤109
所有操作保证合法。
输入样例:
10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty
输出样例:
5
5
YES
4
NO
#include <iostream>
using namespace std;
const int N=100010;
int stk[N],tt=0;//栈和栈顶指针(和书本写法还是有yi点区别的 但是最终实现效果一样)
void push(int x)//压栈
{
stk[++tt]=x;
}
void pop()//出栈
{
tt--;
}
int head()//读取栈顶元素
{
return stk[tt];
}
bool isempty()//判断栈是否为空
{
if(tt)return false;
return true;
}
int main(void)
{
int m;
cin>>m;
while(m--)
{
string op;
cin>>op;
if(op=="push")
{
int x;
cin>>x;
push(x);
}
if(op=="pop")
{
pop();
}
if(op=="empty")
{
if(isempty())puts("YES");
else puts("NO");
}
if(op=="query")
cout<<head()<<endl;
}
return 0;
}
单调栈
插入元素的同时保证栈里有一定的单调性
比如我们遇到这样的问题: 对于一个数组 遍历每个元素时候,要求找到第一个比他小的元素,用朴素的做法就是写一个双重循环,第一个循环遍历数组元素,第二个循环就是从后往前遍历,从而找到答案。
有没有什么办法可以优化一下呢,我们发现有的数始终用不到,举个例子,546,现在我们要找6的第一个比他小的数,答案就是4,因为5在4的前面,只要4在的一天,5永远没有出头之日,因此5就没用了。
我们可以用一个栈来实现这个单调性, 就是让那些所谓"在4前面没用的元素"给弹掉,然后只留下所谓的"4",然后会发现这样做以后,栈里面留下的都是比新插入元素小的,这样就实现了单调性!
下面看例题
Acwing:830. 单调栈
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
输入格式
第一行包含整数 N,表示数列长度。
第二行包含 N个整数,表示整数数列。
输出格式
共一行,包含 N个整数,其中第 i 个数表示第 i 个数的左边第一个比它小的数,如果不存在则输出 −1。
数据范围
1≤N≤105
1≤数列中元素≤109
输入样例:
5
3 4 2 7 5
输出样例:
-1 3 -1 2 2
#include <iostream>
using namespace std;
const int N = 100010;
int stk[N],tt=0;
int main(void)
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
int x;
scanf("%d",&x);
while(tt&&stk[tt]>=x)tt--;
//如果比x大 弹出了 反正后面x放进来 x就是比这些弹出去的更优 x在这些不弹出去留着也没用
if(tt)printf("%d ",stk[tt]);
else printf("-1 ");
stk[++tt]=x;
}
return 0;
}
还是有点模糊的可以看一下这个
/*
基本思想:
①先用暴力作法发现要用两重循环,时间复杂度高,为了降低时间复杂度要发现一种性质。会发现当输入一个新的数时,前面比它大的数都不会再用到。
②可用栈,在每次输入的数入栈前,将栈中比它大的数都删掉,使栈变成从栈底到栈顶单调递增的单调栈,此时的栈顶便是比它小且离它最近的数。
朴素算法是i遍历一遍 然后内层循环往前遍历 找到第一个比它小的 break
这个用单调队列
3 4 2 7 5
= = = = =
i j x
如果i<j a[i]>=a[j] 且都<x 那么a[i]永远不会被用到
所以可以把a[i]弹出 后面插入更好的x 这样是也保证了是单调的
就是
3 存下来 输出-1 当前队列 3
3<4 输出3 4 存下来 当前队列 3 4
4>2 pop 3>2 pop 输出-1 2存下来 当前队列 2
2<7 2 7存下来 当前队列 2 7
7>5 pop 2<5 输出2 5存下来 当前队列 2 5
*/
模拟队列
队列是一种常用的数据结构
他的逻辑就是先进先出
就像排队出门口一样 排前面的可以先出去 排后面的后出去
这里用数组实现一个静态的队列
Acwing:829. 模拟队列
实现一个队列,队列初始为空,支持四种操作:
push x – 向队尾插入一个数 x;
pop – 从队头弹出一个数;
empty – 判断队列是否为空;
query – 查询队头元素。
现在要对队列进行 M
个操作,其中的每个操作 3 和操作 4都要输出相应的结果。
输入格式
第一行包含整数 M,表示操作次数。
接下来 M行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。
输出格式
对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。
其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示队头元素的值。
数据范围
1≤M≤100000,
1≤x≤109,
所有操作保证合法。
输入样例:
10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6
输出样例:
NO
6
YES
4
#include <iostream>
using namespace std;
const int N=100010;
int q[N],hh,tt=-1;//队列 队头指针 队尾指针
void push(int x)//进队
{
q[++tt]=x;
}
void pop()//出队
{
hh++;
}
bool isempty()//判空
{
if(hh<=tt)return false;//队头在前面 队尾在后面 这样就不为空
return true;
}
int head()//读取队头元素
{
return q[hh];
}
int main(void)
{
int m;
cin>>m;
while(m--)
{
string op;
cin>>op;
if(op=="push")
{
int x;
cin>>x;
push(x);
}
if(op=="pop")
{
pop();
}
if(op=="empty")
{
if(isempty()) puts("YES");
else puts("NO");
}
if(op=="query")
cout<<head()<<endl;
}
return 0;
}
单调队列(双端队列deque)
让队列元素保持单调性
给定一个数组,每次在一个范围里面找到他的最小值(范围会改变)
接下来用一个例题来体会一下单调队列是如何运用的 道理和单调栈差不多
Acwing 154.滑动窗口
给定一个大小为 n≤106
的数组。
有一个大小为 k
的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k
个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],k为 3。
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数 n和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例:
8 3
1 3 -1 -3 5 3 6 7
输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7
我们可以发现 如果我们要求最小值 举个例子 1 3 -1 最小值-1 然后窗口往前移动
3 -1 -3 我们可以发现 3 在-1前面 也是没啥用了
重点理解注释
#include <iostream>
using namespace std;
const int N=1000010;
int a[N];
int main(void)
{ int n,k;
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++)cin>>a[i];//存元素
int q[N],hh=0,tt=-1;
//维护单调递增 队头最小
for(int i=0;i<n;i++)//滑入窗口的元素指针
{
if(hh<=tt&&q[hh]<i-k+1)hh++;//如果窗口划过队头 说明队头出队了
//队头可能是上一个窗口中任意一个元素的下标! 不一定就是窗口中最左边的那个
//自己考虑一下如果加入新元素 把队列所有元素都弹出的情况 这时候tt会
//先越过hh 然后++tt 然后队列剩下一个新加入的元素 这时候tt hh位置一样他们存同一个下标
//并且这个下标比窗口左端大呢
while(hh<=tt&&a[q[tt]]>=a[i])tt--;//如果队不空 并且前面没用 弹出
q[++tt]=i;//存下标 下标有序
if(i-k+1>=0)printf("%d ",a[q[hh]]);//窗口长度够才输出
}
puts("");//换行
//清空队
hh=0,tt=-1;
//维护单调递减队头最大
for(int i=0;i<n;i++)
{
if(hh<=tt&&q[hh]<i-k+1)hh++;
//窗口每次只往后挪一格
//所以队列往前一格就行了 下一格的下标绝对在队列里 因为最坏队列存的也是
//举个例子 [1 2 3]4 ->1[2 3 4] 窗口这么移动 队列弹去1还有2这是最坏的
//如果不是这样 hh++ q[hh]可能像前面说的 可能指的别的位置了 是合法的(在新窗口)
while(hh<=tt&&a[q[tt]]<=a[i])tt--;
q[++tt]=i;
if(i-k+1>=0)printf("%d ",a[q[hh]]);
}
return 0;
}
标记一下(讲解来源:董晓+yxc)