(a)接口与实现

官官方信息技术老师·17 页·深入(追求细节与边界)·0 次浏览·38 天前
数据结构接口与实现性能权衡

接口与实现

看完你能说清列表接口与实现分离的好处,并按场景选对实现

按 空格/→ 演示下一步

问一句
1 / 17 页

全部页面点击任意一页,跳回舞台从这页播放

数据结构接口与实现性能权衡

接口与实现

看完你能说清列表接口与实现分离的好处,并按场景选对实现

1第 1 页 · 接口与实现

静态与动态

上一页说接口管约定、实现管做法,这是把代码横着切了一刀。现在再竖着切一刀:想想一栋楼,建成后格局定死,但住户随时进出。代码里也有这样两种存在方式,它们对应着完全不同的生命周期。

静态结构
类型、类层次等编译期确定的骨架,建成后生命周期封闭
动态结构
运行期创建的对象与调用关系,可随时增改
分界线
编译期看声明,运行期看实例;接口连接两个世界
楼盘格局与住户对应 →静态与动态结构

格局对应编译期骨架,住户进出对应运行期对象增改

2第 2 页 · 静态与动态

静态 vs 动态数据结构

两者都能存数据,但容量与内存行为完全不同——选错一个,性能和正确性都会出问题。

静态数据结构
  • 编译时定长,容量固定
  • 内存一次分配,连续紧凑
  • 扩容需整体搬家重建
动态数据结构
  • 运行时按需增减元素
  • 内存分散分配,靠指针串联
  • 插入删除只改局部链接
数据量已知且访问密集时选静态;增删频繁、规模不定时选动态。
3第 3 页 · 静态 vs 动态数据结构

动态化的动机

前面看到静态数组快是快,但它有个先天缺陷:容量在创建时就写死了。而现实中数据量谁也说不准——通讯录今天 100 人,明天可能 1000 人。

预估的两难
开太大浪费内存,开太小装不下,都无法两全
扩容的代价
静态满了只能整体搬新家,一次性 O(n)
按需伸缩
动态结构让容量跟着数据走,摊还成本更低
订餐厅包间对应 →容量策略

提前包 30 人大间,人少空转、人多坐不下,不如随到随加座

4第 4 页 · 动态化的动机

向量的瓶颈

前面我们把静态数组动态化了,容量可以翻倍扩容。但动态化不是免费的:扩容那一刻要整体搬家,中间插队更要挪动后面所有元素。这一页看清这些代价藏在哪。

静态空间
容量申请后定死,超了要搬家,备多了浪费
插入搬移
中间插入需整体后移,最坏 O(n)
删除搬移
删除后补位,同样要移动后续元素
扩容成本
翻倍扩容瞬间是 O(n) 的整体复制
坐满的观众席对应 →向量中间插入

中间塞进一人,右边所有观众都要挪一个座位

5第 5 页 · 向量的瓶颈

列表的思路

向量要求内存连续,插队就得全员搬家。那能不能放弃「物理挨着」,改成「知道下一个是谁」?火车车厢不相连,靠挂钩照样成列——链表就是这个思路。

节点 + 指针
每段数据存一个节点,节点里记着下一个节点的地址
逻辑连续
物理上散落各处,靠指针串成一条逻辑链
插入只需改指针
断开一处、接上两根指针即可,无需搬动其他数据
代价:随机访问慢
想找第 i 个,只能从头顺着指针一个个数过去
火车车厢挂钩对应 →链表节点

车厢的挂钩对应节点的指针,挂钩解开重接对应修改指针

6第 6 页 · 列表的思路

向量与列表结构对比

左:向量地址连续,靠算术定位;右:列表节点散落,靠指针串联。看访问方式的不同。

图解渲染中…
a1:实线---表示物理相邻,无需指针记录关系b1:箭头-->是指针,节点内存地址互不相干a4:虚线表示可由下标直接算出地址,随机访问O(1)
7第 7 页 · 向量与列表结构对比

秩的含义与局限

想象一排 tightly packed 的储物柜,每个都有固定编号。你不仅能按号快速找到柜子,还能直接算出任意柜的位置——这就是秩的物理映射本质。

秩的本质
物理位置的直接编号,非逻辑序号
寻址优势
基地址+秩×元素大小,O(1)直达
结构代价
中间插入需整体后移,O(n)成本
适用边界
读多写少、规模稳定的静态场景
电影院座位号对应 →秩寻址

