目录
前言
栈(stack)是一种后进先出(LIFO, last in first out)的数据结构,这是一种非常基础的数据结构,在很多地方,尤其是DFS和单调栈中有广泛的应用。
一、栈的结构
可以使用C++ STL中的stack实现,也可以自己模拟。
通常用一个数组和一个指针模拟一个栈,数组用于存储元素,指针top表示栈顶,入队时将top向后移动,出队时将top向前移动。
栈并不需要支持访问任意元素,所以在某些情况下会用链表模拟栈。
二、基本操作
bool InitStack(SqStack &S){
S.base = (Data *)malloc(STACK_SIZE*sizeof(Data));
if(!S.base) return false;
S.top = S.base;
S.stacksize = STACK_SIZE;
return true;
}
bool isEmptyStack(SqStack S){
if(S.top == S.base)
return true;
else
return false;
}
bool Push(SqStack &S, Data e){
if(S.top - S.base >= S.stacksize){
Data *p = (Data *)realloc(S.base, (S.stacksize+10)*sizeof(Data));
if(!p) return false;
S.base = p;
S.top = p + S.stacksize;
S.stacksize += 10;
}
*(S.top++) = e;
return true;
}
bool Pop(SqStack &S, Data &e){
if(isEmptyStack(S))
printf("这是空栈!\n");
e = *(--S.top);
return true;
}
bool ShowStack(SqStack &S){
while(--S.top!=S.base){
printf("%c",(*S.top).c);
}
}
三、栈的应用
1. 栈模拟
(1)出栈序列判断
栈模拟以该出栈序列为出栈时的具体过程,当不满足时,栈中元素将无法全部出栈
#include<bits/stdc++.h>
#include<stack>
using namespace std;
int m[] = {0, 5, 4, 6, 3, 2, 1}; //从m[1]开始
//按照1~n的顺序输入,实际任意有序序列都可以判断
bool JudgeStack(int a[], int n){
stack<int> st;
int index = 1;
for(int i = 1; i <= n; i++){
st.push(i); //正常栈输入,当输入到与出栈序列元素相等时,执行出栈,模拟该出栈序列实际的出栈过程
while((!st.empty()) && (st.top()==a[index])){
st.pop();
index++;
}
}
return st.empty();
}
int main(){
if(JudgeStack(m, 6))
cout << "true" << endl;
else
cout << "false" << endl;
return 0;
}
(2)表达式求值 Luogu P1175
给出中缀表达式求后缀表达式,并返回后缀表达式计算过程和计算结果。
方法一、用栈实现中缀转后缀和后缀计算
方法二、构建表达式二叉树求解
(3)判断括号是否匹配 Luogu P739
2.凸包/凸壳 Luogu P5155
可以用来求凸包,但是因为凸包需要访问最后22个元素,用以判断折线的方向,所以不能用STL的Stack
3.递归
递归算法都会用到栈,只不过系统会自动维护。
如果想使用非递归算法,那么就需要自己实现栈了。
4.单调栈
定义:
单调栈就是栈内元素单调按照递增(递减)顺序排列的栈,分为单调递增栈和单调递减栈。
适用问题:
在队列或数组中,我们需要通过比较前后元素的大小关系找寻到下一个比他大(小)的元素来解决问题时通常使用单调栈。进而可以在较小的复杂度内借助单调栈这一数据结构来解决此问题。
具体思路:
for遍历数组,
单调递增栈:找两侧第一个小于本身的元素使用
单调递减栈:找两侧第一个大于本身的元素使用
个人理解:
借助模板题 洛谷 P5788 单调栈 和后面的题解终于理清了遍历数组的过程时到底找寻的是哪一个元素的第一个大的最近元素是哪个元素。
有两种思路:首先该题要找大的,需要单调递减栈
(1)从前往后遍历,记录当前元素i是哪些元素的右边第一个大于的值,元素i是弹出的每个栈顶元素的右边第一个大于的元素;(出栈找到右边第一个大于的元素,需要在最后一个元素后面加上一个元素为INF,保证栈的元素都能弹出)
(2)从前往后遍历,考虑当前元素i的左边的第一个大于的值是哪一个元素,即一直出栈到不在出栈为止,因为是单调递减的,所以当前栈顶元素是左边第一个大于的元素。(入栈可以找到左边第一个大于的元素,若栈为空,则说明左边无第一个大于的元素)《这题因为是求单侧的因此这种思路是采取从后往前遍历的》
从以上两种思路就可以在一次遍历的过程中找到每个元素左右第一个大于或小于的元素。
此题代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 3e6 + 10;
int a[N], stk[N], top = 0, n = 0;
int ans[N];
int main(){
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
// for (int i = 1; i <= n; i++){
// while(top && a[stk[top]] < a[i]){ //单调递减,注意是否需要等于处理
// ans[stk[top--]] = i; //top的右边第一个最小值为i //出栈
// }
// stk[++top] = i;
// }
stk[0] = 0;
for (int i = n; i >= 1; i--){
while(top && a[stk[top]] < a[i]) top--;
ans[i] = stk[top]; //当栈空时,需要让stk[0]为0,找左边最大的值
stk[++top] = i;
}
for (int i = 1; i <= n; i++)
printf("%d ", ans[i]);
return 0;
}
单调栈的伪代码模板 :<不要死套模板>
/*
* 本伪代码对应的是单调递增栈
*共n个元素,编号为0~n-1
*/
while(栈为空) 栈顶元素出栈; //先清空栈
a[n]=-1;
for(i=0;i<=n;i++)
{
if(栈为空或入栈元素大于等于栈顶元素) 入栈; //当前元素i左边第一个小于的元素为此时栈顶top
else
{
while(栈非空并且栈顶元素小于入栈元素)
{
栈顶元素出栈;
更新结果; //每个出栈的栈顶元素右边第一个元素是当前元素i
}
将最后一次出栈的栈顶元素(即当前元素可以拓展到的位置)入栈;
更新最后一次出栈的栈顶元素其对应的值; //经历过弹出元素后,当前元素i左边第一个小于的元素为此时栈顶top
}
}
(1)视野问题 牛客 最优屏障 <单调递减栈> 也可以用同一种思路经过两次遍历,从前往后和从后往前
#include<bits/stdc++.h>
#include<stack>
using namespace std;
const int N = 5e4 + 7;
int a[N], v1[N], v2[N];
stack<int> st;
//v1[i]表示第1~i个的山所有前向防御力之和
//v2[i]表示第i~n个的山所有后向防御力之和
//每个山单侧的防御力计算是借助单调栈来计算的。
void show(int v1[], int v2[], int n){
for(int j = 1; j <= n; j++)
cout << v1[j] << v2[j] << endl;
}
int main(){
int T = 0, num;
scanf("%d", &T);
for(int i = 1; i <= T; i++){
int n = 0;
scanf("%d", &n);
for(int j = 1; j <= n; j++)
scanf("%d", &a[j]);
memset(v1, 0, sizeof(v1)); //初始化每组的防御力数组
memset(v2, 0, sizeof(v2));
while(!st.empty()) st.pop();
//前向防御力计算
for(int j = 1; j <= n; j++){
num = 0; //num计数i山的防御力数值
while(!st.empty() && st.top() < a[j]){
num++; //每去除一个小于的山顶,中间都是没有高山的,因此防御力+1
st.pop(); //为保持单调递减
}
if(st.empty())
v1[j] += v1[j - 1] + num;
else
v1[j] += v1[j - 1] + num + 1;
st.push(a[j]); //必须等计算完,再加当前数字
}
while(!st.empty()) st.pop();
//同理计算后向防御力前缀和
for(int j = n; j >= 1; j--){
num = 0;
while(!st.empty() && st.top() < a[j]){
num++;
st.pop();
}
if(!st.empty()) v2[j] += v2[j+1] + num + 1;
else v2[j] += v2[j+1] + num;
st.push(a[j]);
}
//v1[n] = v2[1],因为
int x = 0, c = 0;
for(int j = 1; j <= n; j++)
if(v1[n] - v1[j] - v2[j + 1] > c){
c = v1[n] - v1[j] - v2[j + 1];
x = j + 1;
}
printf("Case #%d: %d %d\n",i, x, c);
}
return 0;
}
/*
2
3
2 1 3
03
11
30
Case #1: 2 2
5
4 5 2 6 3
05
14
22
41
50
Case #2: 3 2
*/
(2)最大矩形面积 此部分代码来源于:https://blog.csdn.net/XxxxxM1/article/details/81324228
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
while(cin>>n)
{
int a[1010],s[1010],w[1010],p=0;
long long ans=0;
memset(a,0,sizeof(a));
memset(s,0,sizeof(s));
memset(w,0,sizeof(w));
for(int i=1; i<=n; i++) cin>>a[i];
for(int i=1; i<=n+1; i++)
{
if(a[i]>s[p]) s[++p]=a[i],w[p]=1; //因此栈顶元素就是当前左边第一个小于的,宽度就是本身的1
else
{
int width=0;
while(s[p]>a[i])
{
width+=w[p]; //在找寻右边第一个小于的元素过程,需要将每个大于的宽度加上
ans=max(ans,(long long)width*s[p]);
p--;
}
s[++p]=a[i],w[p]=width+1; //经历过栈弹出过程,找寻左边第一个小于元素,不仅要加上本身的1,还要加上弹出过程的总宽度
}
}
cout<<ans<<endl;
}
return 0;
}
(3)求最大区间
未完待续
总结
如有错误,欢迎大家评论区指正!