先问谁应该下一个离开
栈和队列描述的是取出元素的规则,不是元素本身的类型。栈采用后进先出:最新加入且尚未移除的元素先离开。队列采用先进先出:最早加入且尚未移除的元素先离开。两者都可以保存数字、任务或对象,差别在允许的操作以及操作作用于哪一端。
不要仅凭现实场景中的名字判断。例如一份叫“任务队列”的程序,也可能按优先级选择任务,而不是严格按到达次序。写题时应先明确接口契约:从哪里加入,从哪里取出,空的时候发生什么,再选择实现。本文先讨论单线程、等优先级的普通结构。
原创共同输入:中途取出会改变结果
依次执行“加入甲、加入乙、取出一次、加入丙、加入丁、取出一次”。若是栈,并约定右端为栈顶:加入后为 [甲,乙],第一次取出乙;加入丙、丁后为 [甲,丙,丁],第二次取出丁,剩下 [甲,丙]。每一步都只操作右端。
若是队列,并约定左端为队首:第一次取出甲;加入丙、丁后为 [乙,丙,丁],第二次取出乙,剩下 [丙,丁]。两种结构收到完全相同的加入指令,却输出不同序列。这说明先把所有元素排完再倒序,不能替代对交错操作的逐步追踪。
Python 中选对操作,也选对容器
列表末端的 append 和不带下标的 pop 可以形成栈。使用时明确“末端是栈顶”,不要一部分操作从开头取,另一部分从结尾取。对空列表执行 pop 会抛出 IndexError,因此若函数约定返回一个特殊值,需要在封装层明确处理,不能默认为自动得到空值。
队列常用 collections.deque:append 从右侧加入,popleft 从左侧取出。Python 官方教程建议用双端队列处理这种两端操作。列表的 pop(0) 虽然能得到相同的逻辑结果,却需要移动后续元素,任务较多时成本不同。功能正确和复杂度合适是两项独立检查。
栈的应用:为什么括号匹配需要记住最近一个
只考虑三种括号及普通字符。扫描文本时,遇到左括号就压入;遇到右括号,必须与栈顶的左括号类型匹配,再弹出。扫描完后,栈必须为空。以“{[()]}”为例,前三步依次压入 {、[、(;随后 )、]、} 正好依次与栈顶匹配,因此成立。
对“([)]”,读到 ) 时栈顶为 [,立即判定不匹配。只计算左右括号数量会误判,因为这个例子每类括号数量都相等,但嵌套次序错误。若开头就是 ),空栈检查必须发生在读取栈顶之前。实际编程语言还涉及字符串、转义和注释,不能仅靠此简化扫描器解析。
队列的应用:先进先出不等于最快完成
设单个处理器依次收到耗时 6、1、1 个时间单位的三个任务,且没有抢占。按先进先出执行,完成时刻分别是 6、7、8,后两个短任务都要等第一个结束。队列规则体现到达次序,却不保证最小平均等待时间;是否应改用其他调度策略取决于需求。
如果有两个处理器从同一队列取任务,“取出次序”仍可能是先进先出,但完成次序可能不同。不要看到第二个任务先结束,就断言队列违反了定义。并发程序还要处理多个线程同时查看和取出的竞争条件,普通的检查再操作不能自动成为一个不可分割步骤。
固定容量时,要区分逻辑顺序和物理位置
用容量为 4 的数组模拟环形队列,初始队首索引 head=0、元素数 count=0。下一次写入位置可定义为 (head+count) 除以 4 的余数;取出后令 head=(head+1) 除以 4 的余数,再减少 count。这样可以复用已经取走的槽,而不用整体搬移元素。
例如加入甲、乙、丙后取出甲、乙,此时 head=2、count=1。再加入丁写到槽 3,加入戊写到槽 0。逻辑顺序为丙、丁、戊,物理槽位并非从零连续排列。这里用 count 区分空与满:0 为空,4 为满;其他实现可以保留一个空槽,但容量含义会随之改变。
自测与解析:用输出和剩余状态同时核对
练习一:对空容器执行加入 2、加入 5、取出、加入 8、取出、取出。栈的输出是 5、8、2;队列的输出是 2、5、8;二者最终都为空。如果只检查最终为空,就无法发现中间取出顺序错误。因此测试既要核对返回值,也要核对长度和余下元素。
练习二:容量为 4 的环形队列中,head=3、count=2,下次写入位置是什么?答案为 (3+2)%4=1。写入后 count=3,head 仍为 3。若再取出一次,head 变为 0,count 变为 2。加入时不应移动队首;取出时也不应把队首直接重置为零。
实现前写好空、满和丢弃策略
固定容量满了以后,可以拒绝新元素、阻塞等待,或按明确规则丢弃旧元素,不同需求适合不同选择。尤其要注意,Python 设置 maxlen 的 deque 在两端追加且已经满时,会从相反一端丢弃元素;这与“满了就报错”的教学环形队列不是相同契约。
复习清单应包含正常交错操作、空容器取出、容量边界、回绕以及重复值。若元素值相同,可以给每次加入附一个唯一编号,防止把顺序错误隐藏起来。本文的结构推演帮助理解规则;真实多线程任务分发还应使用具有相应同步能力的队列工具,并测试失败和取消路径。
增加一个满容量练习:一个 maxlen 为 3 的双端队列当前按左到右保存甲、乙、丙,再从右端追加丁,结果是乙、丙、丁,甲被丢弃。若需求是所有任务都必须处理,这种自动丢弃就可能不合适;若需求只是保留最近三条观测,它反而符合目标。容器没有脱离需求的好坏,关键是把丢弃行为写进契约和测试,而不是等数据消失后再解释。
参考资料
下一步
如想练习数据结构的操作追踪,请说明所学模块、使用语言,以及目前混淆的入栈、出栈或队列状态。相关辅导能否安排,取决于学科匹配和导师时间。
