栈 stack
今天我们来学习数据结构中最简单的——栈
引入
一只NOI选手csk因在NOI赛场上忘开
l
o
n
g
l
o
n
g
longlong
longlong,本来打出了正解的题目只拿了
30
p
t
s
30pts
30pts,结果离金牌只差了
10
p
t
s
10pts
10pts,从而没有与心念的清北签约,加上高考的失利,走投无路的他来到食堂打工 (十年OI一场空,不开longlong见祖宗)
他当上了一名洗碗工,每天都要洗盘子
第一个人吃完饭后,送来了 1 , 2 , 3 1,2,3 1,2,3号盘子,csk将这些盘子依次摆放,很快他就先后洗完了最上面的第 3 , 2 3,2 3,2号盘子,随后,又一位客人过来,放上了第 4 , 5 4,5 4,5号盘子,csk继续从上往下洗盘子,很快就洗完了
这种后进先出的数据结构,就是栈
栈的概念
栈是一种后进先出的数据结构,支持 入栈(push),出栈(pop)和访问栈顶(top) 三个操作,新入栈的元素会被后入栈的元素压到下面。
代码实现
为了方便理解,代码以结构体和函数的形式
定义和初始化(init)
我们只需用一个数组和一个变量即可实现栈
struct stack{
int s[MAX];//存放栈
int top;//指向栈顶的下标,初始为0
};
入栈(push)
将该元素 a a a放入栈顶,即将其放到数组的最后一个位置
void push(int a){
s[++top] = a;
}
出栈(pop)
将栈顶元素弹出,仅需将数组最后一个元素的指针 − 1 -1 −1即可
void pop(){
if(top)//判断栈是否为空
top--;
}
访问栈顶元素(query)
直接返回数组的最后一个元素即可,栈为空返回-1
int query(){
if(top)
return s[top];
return -1;
}
查询栈的元素个数(size)
其实元素个数就是top的值
int size(){
return top;
}
判断是否为空(empty)
栈为空,返回1,否则返回0
bool empty(){
if(top)
return true;
return false;
}
当然也可写作
bool empty(){
return top == 0;
}
清空栈(clear)
将数组最后元素的指针改为0即可
void clear(){
top = 0;
}
完整模板
将以上和在一起即可
struct stack{
int s[MAX];//存放栈
int top;//指向栈顶的下标
void push(int a){
//入栈
s[++top] = a;
}
void pop(){
//出栈
if(top)
top--;
}
int query(){
//访问栈顶
return s[top];
}
int size(){
//返回栈元素个数
return top;
}
bool empty(){
//判断栈是否为空
return top == 0;
}
void clear(){
//清空栈
top = 0;
}
};
练习题
请你实现一个栈(stack),支持如下操作:
push(x)
:向栈中加入一个数 x x x。pop()
:将栈顶弹出。如果此时栈为空则不进行弹出操作,输出Empty
。query()
:输出栈顶元素,如果此时栈为空则输出Anguei!
。size()
:输出此时栈内元素个数。
STL的栈
STL也为我们提供了栈
头文件 stack
#include <stack>
定义
stack <变量类型> 栈名;
stack <int> s;//定义一个名为s,int类型的栈
常见操作
支持的操作类似模板
stack <int> s; int x;
s.push(x);//在栈顶压入变量x
s.pop();//栈顶出栈
s.top();//返回栈顶
s.size();//返回栈元素个数
s.empty();//判断栈是否为空
s.clear();//清空栈
用STL的栈完成例题
#include<bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false);
int T;
cin >> T;
while(T--){
int n;
cin >> n;
stack <unsigned long long> a;
string b;
for(int i=1;i<=n;i++){
cin >> b;
if(b == "push"){
unsigned long long c;
cin >> c;
a.push(c);
}
else if(b == "pop"){
if(a.empty()) cout << "Empty\n";
else a.pop();
}
else if(b == "query"){
if(a.empty()) cout << "Anguei!\n";
else cout << a.top() << "\n";
}
else cout << a.size() << "\n";
}
}
return 0;
}
表达式计算
表达式的计算是栈的一大用处。
对于算术表达式,有前缀表达式,中缀表达式,后缀表达式(注:
o
p
t
opt
opt为运算符,
x
,
y
x,y
x,y表示数字)
中缀表达式: 就是我们数学中最常使用的一种表达式,例如
1
+
(
2
−
3
)
1+(2-3)
1+(2−3)
前缀表达式: 又称波兰式,就是将运算符放到数字前面,形如
o
p
t
x
y
opt \ x \ y
opt x y 表示
x
o
p
t
y
x \ opt \ y
x opt y,例如
+
1
−
2
3
+\ 1-2 \ \ 3
+ 1−2 3
后缀表达式: 又称逆波兰式,就是将运算符放到数字后面,形如
x
y
o
p
t
x \ y \ opt
x y opt 表示
x
o
p
t
y
x \ opt \ y
x opt y,例如
2
3
−
1
+
2 \ \ 3 - 1 \ +
2 3−1 +
对于计算机而言,我们最常使用的是后缀表达式,可以用栈
O
(
l
e
n
)
O(len)
O(len)(len表示表达式长度)求出它的值
后缀表达式求值
如何求后缀表达式的值?
- 先建立一个栈,并扫描一遍表达式
-(1)如果遇到一个数,就把它入栈
-(2)如果遇到一个运算符,就取出栈顶的两个元素进行计算,再把结果入栈- 最后栈中只剩一个数,这个数就是表达式的值
例题(洛谷P1449)
所谓后缀表达式是指这样的一个表达式:式中不再引用括号,运算符号放在两个运算对象之后,所有计算按运算符号出现的顺序,严格地由左而右新进行(不用考虑运算符的优先级)。
如:
3*(5-2)+7
\texttt{3*(5-2)+7}
3*(5-2)+7 对应的后缀表达式为:
3.5.2.-*7.+@
\texttt{3.5.2.-*7.+@}
3.5.2.-*7.+@。在该式中,@
为表达式的结束符号。.
为操作数的结束符号。
long long now=0;
while(cin >> ch ){
if(ch=='@')
break;
if(ch>='0' && ch<='9'){
now=now*10+ch-'0';
continue;
}
else if(ch=='.'){
sta[++top]=now;
now=0;
continue;
}
else{
if(ch=='+'){
sta[top-1]+=sta[top];
--top;
}
else if(ch=='-'){
sta[top-1]-=sta[top];
--top;
}
else if(ch=='*'){
sta[top-1]*=sta[top];
--top;
}
else if(ch=='/'){
sta[top-1]/=sta[top];
--top;
}
}
}
cout<<sta[1];
中缀表达式转成后缀表达式
中缀表达式转成后缀表达式
- 先建立一个栈用来存储运算符,扫描一遍表达式的元素
-(1)如果遇到一个数,直接输出这个数
-(2)如果遇到左括号,把左括号入栈
-(3)如果遇到右括号,不断取出栈顶并输出,直到栈顶为左括号,再把左括号出栈
-(4)如果遇到运算符,只要栈顶运算符的优先级不低于新运算符,就不断输出并弹出栈顶,最后把新符号入栈- 最后依次输出并弹出栈顶,输出的序列就是一个与原中缀表达式等价的后缀表达式
代码实现, n > 0 n>0 n>0
char ch;//中缀表达式
unsigned long long num = 0;
map <char,int> mp;//存储运算符优先级
mp['+'] = 1; mp['-'] = 1; mp['*'] = 2; mp['/'] = 2;
//有需要自己写
stack <char> s;
while(cin >> ch){
if(ch >= '0' && ch <= '9')
num = num*10+ch-'0';
else{
if(num){
cout << num << " ";
num = 0;
}
if(ch == '(')
s.push(ch);
else if(ch == ')'){
while(s.top() != '('){
cout << s.top() <<" ";
s.pop();
}
s.pop();
}
else{
while(!s.empty()&&mp[s.top()]>=mp[ch]){
cout<< s.top() << " ";
s.pop();
}
s.push(ch);
}
}
}
if(num > 0) cout << num << " ";
while(!s.empty()){
cout << s.top() << " ";
s.pop();
}
括号序列
栈也经常用来匹配括号序列
对于括号序列,我们可以用类似于中缀表达式的括号处理,遇到左括号入栈,遇到右括号判断栈顶是不是不这个右括号匹配,是就出栈,不是则不合法,读者可以自己模拟一下这个过程
string str;
cin >> str;
stack <char> s
for(int i=0;i<str.size();i++){
if(str[i] == '(' || str[i] == '[' || str[i] == '{'){
s.push(str[i]);
else{
if(str[i] == ')' && s.top() == '('){
s.pop(); continue;
}
if(str[i] == ']' && s.top() == '['){
s.pop(); continue;
}
if(str[i] == '}' && s.top() == '{'){
s.pop(); continue;
}
cout << "no";
return 0;
}
}
if(s.empty() cout << "yes";
else cout << "no";
单调栈
顾名思义,就是栈内元素具有单调性的一种栈
单调栈一般用来维护最大或最小值
现以单调递减栈为例,模拟一遍单调栈的过程
- 建立一个栈,扫描一遍待操作元素
-(1)如果栈内没有元素,直接入栈
-(2)如果栈顶元素比新元素大,否则持续弹出栈顶元素,直到栈顶元素比新元素大或栈为空为止,此时被弹出元素之后第一个比它大的元素即为这个新元素,最后记得把新元素入栈
-(3)栈底的元素即为被扫过元素的最大值
stack <int> s;
//a为待操作元素
for(int i=1;i<=n;i++){
while(!s.empty() && a[i] > a[s.top()])
s.pop();
s.push(i);
}
经典例题——Largest Rectangle in a Histogram
如图所示,在一条水平线上有
n
n
n 个宽为
1
1
1 的矩形,求包含于这些矩形的最大子矩形面积(图中的阴影部分的面积即所求答案)。
对于这一道题,我们发现:
如果一个矩形的高度低于上一个矩形的高度,那么上一个矩形比它高的部分在之后将毫无用处
则这些没用的部分我们就可以删掉
所以,我们可以建立一个单调栈维护矩形高度,每弹出一个矩形,就把该高度矩形宽度
+
1
+1
+1,并用宽度乘高度来更新答案
#include <iostream>
#include <cstring>
#define ull unsigned long long
using namespace std;
const int MAX = 1e5+10;
int top;
long long w[MAX],s[MAX],ans;
int main(){
ios::sync_with_stdio(false);
int n;
while(cin >> n){
top = 0; ans = 0;
memset(w,0,sizeof(w));
if(n == 0) break;
for(int i=1;i<=n+1;i++){
long long h;
if(i == n+1) h = 0;
else cin >> h;
if(h > s[top] || !top){
s[++top] = h;
w[top] = 1;
}
else{
long long width = 0;
while(top && h <= s[top]){
width += w[top];
ans = max(ans,width*s[top]);
top--;
}
s[++top] = h; w[top] = width+1;
}
}
cout << ans << "\n";
}
return 0;
}
练习题
请自行完成以下练习
表达式的值(提示:动态规划)
总结
栈(stack)作为大多数人学的第一个算法 (也是最简单的一个) 它的应用十分广泛,最主要就是用于表达式的计算 (其它好像没什么用)
本文字数5000,用时约6小时,求个三连不过分吧