栈与队列通关八题:从括号匹配到接雨水,手撕LeetCode高频题(Python + C++)
栈和队列是数据结构的基础,也是面试笔试的高频考点。本文整理了8道经典题目,每道题都包含:题目描述、解题思路、图解(文本示意)、Python代码、C++代码。全部理解后,栈和队列类题目基本通关。
📌 题目清单
| 题号 | 题目 | 核心考点 |
|---|---|---|
| 20 | 有效的括号 | 栈匹配 |
| 155 | 最小栈 | 辅助栈 / 差值法 |
| 150 | 逆波兰表达式求值 | 栈计算 |
| 232 | 用栈实现队列 | 双栈模拟 |
| 239 | 滑动窗口最大值 | 单调队列(双端队列) |
| 739 | 每日温度 | 单调栈(下一个更大元素) |
| 84 | 柱状图中最大的矩形 | 单调栈 + 哨兵 |
| 42 | 接雨水 | 单调栈 / 双指针 |
1. 有效的括号(LeetCode 20)
题目描述
给定一个只包含 '(', ')', '{', '}', '[', ']' 的字符串,判断字符串是否有效。
有效条件:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
示例 :
输入:"()[]{}" → 输出:true
输入:"([)]" → 输出:false
解题思路
- 遍历字符串,遇到左括号则压入栈。
- 遇到右括号时,检查栈顶是否为对应的左括号:
- 如果是,弹出栈顶。
- 如果不是或栈为空,返回
false。
- 遍历结束后,栈为空则有效。
图解
字符串: ( [ ] { } )
栈变化:
( → 栈: [
( [ → 栈: [ [, [
遇到 ] → 栈顶是 [,匹配,弹出 → 栈: [
遇到 } → 栈顶是 {,匹配,弹出 → 栈: (
遇到 ) → 栈顶是 (,匹配,弹出 → 栈: 空
最终栈空 → true
Python代码
python
def isValid(s: str) -> bool:
mapping = {')': '(', ']': '[', '}': '{'}
stack = []
for ch in s:
if ch in mapping:
if not stack or stack[-1] != mapping[ch]:
return False
stack.pop()
else:
stack.append(ch)
return not stack
C++代码
cpp
class Solution {
public:
bool isValid(string s) {
stack<char> st;
unordered_map<char, char> mapping = {{')', '('}, {']', '['}, {'}', '{'}};
for (char ch : s) {
if (mapping.count(ch)) {
if (st.empty() || st.top() != mapping[ch]) return false;
st.pop();
} else {
st.push(ch);
}
}
return st.empty();
}
};
2. 最小栈(LeetCode 155)
题目描述
设计一个支持 push、pop、top 操作,并能在常数时间内检索到最小元素的栈。
实现 MinStack 类:
push(val)将元素压入栈pop()删除栈顶元素top()获取栈顶元素getMin()获取栈中的最小元素
解题思路:辅助栈
维护一个数据栈 st 和一个最小栈 minSt,minSt 的栈顶始终存储当前数据栈中的最小值。
push:数据栈直接压入,最小栈压入当前最小值(min(当前值, minSt栈顶))pop:两个栈同时弹出getMin:返回minSt栈顶
图解
操作: push 3, push 5, push 2, getMin, pop, getMin
数据栈: 3 3,5 3,5,2 3,5 3
最小栈: 3 3,3 3,3,2 3,3 3
getMin: 3 -> 3 -> 2 -> 2 -> 3
Python代码
python
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self) -> None:
if self.stack.pop() == self.min_stack[-1]:
self.min_stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
C++代码
cpp
class MinStack {
stack<int> st;
stack<int> minSt;
public:
MinStack() {}
void push(int val) {
st.push(val);
if (minSt.empty() || val <= minSt.top()) minSt.push(val);
}
void pop() {
if (st.top() == minSt.top()) minSt.pop();
st.pop();
}
int top() { return st.top(); }
int getMin() { return minSt.top(); }
};
3. 逆波兰表达式求值(LeetCode 150)
题目描述
给你一个字符串数组 tokens ,表示一个逆波兰表达式,求表达式的值。
有效运算符为 '+'、'-'、'*'、'/'。除法向零截断。
示例 :
输入:tokens = ["2","1","+","3","*"]
输出:9
解释:((2 + 1) * 3) = 9
解题思路
- 遍历 tokens,遇到数字压栈。
- 遇到运算符,弹出两个数字(先弹出右操作数,后弹出左操作数),计算结果后压回栈。
- 最后栈顶即为结果。
图解
tokens: 2, 1, +, 3, *
栈:
2 → [2]
1 → [2,1]
+ → 弹出1,2 → 2+1=3 → 压3 → [3]
3 → [3,3]
* → 弹出3,3 → 3*3=9 → [9]
结果:9
Python代码
python
def evalRPN(tokens):
stack = []
for t in tokens:
if t in {"+", "-", "*", "/"}:
b = stack.pop()
a = stack.pop()
if t == "+": stack.append(a + b)
elif t == "-": stack.append(a - b)
elif t == "*": stack.append(a * b)
else: stack.append(int(a / b)) # 向零截断
else:
stack.append(int(t))
return stack[0]
C++代码
cpp
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
for (string& t : tokens) {
if (t == "+" || t == "-" || t == "*" || t == "/") {
int b = st.top(); st.pop();
int a = st.top(); st.pop();
if (t == "+") st.push(a + b);
else if (t == "-") st.push(a - b);
else if (t == "*") st.push(a * b);
else st.push(a / b);
} else {
st.push(stoi(t));
}
}
return st.top();
}
};
4. 用栈实现队列(LeetCode 232)
题目描述
请你仅使用两个栈实现先入先出队列。实现 MyQueue 类:
push(x)将元素 x 推到队列的末尾pop()从队列的开头移除并返回元素peek()返回队列开头的元素empty()判断队列是否为空
解题思路:双栈模拟
- 输入栈
inSt用于push。 - 输出栈
outSt用于pop和peek。 - 当
outSt为空时,将inSt的所有元素弹出并压入outSt,这样outSt的栈顶就是队首元素。
图解
push 1,2,3:
inSt: [1,2,3] (栈顶3), outSt: []
pop:
outSt为空 → 将inSt全部移到outSt:
inSt: [], outSt: [3,2,1] (栈顶1)
pop弹出1 → outSt: [3,2]
Python代码
python
class MyQueue:
def __init__(self):
self.in_st = []
self.out_st = []
def push(self, x: int) -> None:
self.in_st.append(x)
def pop(self) -> int:
self._move()
return self.out_st.pop()
def peek(self) -> int:
self._move()
return self.out_st[-1]
def empty(self) -> bool:
return not self.in_st and not self.out_st
def _move(self):
if not self.out_st:
while self.in_st:
self.out_st.append(self.in_st.pop())
C++代码
cpp
class MyQueue {
stack<int> inSt, outSt;
void move() {
if (outSt.empty()) {
while (!inSt.empty()) {
outSt.push(inSt.top());
inSt.pop();
}
}
}
public:
void push(int x) { inSt.push(x); }
int pop() {
move();
int val = outSt.top(); outSt.pop();
return val;
}
int peek() {
move();
return outSt.top();
}
bool empty() { return inSt.empty() && outSt.empty(); }
};
5. 滑动窗口最大值(LeetCode 239)
题目描述
给定一个整数数组 nums 和一个大小为 k 的滑动窗口,窗口从数组最左端移动到最右端,每次向右移动一位。返回每个窗口中的最大值。
示例 :
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解题思路:单调队列(双端队列)
- 使用双端队列
deque存储数组的下标 ,保证队列中下标对应的值单调递减。 - 遍历数组:
- 移除队列中已经滑出窗口的下标(
i - deque[0] >= k) - 从队尾移除所有比当前元素小的元素(因为它们不可能成为后续窗口的最大值)
- 将当前下标加入队尾
- 当
i >= k-1时,队首元素就是当前窗口的最大值
- 移除队列中已经滑出窗口的下标(
图解
nums: [1,3,-1,-3,5,3,6,7], k=3
i=0: deque=[0] (1)
i=1: 3>1 → 弹出0 → deque=[1] (3)
i=2: -1<3 → 加入2 → deque=[1,2] (3,-1) → 窗口[0-2]最大值3
i=3: -3<-1 → 加入3 → deque=[1,2,3] → 但1未出窗口(i=3,窗口[1-3])max=3
i=4: 5>队尾-3 → 弹出3,2 → 5>3 → 弹出1 → deque=[4] (5) → 最大值5
...
Python代码
python
from collections import deque
def maxSlidingWindow(nums, k):
dq = deque()
res = []
for i, num in enumerate(nums):
# 移除滑出窗口的索引
if dq and dq[0] <= i - k:
dq.popleft()
# 移除队尾所有小于当前值的
while dq and nums[dq[-1]] < num:
dq.pop()
dq.append(i)
if i >= k - 1:
res.append(nums[dq[0]])
return res
C++代码
cpp
class Solution {
public:
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;
}
};
6. 每日温度(LeetCode 739)
题目描述
给定一个整数数组 temperatures,表示每天的温度,返回一个数组 answer,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果之后没有更高温度,则为 0。
示例 :
输入:temperatures = [73,74,75,71,69,72,76,73]
输出:[1,1,4,2,1,1,0,0]
解题思路:单调栈(下一个更大元素)
- 维护一个单调递减栈(栈底到栈顶递减),栈中存储下标。
- 遍历温度数组:
- 当当前温度 > 栈顶下标对应的温度时,说明找到了栈顶元素的下一个更高温度,计算距离并弹出栈顶。
- 将当前下标压入栈。
- 最后栈中剩余下标对应结果为0。
图解
temperatures: 73,74,75,71,69,72,76,73
i=0: 栈[0] (73)
i=1: 74>73 → 弹出0 → ans[0]=1-0=1 → 栈[1] (74)
i=2: 75>74 → 弹出1 → ans[1]=1 → 栈[2] (75)
i=3: 71<75 → 压3 → 栈[2,3] (75,71)
i=4: 69<71 → 压4 → 栈[2,3,4]
i=5: 72>69 → 弹出4 → ans[4]=1 → 72>71 → 弹出3 → ans[3]=2 → 72<75 → 压5 → 栈[2,5]
i=6: 76>72 → 弹出5 → ans[5]=1 → 76>75 → 弹出2 → ans[2]=4 → 栈[6]
i=7: 73<76 → 压7 → 结束
ans: [1,1,4,2,1,1,0,0]
Python代码
python
def dailyTemperatures(temperatures):
n = len(temperatures)
ans = [0] * n
stack = []
for i, t in enumerate(temperatures):
while stack and temperatures[stack[-1]] < t:
idx = stack.pop()
ans[idx] = i - idx
stack.append(i)
return ans
C++代码
cpp
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n, 0);
stack<int> st;
for (int i = 0; i < n; ++i) {
while (!st.empty() && temperatures[st.top()] < temperatures[i]) {
int idx = st.top(); st.pop();
ans[idx] = i - idx;
}
st.push(i);
}
return ans;
}
};
7. 柱状图中最大的矩形(LeetCode 84)
题目描述
给定 n 个非负整数,表示柱状图中各个柱子的高度,每个柱子宽度为1。求柱状图中能勾勒出的最大矩形面积。
示例 :
输入:heights = [2,1,5,6,2,3]
输出:10(高度5和6的矩形面积5×2=10)
解题思路:单调栈 + 哨兵
- 维护一个单调递增栈(栈底到栈顶高度递增),存储下标。
- 遍历数组,当遇到高度小于栈顶高度时,说明找到了栈顶柱子的右边界,弹出栈顶并计算以该高度为高的矩形面积(宽度 = 当前下标 - 弹出后的新栈顶下标 - 1)。
- 为了处理所有柱子,在数组前后各加一个高度为0的哨兵,保证最后栈被清空。
图解
heights: [2,1,5,6,2,3]
前后加哨兵: [0,2,1,5,6,2,3,0]
i=0: 0入栈 [0]
i=1: 2>0 → 入1 [0,1]
i=2: 1<2 → 弹出1 → 高度2,左边界0,右边界2 → 面积2*(2-0-1)=2 → 栈[0] → 1>0 → 入2 [0,2]
i=3: 5>1 → 入3 [0,2,3]
i=4: 6>5 → 入4 [0,2,3,4]
i=5: 2<6 → 弹出4 → 高6,左边界3,右边界5 → 面积6*(5-3-1)=6 → 2<5 → 弹出3 → 高5,左边界2,右边界5 → 面积5*(5-2-1)=10 → 2>1 → 入5 [0,2,5]
...继续
最大面积10
Python代码
python
def largestRectangleArea(heights):
heights = [0] + heights + [0]
stack = []
max_area = 0
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
left = stack[-1] if stack else -1
width = i - left - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
C++代码
cpp
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
heights.insert(heights.begin(), 0);
heights.push_back(0);
stack<int> st;
int maxArea = 0;
for (int i = 0; i < heights.size(); ++i) {
while (!st.empty() && heights[st.top()] > heights[i]) {
int h = heights[st.top()]; st.pop();
int left = st.empty() ? -1 : st.top();
int width = i - left - 1;
maxArea = max(maxArea, h * width);
}
st.push(i);
}
return maxArea;
}
};
8. 接雨水(LeetCode 42)
题目描述
给定 n 个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例 :
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解题思路:双指针(推荐,O(n)时间 O(1)空间)
- 使用左右指针
left,right和左右最大高度left_max,right_max。 - 每次移动较小高度的一侧,计算当前柱子能接的雨水并累加。
原理 :对于位置 i,能接的雨水量 = min(左边最大值, 右边最大值) - height[i]。双指针通过维护 left_max 和 right_max 来避免额外数组。
图解
height: 0,1,0,2,1,0,1,3,2,1,2,1
left=0, right=11, left_max=0, right_max=1
left_max<right_max → 处理left: left_max=max(0,0)=0, water+=0-0=0, left=1
left=1, left_max=1, right_max=1 → 相等可处理任意,比如处理left: left_max=1, water+=1-1=0, left=2
left=2, left_max=1, right_max=1 → 处理left: height[2]=0, water+=1-0=1, left=3
left=3, left_max=2, right_max=1 → left_max>right_max → 处理right: height[11]=1, right_max=1, water+=1-1=0, right=10
...最终得到6
Python代码
python
def trap(height):
left, right = 0, len(height) - 1
left_max = right_max = 0
water = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
water += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
water += right_max - height[right]
right -= 1
return water
C++代码
cpp
class Solution {
public:
int trap(vector<int>& height) {
int left = 0, right = height.size() - 1;
int leftMax = 0, rightMax = 0;
int water = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= leftMax) leftMax = height[left];
else water += leftMax - height[left];
++left;
} else {
if (height[right] >= rightMax) rightMax = height[right];
else water += rightMax - height[right];
--right;
}
}
return water;
}
};
好的,在原表格基础上补充空间复杂度列,使总结更完整。
🎯 栈与队列八题总结(含空间复杂度)
| 题目 | 核心技巧 | 时间复杂度 | 空间复杂度 |
|---|---|---|---|
| 20. 有效的括号 | 栈匹配 | O(n) | O(n)(栈最坏存所有左括号) |
| 155. 最小栈 | 辅助栈存储最小值 | O(1) per op | O(n)(辅助栈最坏存所有元素) |
| 150. 逆波兰表达式 | 栈计算 | O(n) | O(n)(栈深度等于表达式嵌套深度) |
| 232. 用栈实现队列 | 双栈(输入输出) | 均摊O(1) | O(n)(两个栈总存所有元素) |
| 239. 滑动窗口最大值 | 单调队列(双端队列) | O(n) | O(k)(双端队列最多存k个下标,k为窗口大小) |
| 739. 每日温度 | 单调栈(下一个更大元素) | O(n) | O(n)(栈最坏存所有下标) |
| 84. 柱状图最大矩形 | 单调栈 + 哨兵 | O(n) | O(n)(栈存下标,最坏情况) |
| 42. 接雨水 | 双指针 / 单调栈 | O(n) | O(1)(双指针法)/ O(n)(单调栈法) |
说明:
- 最小栈的辅助栈法空间 O(n),差值法可优化至 O(1) 但较复杂。
- 滑动窗口最大值的单调队列空间为 O(k),优于 O(n)。
- 接雨水双指针法空间 O(1) 最优,推荐掌握。
栈和队列的题目往往有固定模式:括号匹配、单调栈、单调队列。多画图、多模拟,理解为什么使用栈/队列以及单调性如何维护,就能举一反三。