(a)接口与实现

官官方信息技术老师·15 页·深入(追求细节与边界)·0 次浏览·38 天前
数据结构抽象与实现动态数组C++

向量:接口与实现

看完你能自己定义向量接口并写出一个可动态扩容的实现

按 空格/→ 演示下一步

问一句
1 / 15 页

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

数据结构抽象与实现动态数组C++

向量:接口与实现

看完你能自己定义向量接口并写出一个可动态扩容的实现

1第 1 页 · 向量:接口与实现

什么是 ADT

家里的插座只规定两孔、220V,谁造的电器都能用;至于电从哪来、怎么发电,插座不管。ADT 就是给数据结构定这样一张「插座规格书」。

只说做什么
声明有哪些操作:插入、删除、按位置访问
不说怎么做
内部用数组还是链表,调用者无需知道
数学化描述
用值集与操作集定义,独立于任何编程语言
电器插座对应 →ADT 接口

插座的孔位与电压规格对应 ADT 规定的操作集

2第 2 页 · 什么是 ADT

接口 vs 实现

很多人把「向量能做什么」和「向量怎么做到」混为一谈,导致换实现就重写代码。这页把两层彻底分开。

接口:逻辑规范
  • 回答「能做什么」:get/insert/remove 的语义
  • 使用者唯一需要知道的东西
  • 稳定不变,是承诺与契约
实现:具体做法
  • 回答「怎么做」:用数组还是链表、扩容策略
  • 使用者应该完全不关心的内部细节
  • 随时可替换,代价与复杂度各异
正确的理解是:接口是契约、实现是履约方式,因为只有解耦,才能换实现而不改使用者的代码。
3第 3 页 · 接口 vs 实现

抽象与实现的分层结构

从上往下读:依赖箭头向下;虚线是「不需要知道」的关系,这正是抽象的意义。

图解渲染中…
u1:任何调用方:业务代码、其他模块i2:契约:操作名+语义,不含数据结构细节r3:具体数据结构与算法,如数组或链表
4第 4 页 · 抽象与实现的分层结构

向量的定义

上一页我们把向量当作一个 ADT 看接口。现在打开它:你最熟悉的数组就是原型——元素排成一条线,靠位置编号访问。向量把这条线推广成真正的抽象结构。

线性序列
元素一个接一个排成一条线,每个元素至多一个前驱和后继
秩(rank)
元素的编号,从 0 起;秩唯一确定一个元素
逻辑与物理
秩是逻辑位置;在数组实现里秩恰好等于物理下标
与数组的边界
数组是向量的一种实现;向量不限定存储方式
电影院座位号对应 →秩与元素

座位号唯一确定一个座位,就像秩唯一确定一个元素

e=V[r],pred(e)=V[r−1],succ(e)=V[r+1]e = V[r],\quad \mathrm{pred}(e)=V[r-1],\quad \mathrm{succ}(e)=V[r+1]
5第 5 页 · 向量的定义

向量 ADT 操作总览

从 ADT 根节点分出读、写两大接口分支,每类下挂具体操作;先看分类,再看每个操作的归属。

图解渲染中…
r0:ADT 只定义操作名与语义,不含实现c1:size 作用于整体,其余四个作用于单个元素b2:insert/remove 会引起元素整体移动
6第 6 页 · 向量 ADT 操作总览

ADT 模板声明

cpp

用 C++ 模板类声明展示向量接口的规范写法:声明即契约,元素类型与操作一目了然。

代码高亮加载中…

class 公有区是 ADT 的操作集(契约),private 区是实现细节。使用者只依赖 public 声明,实现可自由更换。

7第 7 页 · ADT 模板声明

一次插入的完整过程

insert(r,e) 一次调用,底层其实走了四步,一步都省不掉。

1
秩检查
验证 0≤r≤n,非法秩直接抛异常,绝不写入
2
判满扩容
若 n 已达容量,先加倍空间再继续,保证装得下
3
自后前移
从末尾到 r 逐个后移一格,腾出空位
4
写入更新
e 放入 A[r],n 加一,接口返回 r
8第 8 页 · 一次插入的完整过程

区间删除与单元素删除

删除的本质是把后缀整体前移,覆盖被删区间。

1
确认删除区间
单元素删除是 remove(lo,hi) 的特例,hi-lo=1
2
后缀前移
hi 之后每个元素依次左移,覆盖区间 [lo,hi)
3
更新规模
size 减去 hi-lo,尾部旧元素等待析构
4
必要时缩容
装填因子低于下限时收缩,避免空间浪费
9第 9 页 · 区间删除与单元素删除

操作实例自测

点击作答

在长度为 n 的向量中,把元素 e 插入到秩为 r 的位置,插入后 e 的秩以及原来秩为 r 的元素分别是多少?

10第 10 页 · 操作实例自测

构造与析构

前几页我们调 insert、del 很顺手,但 _elem 这块数组是动态 new 出来的。既然是借来的空间,用完谁来还?这页看向量生命周期两端的「管家」:构造与析构。

构造函数
new 分配内部数组,按初始容量就位
析构函数
delete[] 释放数组,防止内存泄漏
成对出现
申请与释放必须配对,随对象生灭
租房与退房对应 →构造与析构

构造对应签约拿房,析构对应退租交还钥匙

11第 11 页 · 构造与析构

复制:copyFrom 实现

cpp

邓俊辉《数据结构》向量真实源码:从数组区间 [lo,hi) 逐元素复制,观察容量如何申请。

代码高亮加载中…

容量取 2×区间长度,为后续 append 留余量、摊还降低扩容次数;逐元素赋值使整体复杂度为 O(hi-lo)。

12第 12 页 · 复制:copyFrom 实现

复制中的内存布局

沿箭头看两块内存:左为源向量,右为新建向量;对比两种复制方式的指针指向差异。

图解渲染中…
a1:源向量的数据指针,指向原始元素区b1:copyFrom 新申请的独立内存空间c1:仅复制指针值,两向量共享同一块内存
13第 13 页 · 复制中的内存布局

要点收束

  • ✓ADT 只规定操作语义,与底层实现解耦
  • ✓接口是契约,实现可替换且不影响使用者
  • ✓构造析构管理生命周期,RAII 保证资源不泄漏
  • ✓复制必须深拷贝,否则共享内存引发悬空指针
  • ✓插入删除的边界与扩容策略决定复杂度
14第 14 页 · 要点收束

课后思考

三个问题请先独立想几分钟,再对照参考答案,看看思路是否一致。

1copyFrom 为什么同时接收『元素区间』和『目标空间』两个职责?合并成一个数组复制函数不行吗?

参考答案关键是职责分离:区间参数描述『复制什么』,目标空间参数描述『复制到哪』。合并后向量扩容、区间拼接等场景都得各自重写,接口反而失去弹性。

2如果 copyFrom 里不加 _elem = new T[r-lo],直接用外部传入的数组会怎样?

参考答案向量将失去对内存的所有权:外部数组一旦被释放或修改,向量立即悬空失效。自管内存是实现『动态扩容』的前提,也是封装边界的底线。

3把这套『接口与实现分离』的思路搬到链表,复制策略还能照搬 copyFrom 的连续内存假设吗?

参考答案不能。链表节点地址不连续,只能逐节点深复制,无 memcpy 式整体搬运。但『自管内存、按需分配』的原则不变——变的只是分配的粒度与方式。

15第 15 页 · 课后思考