接口与实现
看完你能说清列表接口与实现分离的好处,并按场景选对实现
按 空格/→ 演示下一步
全部页面点击任意一页,跳回舞台从这页播放
接口与实现
看完你能说清列表接口与实现分离的好处,并按场景选对实现
静态与动态
上一页说接口管约定、实现管做法,这是把代码横着切了一刀。现在再竖着切一刀:想想一栋楼,建成后格局定死,但住户随时进出。代码里也有这样两种存在方式,它们对应着完全不同的生命周期。
格局对应编译期骨架,住户进出对应运行期对象增改
静态 vs 动态数据结构
两者都能存数据,但容量与内存行为完全不同——选错一个,性能和正确性都会出问题。
- 编译时定长,容量固定
- 内存一次分配,连续紧凑
- 扩容需整体搬家重建
- 运行时按需增减元素
- 内存分散分配,靠指针串联
- 插入删除只改局部链接
动态化的动机
前面看到静态数组快是快,但它有个先天缺陷:容量在创建时就写死了。而现实中数据量谁也说不准——通讯录今天 100 人,明天可能 1000 人。
提前包 30 人大间,人少空转、人多坐不下,不如随到随加座
向量的瓶颈
前面我们把静态数组动态化了,容量可以翻倍扩容。但动态化不是免费的:扩容那一刻要整体搬家,中间插队更要挪动后面所有元素。这一页看清这些代价藏在哪。
中间塞进一人,右边所有观众都要挪一个座位
列表的思路
向量要求内存连续,插队就得全员搬家。那能不能放弃「物理挨着」,改成「知道下一个是谁」?火车车厢不相连,靠挂钩照样成列——链表就是这个思路。
车厢的挂钩对应节点的指针,挂钩解开重接对应修改指针
向量与列表结构对比
左:向量地址连续,靠算术定位;右:列表节点散落,靠指针串联。看访问方式的不同。
秩的含义与局限
想象一排 tightly packed 的储物柜,每个都有固定编号。你不仅能按号快速找到柜子,还能直接算出任意柜的位置——这就是秩的物理映射本质。
座位号直接对应物理位置,如同秩映射存储偏移
位置的概念
列表里没有下标可数,那位置从哪来?答案是:每个节点都牵着邻居的手——前一个节点告诉你后面是谁,位置就藏在这些相互引用里。
纸条上写下一条线索的位置,对应节点存下一节点的地址
秩 vs 位置
都能定位元素,但两者基于完全不同的坐标系,决定了操作效率的天壤之别。
- 地址 = 基址 + 秩×元素大小,算出来
- 秩即物理存储位置,天然对应
- 寻址 O(1),但插入需整体移动
- 位置 = 节点引用,沿指针逐个走到
- 逻辑位置与物理地址无关
- 寻址 O(n),但插入删除只改指针
列表 ADT 接口
前面我们说,列表用「位置」取代「秩」来定位元素。但作为使用者,到底能对它做哪些操作?这就轮到 ADT 接口出场——先约定好对外提供什么,再谈内部怎么实现。
菜单只列可点的菜,对应接口只承诺操作;后厨做法对应实现
节点的实现
看列表节点如何同时装下数据和通往邻居的引用。
节点 = 数据 + prev/next 两个引用;插入时改的只是三个指针,无需搬动任何元素。
列表的实现
看一个真实可运行的 Python 列表:节点如何串起来,位置接口如何落地。
哨兵让首尾插入无需特判;insert_before 只改两根指针、返回新节点作位置——秩与位置的区别在此落地。
接口与实现的分离
从上往下看:上层只认接口,下层可自由替换实现,边界即契约。
自测
将列表 ADT 的「插入」接口从 O(n) 实现改为 O(1) 实现,使用该列表的客户端代码需要注意什么?
要点回顾
- ✓静态结构靠秩寻址,动态结构靠位置关联
- ✓动态化解决了中间插入的移动开销
- ✓接口统一,实现可替换
- ✓秩是数组视角,位置是节点视角
课后思考
先自己想几分钟,再对照参考答案——开放问题没有唯一解。
参考答案从接口与实现分离入手:调用方只依赖接口约定的操作与语义,只要新实现保持相同行为,上层代码一行不动——这正是抽象数据类型的价值。
参考答案选向量:秩访问 O(1) 且内存连续、缓存友好;列表访问要沿引用逐个走,O(n)。反过来若频繁在任意位置插删,列表才占优。
参考答案成立但需重新定义:树、图中节点不再有唯一的『前一个/后一个』,相邻关系变复杂;位置需刻画父子或邻接关系,遍历顺序不再唯一。