1. 栈与队列:LeetCode解题的核心武器
第一次刷LeetCode时,我就被这两兄弟整懵了——栈和队列。它们看起来简单,用起来却处处是坑。记得有次面试,面试官让我用栈实现队列,我手忙脚乱写了半小时,最后发现解法只需要5行代码。从那时起,我系统整理了这两个数据结构的解题套路,现在刷题遇到栈/队列相关题目,基本都能在15分钟内搞定。
栈(Stack)是后进先出(LIFO)结构,就像叠盘子——最后放上去的盘子最先被拿走;队列(Queue)则是先进先出(FIFO),像排队买奶茶——先来的人先被服务。LeetCode中约15%的题目会直接或间接用到它们,尤其是处理括号匹配、滑动窗口、二叉树遍历等问题时。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 栈的实战技巧与经典题型
2.1 栈的三大高频应用场景
括号匹配是栈的经典用例。当遇到左括号时压栈,右括号时弹出栈顶元素检查是否匹配。LeetCode第20题(有效的括号)就是典型例子:
python复制def isValid(s: str) -> bool:
stack = []
mapping = {')': '(', '}': '{', ']': '['}
for char in s:
if char in mapping:
top = stack.pop() if stack else '#'
if mapping[char] != top:
return False
else:
stack.append(char)
return not stack
注意:这里用字典存储括号对能减少if-else判断,'#'作为哨兵值避免空栈异常
表达式求值中,栈用来处理运算符优先级。LeetCode第224题(基本计算器)需要双栈(数字栈和运算符栈)配合:
- 遇到数字时完整读取(处理多位数)
- 遇到'+'或'-'时,先处理栈顶优先级≥当前运算符的计算
- 遇到'('压栈,遇到')'不断出栈计算直到'('
函数调用栈是计算机系统的核心实现。当遇到递归或DFS问题时,实际上就是在模拟系统栈的行为。比如LeetCode第94题(二叉树中序遍历)的迭代解法:
python复制def inorderTraversal(root):
stack, res = [], []
curr = root
while curr or stack:
while curr: # 压左节点到栈
stack.append(curr)
curr = curr.left
curr = stack.pop()
res.append(curr.val)
curr = curr.right # 转向右子树
return res
2.2 单调栈的魔法
单调栈能在O(n)时间内解决"下一个更大元素"类问题。其核心是维护栈内元素的单调性(递增或递减)。以LeetCode第496题(下一个更大元素I)为例:
python复制def nextGreaterElement(nums1, nums2):
stack = []
mapping = {}
for num in nums2:
while stack and num > stack[-1]: # 破坏单调性
mapping[stack.pop()] = num
stack.append(num)
return [mapping.get(num, -1) for num in nums1]
实战技巧:
- 正向遍历找下一个更大元素用递减栈
- 逆向遍历找前一个更大元素用递增栈
- 环形数组问题可将数组长度翻倍模拟
2.3 栈的边界陷阱
我曾在LeetCode第32题(最长有效括号)上栽过跟头。关键点在于栈底始终保存最后一个未匹配的右括号索引:
python复制def longestValidParentheses(s):
stack = [-1] # 哨兵值
max_len = 0
for i, char in enumerate(s):
if char == '(':
stack.append(i)
else:
stack.pop()
if not stack:
stack.append(i) # 更新基准
else:
max_len = max(max_len, i - stack[-1])
return max_len
常见错误:
- 空栈时直接pop会导致异常
- 忽略连续有效括号的累计计算
- 未正确处理开头就是右括号的情况
3. 队列的解题套路大全
3.1 BFS与队列的不解之缘
队列是广度优先搜索(BFS)的基石。LeetCode第102题(二叉树的层序遍历)标准解法:
python复制def levelOrder(root):
if not root: return []
queue = collections.deque([root])
res = []
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left: queue.append(node.left)
if node.right: queue.append(node.right)
res.append(level)
return res
关键细节:
- 使用deque而非list,popleft()时间复杂度O(1)
- 每次处理一层的所有节点(通过level_size控制)
- 空节点不入队避免None.val异常
3.2 滑动窗口的双端队列妙用
LeetCode第239题(滑动窗口最大值)需要单调队列(双端队列实现):
python复制def maxSlidingWindow(nums, k):
dq = collections.deque()
res = []
for i, num in enumerate(nums):
while dq and nums[dq[-1]] <= num: # 维护递减
dq.pop()
dq.append(i)
if dq[0] == i - k: # 移除越界元素
dq.popleft()
if i >= k - 1:
res.append(nums[dq[0]])
return res
注意:队列存储的是索引而非值,便于判断窗口范围
3.3 优先队列(堆)的特殊应用
虽然严格来说堆不是队列,但Python的heapq模块常被当作优先队列使用。LeetCode第23题(合并K个升序链表)的解法:
python复制def mergeKLists(lists):
import heapq
dummy = ListNode()
curr = dummy
heap = []
# 初始化堆
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
# 不断取最小
while heap:
val, i, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
技巧点:
- 元组(val, i, node)中的i用于避免比较ListNode对象
- 时间复杂度O(Nlogk),N是总节点数,k是链表数
4. 栈与队列的混合应用
4.1 用栈实现队列(LeetCode第232题)
核心思路是用两个栈(输入栈和输出栈):
python复制class MyQueue:
def __init__(self):
self.in_stack = []
self.out_stack = []
def push(self, x):
self.in_stack.append(x)
def pop(self):
self._transfer()
return self.out_stack.pop()
def peek(self):
self._transfer()
return self.out_stack[-1]
def _transfer(self):
if not self.out_stack:
while self.in_stack:
self.out_stack.append(self.in_stack.pop())
时间复杂度分析:
- 均摊时间复杂度O(1)
- 最坏情况下pop/peek为O(n)
4.2 用队列实现栈(LeetCode第225题)
单队列旋转法比双队列更高效:
python复制class MyStack:
def __init__(self):
self.q = collections.deque()
def push(self, x):
self.q.append(x)
# 将新元素前的所有元素重新入队
for _ in range(len(self.q) - 1):
self.q.append(self.q.popleft())
def pop(self):
return self.q.popleft()
关键点:
- push时通过旋转让最新元素始终在队首
- pop/push时间复杂度均为O(n)
5. 高频面试题深度剖析
5.1 最小栈问题(LeetCode第155题)
要求在O(1)时间内获取栈内最小值。解法是同步维护最小值栈:
python复制class MinStack:
def __init__(self):
self.stack = []
self.min_stack = [float('inf')]
def push(self, val):
self.stack.append(val)
self.min_stack.append(min(val, self.min_stack[-1]))
def pop(self):
self.stack.pop()
self.min_stack.pop()
def top(self):
return self.stack[-1]
def getMin(self):
return self.min_stack[-1]
优化点:
- min_stack初始放入无穷大避免空栈判断
- 同步pop保持两栈高度一致
5.2 接雨水问题(LeetCode第42题)
单调栈解法比双指针更直观:
python复制def trap(height):
stack = []
res = 0
for i, h in enumerate(height):
while stack and h > height[stack[-1]]:
bottom = height[stack.pop()]
if not stack: break
distance = i - stack[-1] - 1
bounded_height = min(h, height[stack[-1]]) - bottom
res += distance * bounded_height
stack.append(i)
return res
思考过程:
- 维护高度递减栈
- 当遇到更高柱子时,计算前一个柱子能接的雨水量
- 水量 = 距离 × (左右边界较小值 - 底部高度)
5.3 每日温度问题(LeetCode第739题)
单调栈的又一经典应用:
python复制def dailyTemperatures(T):
stack = []
res = [0] * len(T)
for i, temp in enumerate(T):
while stack and temp > T[stack[-1]]:
prev = stack.pop()
res[prev] = i - prev
stack.append(i)
return res
提示:结果数组初始化为0,未找到更高温度的自动保持0
6. 避坑指南与性能优化
6.1 数据结构选择黄金法则
- 需要快速访问最近元素 → 栈
- 需要公平处理所有元素 → 队列
- 需要动态获取极值 → 优先队列
- 需要两端操作 → 双端队列
Python中各结构的实际表现:
python复制# 测试10万次操作时间(秒)
| 操作 | list | deque |
|------------|-------|-------|
| append | 0.012 | 0.011 |
| pop(0) | 1.213 | 0.013 |
| popleft() | 1.201 | 0.012 |
| insert(0,x)| 1.195 | 0.014 |
6.2 递归转迭代的栈式写法
以LeetCode第144题(二叉树前序遍历)为例:
python复制# 递归版
def preorder(root):
if not root: return []
return [root.val] + preorder(root.left) + preorder(root.right)
# 迭代版(显式栈)
def preorder(root):
stack, res = [root], []
while stack:
node = stack.pop()
if node:
res.append(node.val)
stack.append(node.right) # 先右后左
stack.append(node.left)
return res
转换要点:
- 递归调用改为压栈
- 注意入栈顺序与遍历顺序相反
- 显式处理空节点
6.3 内存与异常处理
常见内存错误:
- 递归深度过大导致栈溢出 → 改用迭代
- 队列未及时清空导致内存泄漏 → 及时移除已处理元素
- 循环引用导致无法回收 → 显式断开引用
防御性编程技巧:
python复制# 安全的栈操作
def safe_pop(stack):
try:
return stack.pop()
except IndexError:
return None # 或自定义哨兵值
# 带超时的队列获取
import queue
q = queue.Queue()
try:
item = q.get(timeout=1.0) # 1秒超时
except queue.Empty:
print("Queue timeout")
7. 进阶挑战与扩展思考
7.1 多栈/队列协同工作
LeetCode第341题(扁平化嵌套列表迭代器)需要栈辅助:
python复制class NestedIterator:
def __init__(self, nestedList):
self.stack = nestedList[::-1] # 倒序入栈
def next(self):
return self.stack.pop().getInteger()
def hasNext(self):
while self.stack:
top = self.stack[-1]
if top.isInteger():
return True
self.stack.pop()
for item in top.getList()[::-1]: # 嵌套列表展开
self.stack.append(item)
return False
7.2 线程安全实现
Python Queue模块的实战应用:
python复制from queue import Queue
from threading import Thread
def worker(q):
while True:
item = q.get()
if item is None: break # 终止信号
print(f"Processing {item}")
q.task_done()
q = Queue()
threads = [Thread(target=worker, args=(q,)) for _ in range(4)]
for t in threads: t.start()
for item in range(10):
q.put(item)
q.join() # 阻塞直到所有任务完成
# 优雅关闭
for _ in range(4): q.put(None)
for t in threads: t.join()
7.3 分布式队列实践
Redis队列的简单示例:
python复制import redis
r = redis.Redis()
# 生产者
r.lpush('task_queue', 'task_data')
# 消费者
while True:
task = r.brpop('task_queue', timeout=30) # 阻塞获取
if task:
process_task(task[1]) # task[1]是实际数据
性能考量:
- 批量操作使用pipeline减少网络往返
- 消息确认机制防止任务丢失
- 死信队列处理失败任务
8. 从LeetCode到工程实践
8.1 浏览器历史记录实现
基于双栈的前进后退功能:
python复制class BrowserHistory:
def __init__(self, homepage):
self.back_stack = [homepage]
self.forward_stack = []
def visit(self, url):
self.back_stack.append(url)
self.forward_stack = [] # 清空前进栈
def back(self, steps):
for _ in range(min(steps, len(self.back_stack)-1)):
self.forward_stack.append(self.back_stack.pop())
return self.back_stack[-1]
def forward(self, steps):
for _ in range(min(steps, len(self.forward_stack))):
self.back_stack.append(self.forward_stack.pop())
return self.back_stack[-1]
8.2 消息队列设计要点
基于队列的异步处理系统核心要素:
- 消息持久化(磁盘存储)
- 消费确认机制(ACK/NACK)
- 重试策略与死信队列
- 消费者负载均衡
- 消息优先级支持
简单实现框架:
python复制class MessageQueue:
def __init__(self):
self.queue = collections.deque()
self.consumers = []
def add_consumer(self, callback):
self.consumers.append(callback)
def publish(self, message):
self.queue.append(message)
self._dispatch()
def _dispatch(self):
while self.queue and self.consumers:
msg = self.queue.popleft()
consumer = self.consumers.pop(0)
Thread(target=consumer, args=(msg,)).start()
self.consumers.append(consumer) # 轮询分配
8.3 算法策略选择决策树
遇到新问题时可以这样思考:
code复制是否需要处理最近相关元素?
├─ 是 → 考虑栈
│ ├─ 需要比较相邻元素? → 单调栈
│ └─ 需要反转顺序? → 双栈
└─ 否 → 是否需要公平处理?
├─ 是 → 考虑队列
│ ├─ 需要动态窗口? → 滑动窗口+双端队列
│ └─ 需要优先级? → 优先队列
└─ 否 → 其他数据结构
