1. 栈与队列OJ问题详解:从数据结构基础到算法实战
栈和队列作为计算机科学中最基础的两种线性数据结构,在各类在线判题系统(OJ)中出现的频率极高。无论是校招笔试、算法竞赛还是日常编程练习,掌握它们的特性和应用场景都至关重要。我参加过十余场算法竞赛,处理过上百道栈与队列相关的OJ题目,今天就来系统梳理这两类问题的解题套路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据结构核心特性对比
2.1 栈的FILO特性与典型应用
栈(Stack)遵循后进先出(LIFO)原则,就像我们叠放的一摞盘子,最后放上去的总是最先被取用。在C++中通常用stack<int> s声明,主要操作包括:
cpp复制s.push(1); // 入栈
s.pop(); // 出栈(不返回元素)
s.top(); // 获取栈顶元素
s.empty(); // 判空
经典应用场景包括:
- 函数调用栈(递归转化为非递归)
- 括号匹配(LeetCode 20)
- 表达式求值(逆波兰表达式)
- 浏览器前进后退功能
2.2 队列的FIFO特性与变种实现
队列(Queue)遵循先进先出(FIFO)原则,就像排队买票的队伍。C++中的基础队列:
cpp复制queue<int> q;
q.push(1); // 入队
q.pop(); // 出队
q.front(); // 队首元素
特殊队列变种:
- 双端队列(Deque):两端都可操作
cpp复制deque<int> dq; dq.push_front(1); dq.pop_back(); - 优先队列(Priority Queue):自动排序
cpp复制priority_queue<int> pq; // 大顶堆
3. OJ高频题型解题套路
3.1 栈的典型问题解法
3.1.1 括号匹配问题(LeetCode 20)
cpp复制bool isValid(string s) {
stack<char> st;
for(char c : s) {
if(c == '(' || c == '[' || c == '{') {
st.push(c);
} else {
if(st.empty()) return false;
char top = st.top();
if((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
st.pop();
}
}
return st.empty();
}
关键点:遇到右括号时必须检查栈顶是否匹配的左括号
3.1.2 每日温度(LeetCode 739)
维护单调递减栈的经典应用:
cpp复制vector<int> dailyTemperatures(vector<int>& T) {
stack<int> st;
vector<int> res(T.size());
for(int i = 0; i < T.size(); ++i) {
while(!st.empty() && T[i] > T[st.top()]) {
res[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
return res;
}
3.2 队列的典型问题解法
3.2.1 滑动窗口最大值(LeetCode 239)
使用双端队列维护窗口最大值:
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> dq;
vector<int> res;
for(int i = 0; i < nums.size(); ++i) {
// 移除超出窗口范围的元素
if(!dq.empty() && dq.front() == i - k) {
dq.pop_front();
}
// 维护单调递减队列
while(!dq.empty() && nums[dq.back()] < nums[i]) {
dq.pop_back();
}
dq.push_back(i);
// 记录结果
if(i >= k - 1) {
res.push_back(nums[dq.front()]);
}
}
return res;
}
3.2.2 用队列实现栈(LeetCode 225)
cpp复制class MyStack {
queue<int> q;
public:
void push(int x) {
int n = q.size();
q.push(x);
for(int i = 0; i < n; ++i) {
q.push(q.front());
q.pop();
}
}
int pop() {
int val = q.front();
q.pop();
return val;
}
};
4. 进阶技巧与优化策略
4.1 单调栈的四种变形
- 严格单调递增栈:遇到相等元素也弹出
- 非严格单调递增栈:允许相等元素存在
- 严格单调递减栈
- 非严格单调递减栈
示例:柱状图中最大矩形(LeetCode 84)就需要严格递增栈:
cpp复制int largestRectangleArea(vector<int>& heights) {
heights.push_back(0); // 哨兵
stack<int> st;
int maxArea = 0;
for(int i = 0; i < heights.size(); ) {
if(st.empty() || heights[i] > heights[st.top()]) {
st.push(i++);
} else {
int h = heights[st.top()]; st.pop();
int w = st.empty() ? i : i - st.top() - 1;
maxArea = max(maxArea, h * w);
}
}
return maxArea;
}
4.2 双向BFS中的队列应用
在广度优先搜索中,使用双队列可以显著提升性能:
cpp复制int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> dict(wordList.begin(), wordList.end());
if(!dict.count(endWord)) return 0;
unordered_set<string> beginSet, endSet, *set1, *set2;
beginSet.insert(beginWord);
endSet.insert(endWord);
int len = 1;
while(!beginSet.empty() && !endSet.empty()) {
if(beginSet.size() > endSet.size()) {
set1 = &beginSet;
set2 = &endSet;
} else {
set1 = &endSet;
set2 = &beginSet;
}
unordered_set<string> tmp;
for(string word : *set1) {
for(int i = 0; i < word.size(); ++i) {
char old = word[i];
for(char c = 'a'; c <= 'z'; ++c) {
word[i] = c;
if(set2->count(word)) return len + 1;
if(dict.count(word)) {
tmp.insert(word);
dict.erase(word);
}
}
word[i] = old;
}
}
len++;
*set1 = tmp;
}
return 0;
}
5. 常见错误与调试技巧
5.1 栈溢出问题排查
- 递归转非递归时忘记压栈关键参数
- DFS算法中缺少visited标记导致无限循环
- 单调栈处理时未考虑相等元素的情况
5.2 队列使用的典型错误
- 优先队列自定义比较函数错误:
cpp复制// 正确的小顶堆定义方式
priority_queue<int, vector<int>, greater<int>> minHeap;
- BFS中忘记记录已访问节点导致重复计算
- 滑动窗口问题中窗口大小维护错误
5.3 内存与性能优化
- 预先分配vector容量避免动态扩容
cpp复制vector<int> res; res.reserve(nums.size()); // 预先分配 - 使用数组模拟栈/队列提升性能:
cpp复制int stack[1000], top = -1; stack[++top] = 1; // push int val = stack[top--]; // pop - 在数据量极大时,改用位运算等压缩状态
6. 实战训练建议
-
基础巩固阶段(建议2周):
- 每天完成3道栈相关题目
- 每天完成3道队列相关题目
- 重点掌握:括号匹配、表达式求值、滑动窗口
-
进阶提升阶段(建议3周):
- 每周完成2道单调栈困难题
- 研究双向BFS在各种场景的应用
- 尝试用栈/队列优化动态规划问题
-
竞赛冲刺阶段:
- 限时完成AtCoder/Codeforces上的栈/队列专题比赛
- 学习使用栈实现Tarjan算法等高级应用
- 掌握如何用队列优化网络流算法
我个人的经验是,在解决栈相关问题时,一定要在纸上画出入栈出栈过程;处理队列问题时,最好用实际例子模拟队列变化。曾经在解决一道单调栈问题时,因为没有正确处理相等元素的情况,调试了整整3小时,这个教训让我深刻理解了"魔鬼在细节中"的含义。
