1. 栈结构在算法题中的实战应用
作为一名算法竞赛选手,我最近系统性地刷了一些栈相关的题目。栈这种"后进先出"的数据结构看似简单,但在解决某些特定问题时能发挥奇效。今天我想分享三道经典的栈应用题目,从括号匹配到后缀表达式求值,希望能帮助大家掌握栈的核心应用场景。
2. 括号匹配问题解析
2.1 问题描述与需求分析
P1739这道题要求我们检查一个表达式中括号是否匹配。题目给出的表达式包含小写字母、运算符和圆括号,以@符号结尾。我们需要判断所有左括号'('是否都有对应的右括号')'。
关键约束条件:
- 表达式长度<255
- 左括号数量<20
2.2 栈的解决方案设计
栈是解决这类匹配问题的理想数据结构。基本思路是:
- 遍历表达式中的每个字符
- 遇到左括号'('就压入栈
- 遇到右括号')'就弹出栈顶元素
- 如果弹出时栈为空,说明不匹配
- 遍历结束后检查栈是否为空
这种解法的时间复杂度是O(n),空间复杂度最坏情况下是O(n),但题目限制了括号数量,所以空间消耗很小。
2.3 代码实现与细节处理
cpp复制#include<bits/stdc++.h>
using namespace std;
string s ="";
stack<char> a;
int main(){
cin>>s;
for(char ch: s){
if(ch == '(') a.push(ch);
else if(ch == ')'){
if(a.empty()) {
cout<<"NO";
return 0;
}else {
a.pop();
}
}else if (ch =='@') break;
}
if(a.empty()){
cout<<"YES";
}else{
cout<<"NO";
}
return 0;
}
几个关键细节:
- 使用string存储整个表达式
- 使用标准库的stack容器
- 遇到@符号立即终止处理
- 最后检查栈是否为空来判断整体匹配情况
注意:题目保证输入以@结尾,所以不需要额外检查字符串边界。但在实际工程中,应该考虑各种异常输入情况。
3. 验证栈序列问题
3.1 问题理解与建模
P4387这道题给出了一个入栈序列和一个出栈序列,要求判断这个出栈序列是否可能由给定的入栈序列产生。这是一个经典的栈序列验证问题。
题目特点:
- 序列是1到n的排列(无重复数字)
- 多组测试数据(最多5组)
- 数据规模较大(n≤100000)
3.2 算法思路与优化
基本思路是模拟栈的操作过程:
- 初始化一个空栈和两个指针i,j分别指向pushed和poped序列的起始位置
- 不断将pushed[i]压入栈
- 当栈顶等于poped[j]时,弹出栈顶并移动j指针
- 最后检查栈是否为空
这个算法的时间复杂度是O(n),因为每个元素最多入栈出栈一次。
3.3 完整代码实现
cpp复制#include<bits/stdc++.h>
using namespace std;
int main(){
int q;
scanf("%d",&q);
while(q--){
int x =0;
scanf("%d",&x);
vector<int>c(x);
vector<int>d(x);
for(int i =0;i<x;i++){
scanf("%d", &c[i]);
}
for(int i =0;i<x;i++){
scanf("%d", &d[i]);
}
stack<int>a;
int i=0,j =0;
while(i<x && j<x ){
a.push(c[i]);
i++;
while(!a.empty()&& a.top() == d[j]){
a.pop();
j++;
}
}
if(a.empty()){
printf("Yes\n");
}else{
printf("No\n");
}
}
return 0;
}
几个优化点:
- 使用scanf/printf提高IO效率(数据量大时很重要)
- 使用vector存储序列,避免动态内存分配
- 双指针法减少不必要的操作
实际测试中发现,当n很大时,使用C风格IO比C++的cin/cout快很多。这在算法竞赛中是一个常见的优化技巧。
4. 后缀表达式求值
4.1 后缀表达式特点
P1449这道题要求我们计算一个后缀表达式(逆波兰表达式)的值。后缀表达式有以下特点:
- 操作符在操作数之后
- 不需要括号来指定优先级
- 计算顺序严格从左到右
例如:"3.5.2.-7.+"对应中缀表达式"3(5-2)+7"
4.2 求值算法设计
使用栈可以高效地处理后缀表达式:
- 初始化一个空栈
- 从左到右扫描表达式:
- 遇到数字:累积数字值,直到遇到分隔符'.'
- 遇到操作符:弹出栈顶两个元素,进行计算,将结果压回栈
- 最后栈中剩下的唯一元素就是结果
4.3 代码实现与边界处理
cpp复制#include<bits/stdc++.h>
using namespace std;
#define int long long
signed main(){
string s; stack<int>st; int num =0;
bool flag =true;
cin>>s;
for(char c :s){
if(c>='0'&&c<='9'){
num = num*10+(c-'0');
flag =true;
}else if(c == '.'){
st.push(num);
num =0;
flag = false;
}else if(c == '+'||c == '-'|| c == '*'||c == '/'){
int b = st.top();st.pop();
int a = st.top();st.pop();
int r =0;
if(c=='+') r = a+b;
else if(c=='-') r =a-b;
else if(c=='*') r =a*b;
else r=a/b;
st.push(r);
}else if(c=='@') break;
}
cout<<st.top();
return 0;
}
关键细节:
- 使用long long防止整数溢出
- 正确处理多位数(连续数字字符组成一个数)
- 除法向零取整(与C++默认行为一致)
- 使用标志位flag处理数字和分隔符的转换
特别注意:题目保证除数不为零,但在实际应用中应该检查除零错误。此外,虽然题目限制了表达式长度,但在工程实现中应该考虑更健壮的错误处理。
5. 栈应用的总结与思考
通过这三道题目,我们可以看到栈在算法问题中的几种典型应用场景:
- 括号匹配:利用栈的LIFO特性,可以高效检查嵌套结构的匹配情况
- 序列验证:模拟栈的操作过程,验证某种操作序列的合法性
- 表达式求值:后缀表达式天然适合用栈来处理
在实际编程中,栈的应用远不止这些。比如:
- 函数调用栈
- 浏览器历史记录
- 撤销操作实现
- 深度优先搜索
我个人的经验是,当问题涉及"最近相关"或"嵌套结构"时,就应该考虑使用栈。例如处理XML/HTML标签、计算器实现、语法分析等场景。
对于算法竞赛选手来说,熟练掌握栈的各种应用模式可以快速解决一大类问题。建议初学者多练习类似的题目,培养对数据结构的敏感度。