计算机 · C 语言

抽象数据类型

Abstract Data Types

约 10 分钟 · v11

抽象数据类型 ADT

cs-c/abstract-data-types · COMP2521
0 / 3

1 先看:底层偷偷换掉,你用起来没差别

左边是调用方代码——它只会 push / pop / peek,从不碰内部。右边是底层实现,你可以在「数组」和「链表」之间随时切换。点操作按钮,再切换底层:留意——调用方一行都不用改,每个操作的结果也完全一样,变的只是右边内部长什么样。

一个栈(Stack),两种底层

底层实现:
调用方代码(永远不变)
底层实现(切换看差异)
点上面的操作按钮,再切换底层——结果一致,内部不同。

两种底层的 push/pop 都满足同一条规则:后进先出(LIFO)。数组实现靠"末尾 + top 下标",链表实现靠"头插 + head 指针"——内部完全不同,但对外行为一模一样,所以调用方感知不到换没换。

这就是抽象数据类型(ADT):它只规定有哪些操作、每个操作的行为(契约),不规定内部怎么存、怎么实现。"栈"是一个 ADT——push/pop/peek + LIFO 规则;用数组还是链表,是实现的自由。

一句话:ADT = 接口(能做什么)与实现(怎么做到)的分离。调用方只依赖接口,不依赖内部。

2 接口 vs 实现:到底分的是什么

把一个数据类型拆成两层看,是 COMP2521 的核心思维。栈这个 ADT 为例:

接口(ADT,对外承诺)实现 A:数组实现 B:链表
push(s, x)
把 x 放到栈顶
arr[top++] = x;头插一个新节点
pop(s)
弹出并返回栈顶
return arr[--top];摘下 head,head=head→next
peek(s) / isEmpty(s)arr[top-1] / top==0head→item / head==NULL
契约(不变量)后进先出;满 / 空时的行为有明确约定。两种实现都必须遵守——否则就不是"栈"。

接口是承诺(操作签名 + 行为契约),写在头文件 Stack.h 里;实现是兑现承诺的方式,藏在 Stack.c 里。调用方 #include "Stack.h" 就够,看不到也不该看到 .c 里的数组或链表。

在 C 里靠"不透明指针"(typedef struct stack *Stack;,结构体定义只在 .c 里)做到这层隐藏——调用方拿到的是个句柄,碰不到字段。

3 这层分离值多少钱

为什么要费劲分两层?因为它买来四样东西——上面那个"换底层不改调用方"的演示,就是第一样的现场证据。

① 可替换实现
数组版不够用(要频繁扩容)就换链表版,调用方零改动。先用简单实现跑通,再悄悄换成高效实现。
② 可独立测试
只对着接口写测试(push 三个再 pop,必得最后一个)。换了实现,同一套测试照样跑,验证新实现没违约。
③ 隐藏复杂度
调用方脑子里只装 4 个操作,不用管指针、扩容、边界。认知负担被锁在实现里,团队分工也清爽。
④ 契约即文档
接口 + 不变量就是这个类型的"说明书"。看 Stack.h 就知道怎么用,不必读实现。

你已经天天在用 ADT 而不自知:C 的 FILE *(fopen/fread/fclose,你从不碰它内部)、各种语言的 List / Map / Set——都是"接口稳定、实现可换"的 ADT。

4 自测

三道题,点选项即时判对错。

去做例题:实现一个 Stack ADT(数组版 / 链表版各一遍)
去做例题

一句话带走:

ADT = 接口与实现分离。接口规定"有哪些操作 + 行为契约"(如栈的 push/pop/peek + LIFO),实现规定"内部怎么存"(数组 or 链表)。调用方只依赖接口、碰不到内部——于是换实现不改调用方、能独立测试、复杂度被隐藏、接口即文档。COMP2521 里几乎每个数据结构(栈、队列、列表、集合、优先队列)都是先定 ADT 再选实现——"先想清楚要什么操作,再决定怎么存",这就是这门课的思维主线。

键盘快捷键

P
push 随机数
O
pop
A / L
切到数组 / 链表底层
?
打开本帮助