2.4 栈与队列
学习目标
学完这一节,你能:
- 解释栈的后进先出和队列的先进先出
- 用 Python 实现栈和队列的常见操作
- 用不变量理解括号匹配和任务排队
- 判断何时用
list,何时用collections.deque - 审查 agent 是否把队列写成低效的列表头部删除
栈:最后放进去的先出来
栈的规则是 LIFO:Last In, First Out,后进先出。
像一摞盘子。最后放上去的盘子最先被拿走。
栈通常支持两个核心操作:
push(x):压入一个元素
pop():弹出最近压入的元素Python 可以用 list 实现栈:
stack = []
stack.append("A") # push
stack.append("B")
stack.append("C")
top = stack.pop() # "C"对 Python list 来说,末尾 append 和末尾 pop 都是合适的栈操作。
栈的典型用途:括号匹配
判断括号是否匹配:
def is_balanced(text):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for char in text:
if char in "([{":
stack.append(char)
elif char in ")]}":
if not stack:
return False
if stack.pop() != pairs[char]:
return False
return not stack这里的不变量是:
扫描到当前位置时,栈里保存的是已经看到但还没有匹配的左括号,顺序从早到晚排列。
为什么右括号要和栈顶匹配?因为最近出现的未匹配左括号,必须最先被闭合。
这个例子也说明:数据结构不是孤立知识。栈的规则直接对应问题的结构。
栈的专业接口
栈通常可以定义为:
push(x):压入元素
pop():弹出并返回栈顶元素
peek():查看栈顶但不弹出
is_empty():判断是否为空接口里必须约定边界行为:
空栈 pop 应该报错,返回 None,还是由调用者先检查?不同约定会影响代码可靠性。Agent 写栈代码时,如果没有处理空栈,往往说明它只在正常样例上工作,没有处理边界。
队列:先来的先处理
队列的规则是 FIFO:First In, First Out,先进先出。
像排队买饭。先到的人先被服务。
队列通常支持:
enqueue(x):加入队尾
dequeue():从队头取出Python 中不要用 list.pop(0) 做大队列。应该使用 collections.deque:
from collections import deque
queue = deque()
queue.append("task-1") # enqueue
queue.append("task-2")
queue.append("task-3")
first = queue.popleft() # "task-1"deque 的两端加入和移除都很快,适合队列。
队列的专业接口
队列通常可以定义为:
enqueue(x):加入队尾
dequeue():移除并返回队头元素
peek():查看队头但不移除
is_empty():判断是否为空队列的不变量是:
如果 x 比 y 更早入队,并且二者都还没出队,那么 x 必须比 y 更早出队。这句话比"用什么代码"更根本。只要实现破坏了这个顺序,不管代码多漂亮,都不是队列。
栈和队列的区别不是语法,而是承诺
同样一组任务:
A, B, C如果用栈,处理顺序是:
C, B, A如果用队列,处理顺序是:
A, B, C这会改变程序行为。
例如:
- 撤销操作适合栈:最后一步最先撤销。
- 浏览器前进后退可以用两个栈建模。
- 打印任务适合队列:先提交的先打印。
- 客服工单通常适合队列,但也可能引入优先级。
当需求说"按提交顺序处理"时,用栈就是错误建模;当需求说"撤销最近动作"时,用队列也是错误建模。
用栈模拟撤销
class Editor:
def __init__(self):
self.text = ""
self.history = []
def type_text(self, content):
self.history.append(self.text)
self.text += content
def undo(self):
if self.history:
self.text = self.history.pop()每次修改前,把旧状态压入栈。撤销时弹出最近状态。
这个实现简单,但也有代价:如果文本很长,每次保存完整字符串会浪费空间。更真实的编辑器会保存操作记录,而不是每次保存完整文本。数据结构选择总是和规模有关。
双端队列:两端都快
deque 的名字来自 double-ended queue,意思是双端队列。它同时支持:
from collections import deque
d = deque()
d.append("right")
d.appendleft("left")
d.pop()
d.popleft()这让它既能做队列,也能在需要两端操作时发挥作用。
但 deque 不适合大量按下标随机访问。它的优势是两端操作,而不是像数组那样通过下标跳转。
用本节知识解决:撤回问题和普通排队
课堂互动系统里有两个看起来相似、其实完全不同的需求。
第一个需求:
学生可以撤回自己最近一次提交的问题。
"最近一次"是栈信号。每个学生可以有自己的提交栈:
question_stack_by_student = {}
def remember_question(student_id, question_id):
question_stack_by_student.setdefault(student_id, []).append(question_id)
def undo_last_question(student_id):
stack = question_stack_by_student.get(student_id, [])
if not stack:
return None
return stack.pop()第二个需求:
老师按提交顺序处理普通问题。
"按提交顺序"是队列信号:
from collections import deque
normal_questions = deque()
def add_normal_question(question_id):
normal_questions.append(question_id)
def next_normal_question():
if not normal_questions:
return None
return normal_questions.popleft()这两个需求都在处理"问题",但一个是后进先出,一个是先进先出。现实任务里,结构选择经常不是由数据类型决定,而是由操作语义决定。
Agent 如果把两者都写成 list.append + list.pop(),撤回功能可能是对的,按提交顺序处理就是错的。
如果它把队列写成 list.append + pop(0),顺序对了,大规模时复杂度又可能错。
审查 agent 输出
假设 agent 写了任务队列:
tasks = []
def add_task(task):
tasks.append(task)
def next_task():
return tasks.pop()你要先问:需求到底是栈还是队列?
如果任务要求"后加入的先处理",这就是栈,可以接受。但如果要求"先提交先处理",pop() 会从末尾取出,行为就是错的。改成 pop(0) 行为对了,但复杂度又可能错。更好的结构是 deque:
from collections import deque
tasks = deque()
def add_task(task):
tasks.append(task)
def next_task():
return tasks.popleft()审查时要同时看两件事:
- 行为顺序是否正确。
- 操作复杂度是否适合规模。
本节要点
- 栈是后进先出,队列是先进先出。
- Python
list.append+list.pop适合做栈。 - Python 队列通常用
collections.deque。 - 栈和队列的关键是行为承诺,不只是代码写法。
- 审查 agent 时,既要检查顺序语义,也要检查隐藏复杂度。
课堂练习
某系统有三种任务:
- 用户撤销最近一次编辑。
- 后台按提交顺序处理图片压缩任务。
- 客服先处理普通工单,但会员工单优先。
请回答:
- 哪些适合栈?
- 哪些适合队列?
- 哪个已经超出普通队列,需要下一节之后的结构?
- 如果 agent 都用
list实现,你会重点审查哪些行?
课后测验
📝 课后测验
上一节:2.3 链表
下一节:2.5 集合与字典:哈希表