座位号直接对应物理位置,如同秩映射存储偏移

O(1) 寻址=base+rank×size\text{O(1) 寻址} = \text{base} + \text{rank} \times \text{size}
8第 8 页 · 秩的含义与局限

位置的概念

列表里没有下标可数,那位置从哪来?答案是:每个节点都牵着邻居的手——前一个节点告诉你后面是谁,位置就藏在这些相互引用里。

邻居引用
每个节点存着下一个节点的地址
相对位置
从已知节点出发,走几步就到第几个
入口指针
头节点是唯一确定的起点
寻宝游戏的纸条对应 →节点相互引用

纸条上写下一条线索的位置,对应节点存下一节点的地址

9第 9 页 · 位置的概念

秩 vs 位置

都能定位元素,但两者基于完全不同的坐标系,决定了操作效率的天壤之别。

按秩寻址(向量)
  • 地址 = 基址 + 秩×元素大小,算出来
  • 秩即物理存储位置,天然对应
  • 寻址 O(1),但插入需整体移动
按位置寻址(列表)
  • 位置 = 节点引用,沿指针逐个走到
  • 逻辑位置与物理地址无关
  • 寻址 O(n),但插入删除只改指针
当需要高频随机访问时选向量按秩;当需要频繁插删、顺序遍历时选列表按位置。
10第 10 页 · 秩 vs 位置

列表 ADT 接口

前面我们说,列表用「位置」取代「秩」来定位元素。但作为使用者,到底能对它做哪些操作?这就轮到 ADT 接口出场——先约定好对外提供什么,再谈内部怎么实现。

pred / succ
沿链找前驱和后继,是位置之间的移动方式
insertBefore/After
在某个位置旁边插入新元素,无需移动其他元素
remove
删除指定位置上的元素,只改邻居的指针
操作不带秩
接口只收「位置」作参数,彻底回避 O(n) 的秩换算
餐厅点菜单对应 →列表 ADT 接口

菜单只列可点的菜,对应接口只承诺操作;后厨做法对应实现

11第 11 页 · 列表 ADT 接口

节点的实现

cpp

看列表节点如何同时装下数据和通往邻居的引用。

代码高亮加载中…

节点 = 数据 + prev/next 两个引用;插入时改的只是三个指针,无需搬动任何元素。

12第 12 页 · 节点的实现

列表的实现

python

看一个真实可运行的 Python 列表:节点如何串起来,位置接口如何落地。

代码高亮加载中…

哨兵让首尾插入无需特判;insert_before 只改两根指针、返回新节点作位置——秩与位置的区别在此落地。

13第 13 页 · 列表的实现

接口与实现的分离

从上往下看:上层只认接口,下层可自由替换实现,边界即契约。

图解渲染中…
a2:契约层:声明操作名与语义,不含任何数据结构细节。b1:三种实现都满足同一契约,可整体替换。
14第 14 页 · 接口与实现的分离

自测

点击作答

将列表 ADT 的「插入」接口从 O(n) 实现改为 O(1) 实现,使用该列表的客户端代码需要注意什么?

15第 15 页 · 自测

要点回顾

  • ✓静态结构靠秩寻址,动态结构靠位置关联
  • ✓动态化解决了中间插入的移动开销
  • ✓接口统一,实现可替换
  • ✓秩是数组视角,位置是节点视角
延伸主题:双向链表与哨兵节点位置接口的边界访问实现链表变体:跳表与循环链表
16第 16 页 · 要点回顾

课后思考

先自己想几分钟,再对照参考答案——开放问题没有唯一解。

1列表 ADT 的接口不变,换一套底层实现,哪些代码可以完全不用改?

参考答案从接口与实现分离入手:调用方只依赖接口约定的操作与语义,只要新实现保持相同行为,上层代码一行不动——这正是抽象数据类型的价值。

2如果应用中秩访问远多于插入删除,你会选向量还是列表?为什么?

参考答案选向量:秩访问 O(1) 且内存连续、缓存友好;列表访问要沿引用逐个走,O(n)。反过来若频繁在任意位置插删,列表才占优。

3位置这种抽象,换到树、图这类非线性结构还成立吗?会遇到什么新问题?

参考答案成立但需重新定义:树、图中节点不再有唯一的『前一个/后一个』,相邻关系变复杂;位置需刻画父子或邻接关系,遍历顺序不再唯一。

17第 17 页 · 课后思考