栈接口与实现
看完你能说清栈的接口约定,并亲手写出数组与链表两种实现
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
栈接口与实现
看完你能说清栈的接口约定,并亲手写出数组与链表两种实现
什么是栈
家里叠盘子:洗好的盘子一个压一个往上放,取的时候也只从最上面拿——最先放的反而最后被用。这种「只在一端进出」的存放方式,就是数据结构里的栈。
盘子的最上层对应栈顶,放盘取盘对应 push 和 pop
LIFO:后进先出
沿箭头从上往下看:三个元素依次进栈,再依次出栈,注意进出顺序刚好相反。
栈的实例:身边的栈
上一页说栈是「后进先出」。听起来抽象?其实你每天都在用:浏览器后退、Excel 撤销、函数嵌套调用,背后都是同一个栈在悄悄工作。
最后放上去的盘子最先被取走,对应各场景的「最近优先」处理
接口与实现分离
你已经知道栈是后进先出的容器。但写程序前得先约定:外界只能对它做哪些操作?先把「能做什么」说清楚,再谈「怎么做」,这就是接口与实现的分离。
你只用投币按钮对应操作接口,内部货道机构对应实现
栈的 ADT 操作集
图中以栈顶为枢纽:所有操作只发生在 top 一端,沿箭头看每次调用如何改变栈顶。
栈接口的定义
用 Python 抽象基类给出栈接口:五个方法,各自的语义与前置条件。
继承 ABC 强制子类实现全部方法——接口只声明契约;pop 与 peek 的区别在于是否移除元素,空栈是两者共同的边界条件。
一次 pop 的完整约定
pop 一次调用,内部走三步约定,缺一步都不完整。
实现思路总览
前面我们把 pop 的约定写得清清楚楚,却从没说过数据放在哪。这正是接口分离的好处:只要行为守约,底层既可以是连续数组,也可以是链式列表。本页先看这两条路线的全貌。
插座孔位不变(接口),墙后走明线还是暗线(实现)随意
基于数组的栈
一个数组的两头,左端固定是栈底,右端才是栈顶;size 只是一个计数器,不指向数据本身。
基于链表的栈
从左往右看:head 永远是栈顶,push 和 pop 都只动头部,不碰后面任何节点。
关键操作的实现代码
对照两种实现的 push/pop 源码,看栈顶位置的选择如何决定代码形态。
两边都选 O(1) 端作栈顶:数组选尾部,链表选头部。若数组用头部、链表用尾部,操作都退化为 O(n)。
两种实现的对比
数组栈和链表栈都能实现栈,但性能与适用场景不同,容易混淆,逐维度对比。
- 存取快:数组连续,缓存友好
- 空间固定:满时需整体扩容搬迁
- 无每元素指针,内存更紧凑
- 存取稍慢:节点分散,缓存不友好
- 空间弹性:按需分配节点,无需扩容
- 每节点多一个指针,有额外开销
自测:栈的接口与实现
一道选择题检验对栈顶操作与实现取舍的理解
要点收束
- ✓栈是行为约束:只看栈顶、LIFO,与底层结构无关
- ✓接口与实现分离:使用者只依赖 ADT 操作集
- ✓数组实现:栈顶下标即插入位置,O(1) 增删
- ✓链表实现:头结点即栈顶,天然动态扩容
- ✓边界要约定清楚:空栈 pop、满栈 push 各有定义
课后思考
先自己想,想不动了再看参考答案——思路比结论重要。
参考答案想想嵌套调用:最先调用的函数最后返回,正是 LIFO。若用队列,最先调用的函数反而最先返回,调用者的上下文还没用完就被覆盖,嵌套结构无法维持。
参考答案核心是调用方能否区分「空」与「值恰好为 null/哨兵」。语言支持异常时可抛异常让错误显式暴露;或提供 isEmpty 前置判断,约定写在接口文档里,调用方负责。
参考答案数组栈怕扩容瞬间开销与预留空间浪费;链表栈每个元素都要付节点指针的内存与缓存局部性代价。大量随机访问或内存紧张时值得重新权衡。