Computer Science · An Overview · Chapter 8

数据抽象DATA ABSTRACTION

主存储器只是一排可寻址的单元——本章研究如何模拟出数组、栈、队列、树这样的结构,让数据的用户把它们当作抽象工具来访问,再进一步用抽象数据类型(ADT)和类把数据与操作打包成完整的类型。

数组 array栈 stack · LIFO队列 queue · FIFO树 tree 指针 pointer环状队列 circular queue二叉搜索树 BST抽象数据类型 ADT类与对象 class & object
数据抽象:内存单元之上模拟出栈、队列与树
内存只有一排可寻址单元——数组、栈、队列、树都是“模拟”出来的抽象工具。
Learning Map

本章学习地图

每节末尾附「本节小结」与 2 道课堂选择题(A / B / C / D),共 12 题,参考答案见页尾。一条主线:内存只有一排单元——所有数据结构都是模拟出来的抽象工具。

小节核心问题关键概念
8.1 基本数据结构常用的数据组织方式有哪些?数组 / 聚合 · 列表 · 栈 LIFO · 队列 FIFO · 树
8.2 相关概念这些结构在内存里真实存在吗?抽象与用户 · 静态 / 动态结构 · 指针 · 垃圾收集
8.3 数据结构的实现抽象结构怎样落进主存储器?邻接表 · 头 / 尾指针 · 环状队列 · 链式二叉树
8.4 案例研究如何按字母顺序存储名字列表?二叉搜索树 · 递归搜索
8.5 定制的数据类型基本类型不够用怎么办?用户定义类型 · 实例 · 抽象数据类型 ADT
8.6 类和对象ADT 在面向对象里长什么样?类是 ADT 的扩展 · 对象是实例
8.1 Basic Data Structures

8.1 基本数据结构:数组、列表、栈、队列与树

数据结构 data structure:在计算机内存中组织和存储数据的方式——选合适的数据结构可以显著提高程序的效率和性能。

Array & Aggregate

数组与聚合

数组 array:“矩形的”数据块,每一项具有相同的数据类型——例:24 小时的温度读数表。

聚合类型 aggregate type:数据项可以有不同类型和大小的块,各项称为字段 field——例:一条员工记录 = 姓名 + 年龄 + 技能级别。

List

列表

列表 list:表项按顺序排列的集合。开头叫表头 head,结尾叫表尾 tail

严格限制列表项的访问方式,就得到两种特殊列表:队列

Stack · LIFO

栈:后进先出

stack:项只能在表头(栈顶 top)添加和删除——入栈 push、出栈 pop。最后放入的最先移除,故称后进先出 LIFO

栈常用于回溯 backtracking——退出顺序与进入顺序相反,像一摞盘子:最后放上去的最先被拿走。

Queue · FIFO

队列:先进先出

队列 queue:项只能从表头移除,新项只能从表尾插入——按存储的顺序依次移除,故称先进先出 FIFO

像排队买票:先来先服务。打印任务、消息缓冲都是队列的典型应用。

动手试一试:栈的 push / popInteractive · Stack
点击「入栈 push」把盘子叠到栈顶,「出栈 pop」只能从栈顶拿走——注意移除顺序与放入顺序正好相反(LIFO)。
栈为空。先 push 几个盘子试试。
Tree

树:层次化的集合

tree:项具有层次化组织形式的集合——例:公司组织结构图。树中每个位置叫节点 node;顶部是根节点 root node,端点处是终端节点 / 叶节点 leaf node;从根到叶子的最长路径上的节点数叫树的深度 depth

直接后代 / 直接祖先 / 同一双亲的节点分别称为孩子 / 双亲 / 兄弟 children / parent / sibling。每个双亲的孩子都不超过两个的是二叉树 binary tree,提及时分左分支和右分支;任意节点与其下面的节点构成子树 subtree,每个孩子是其双亲下面一棵子树的根,称为一个分支 branch

本节小结 · 8.1

数组各项同类型,聚合各项可不同类型;栈在栈顶进出(LIFO,用于回溯),队列头出尾进(FIFO);树 = 层次化的集合,根、叶、深度、孩子 / 双亲 / 兄弟、子树 / 分支是描述树的基本词汇,孩子不超过两个的是二叉树。一句话:栈像一摞盘子,队列像一条队伍——限制访问方式,就得到了可预期的行为。

课堂练习 · 8.1(单选,参考答案见页尾)
Q1栈(stack)是一种后进先出(LIFO)结构,它的添加和删除操作发生在?
答案 B。栈的入栈 push 和出栈 pop 都只能在表头(栈顶)进行——这正是它能保证“后进先出”的原因。
Q2一棵树中,“深度”(depth)指的是?
答案 C。深度 = 根到叶子的最长路径上的节点数,反映树的“层数”。
8.2 Related Concepts

8.2 相关概念:抽象、静 / 动态结构与指针

主存储器并不是按数组、栈、队列、树来组织的——它只是一组可寻址的存储单元顺序组成,所有这些结构都必须被模拟

Abstraction Again

再谈抽象

数组、列表、栈、队列、树都是被创建的抽象工具:对数据的用户屏蔽实际存储的细节,让用户就像数据以方便的形式存储着一样来访问。

用户不一定是人——可能是客户机,也可能是程序中的任何模块 user = person / client / module

Pointer

指针

主存单元由数字地址标识,地址本身也可编码存储。指针 pointer 就是包含这种编码地址的存储区,用来记录数据项的位置。

许多现代语言把指针作为基本数据类型——像整数一样可声明、分配和操作。

对比项静态结构 static动态结构 dynamic
判断依据大小或形状随时间改变大小或形状随时间改变
实现要求只需提供访问(或许加修改)指定项的方法要处理添加和删除项,还要找到增长所需的存储空间
管理难度更容易管理更复杂:伴随垃圾收集 garbage collection——回收不再使用的存储空间以备将来使用
本节小结 · 8.2

所有数据结构都是模拟出来的抽象工具;用户(人或模块)享有按抽象方式访问数据的特权。静态结构只管访问,动态结构还要管增删与扩容;指针 = 存着地址的存储区,是链接结构的黏合剂;垃圾收集回收动态结构不再使用的空间。抽象的意义:换一个底层实现,用户的代码一行都不用改。

课堂练习 · 8.2(单选,参考答案见页尾)
Q3关于数据结构中的“用户”(user),下列说法正确的是?
答案 B。“用户”取决于视角:人、客户机或程序模块都行——凡是按抽象方式访问数据的一方都是用户。
Q4指针(pointer)是?
答案 B。指针存的是“地址”而非数据本体——它告诉你数据项放在哪里。
8.3 Implementing Data Structures

8.3 数据结构的实现:让抽象落进主存储器

目标:理解处理这些结构的程序,如何被翻译成处理主存储器中数据的机器语言程序。

Contiguous List & Stack

邻接表与存栈

邻接表 contiguous list:整个列表存储在一大块存储单元中,连续的项依次放在相邻单元——适合顺序访问,但插入删除要挪动一片。

存栈:预留足够容纳最大栈的存储块,块底为栈底;用一个栈指针记录栈顶位置,push 时指针上移,pop 时下移。

Linked Binary Tree

链式二叉树

每个节点 = 数据 + 左孩子指针 + 右孩子指针;专门的根指针 root pointer 存放根节点地址,对树的访问从根指针开始。

某方向没有节点时,对应指针赋 null(终端节点的两个指针都是 null)。另一种方案:连续存储块——单元 n 的左、右孩子分别存单元 2n 和 2n+1,一层接一层。

动手试一试:环状队列 circular queueInteractive · Queue
随着插入和移除,队列会在存储器中慢慢“漂移”出预留的存储块。解决方案:队尾到达末端时回到起始端继续插入,头指针越界时同样调回开头——就像把存储块首尾接成一个环。下面 8 个单元的环状队列,点「入队 / 出队」观察头、尾指针如何绕环移动。
队列长度:3 / 8
初始:队列占单元 3、4、5,头指针指向队头,尾指针指向下一个空闲处。
操控数据结构:PrintList() —— 一个抽象工具
def PrintList(List):
    CurrentPointer = List.Head
    while CurrentPointer != None:
        print(CurrentPointer.Value)  · 沿指针走向下一项

函数完成后就是一个抽象工具:用户只管调用 PrintList(Economics301);以后改存储方式只需改函数内部,用户调用不变。

本节小结 · 8.3

栈:一块存储 + 一个栈指针;队列:一块存储 + 头指针和尾指针,并以环状方式防止队列漂移出存储块;链式二叉树:节点 = 数据 + 左 / 右孩子指针,根指针指路,null 标记尽头,也可用 2n / 2n+1 规则连续存储。把存储细节包进函数,用户按抽象工具的方式下指令——指针微调 = 逻辑操作:移动一个指针,就完成了“出队”这个动作。

课堂练习 · 8.3(单选,参考答案见页尾)
Q5环状队列(circular queue)解决的问题是?
答案 B。环状队列让队列首尾相接、在预留块内循环移动,不再漂出存储块。
Q6链式存储的二叉树中,终端节点(叶节点)的特征是?
答案 B。终端节点两个指针都为 null——下面没有孩子了;数据字段照常存数据。
8.4 A Short Case Study

8.4 一个简短的案例研究:按字母顺序存储名字

任务:对一个按字母顺序的名字列表,支持搜索 search、按序打印 print、插入 insert——开发一组函数构成完整的抽象工具。

二叉搜索树:小了往左、大了往右
二叉搜索树 binary search tree:左子树都比节点小,右子树都比节点大——每次比较排除一半。
Why a Tree?

为什么用树而不是列表?

每次比较都能排除一半的子树——和二分查找异曲同工,比在顺序列表里逐个翻找快得多。

Recursion

递归表达

“在子树中搜索”与“在整棵树中搜索”是同一个问题,只是规模更小——所以直接调用 Search 自己,代码简洁且与树的层次结构天然吻合。

二叉搜索树的搜索逻辑(递归伪代码)
def Search(Tree, TargetValue):
    if Tree is None: return None        # 失败:走到空树
    elif 目标 == Tree.Value: return Tree  # 成功
    elif 目标 < Tree.Value:
        搜左子树 Search(Tree.Left, ...)
    elif 目标 > Tree.Value:
        搜右子树 Search(Tree.Right, ...)
动手试一试:二叉搜索树的递归搜索Interactive · BST Search
选一个目标字母,观察 Search 如何逐层比较:目标更小 → 左子树目标更大 → 右子树,每一步排除一半。字母按字典序比较。
选择上方任意目标开始搜索(含一个不存在的 X,看看“走到空树”长什么样)。
本节小结 · 8.4

案例研究把本章串起来:选结构(二叉搜索树)→ 定存储(链式节点)→ 写函数(Search 递归)→ 得抽象工具。递归搜索的核心:与当前节点比较,小了去左子树、大了去右子树,空树则失败。好的数据结构 + 好的算法 = 又快又清晰的程序。

课堂练习 · 8.4(单选,参考答案见页尾)
Q7案例研究的 Search 函数中,若目标值小于当前节点的值,下一步应该?
答案 B。二叉搜索树左子树的值都比当前节点小——目标更小就往左递归。
Q8案例研究的任务是按字母顺序存储名字列表,并要求支持的操作不包括
答案 D。任务是搜索、按序打印、插入三件事,没有“数笔画”。
8.5 Customized Data Types

8.5 定制的数据类型:从用户定义类型到 ADT

基本类型(整型 / 浮点型 / 字符型 / 布尔型)不够用?用它们作构建块,定义自己的类型——用户定义的数据类型 user-defined data type

C 语言:定义一个新聚合类型
struct EmployeeType
{
    char  Name[25];
    int   Age;
    float SkillRating;
};
struct EmployeeType DistManager, SalesRep1, SalesRep2;
Employee1.Age = 26;
Type vs Instance

类型与实例

用户定义的数据类型本质上是构建实例的模板:模板描述所有实例共有的属性,但本身不是实例。EmployeeType 是模板,DistManager、SalesRep1、SalesRep2 是它的 3 个实例 instance

Limitation

传统用户定义类型的局限

只允许程序员定义新的存储系统,没有提供对这些数据进行的操作——而且程序里任何函数都能直接访问字段,绕开仔细检查,可能破坏结构的固有特征(比如栈的 LIFO)。

Abstract Data Type

抽象数据类型 ADT:完整的数据类型

抽象数据类型 abstract data type(ADT):同时包含数据(表示)函数(行为)的用户定义数据类型。两大特征:① 定义为单个单元——语言提供语法把 ADT 的数据和函数组织在一起,简化维护和调试;② 隐藏内部结构——外部代码要访问数据,必须通过专门提供的函数,提供了可靠性。对比:用户定义类型只有数据;ADT 有数据 + 操作,因此是完整的数据类型

Java interface:StackType
interface StackType
{
    public int     pop();            // 取栈顶项
    public void    push(int item); // 入栈
    public boolean isEmpty();        // 是否为空
    public boolean isFull();         // 是否已满
}

interface 不指定栈如何存储、函数用什么算法——细节被抽象出来,由别处的代码实现;程序员照样可以把变量声明为 StackType 类型,用 StackOne.push(25) 这样的调用使用栈。

动手试一试:ADT 的“保护墙”Interactive · Hiding
ADT 把内部结构隐藏起来:左边是“用户代码”,右边墙内是栈的私有数据。试试两种访问方式——
用户代码 USER CODE
墙内 · 私有数据 HIDDEN
private int[] StackEntries
private int StackPointer
没有保护机制,一条草率的赋值语句就能破坏栈的后进先出行为。
本节小结 · 8.5

用户定义的数据类型 = 基本类型组合成的同名聚合体;用与基本类型相同的方式声明变量,用“变量.字段”访问各项;区分类型与实例:类型是模板,实例是按模板建的实例。ADT = 数据表示 + 操作函数,组织成单个单元并隐藏内部结构,外部只能通过规定好的函数访问——模板描述“有什么”,实例才是“那一个”,就像图纸与房子。

课堂练习 · 8.5(单选,参考答案见页尾)
Q9抽象数据类型(ADT)与用户定义的数据类型的本质区别是?
答案 B。用户定义类型只有数据表示;ADT 有数据 + 操作函数,所以是完整的数据类型。
Q10在 C 语言的例子中,EmployeeType 与 DistManager 的关系是?
答案 C。类型是模板,DistManager 是按 EmployeeType 模板声明(构建)的实例。
8.6 Classes and Objects

8.6 类和对象

面向对象范型:系统由称为对象的单元组成,对象通过彼此交互完成任务;每个对象都是响应其他对象消息的实体,对象由称为类的模板描述。

Java:StackOfIntegers 类(节选)
class StackOfIntegers implements StackType
{
    private int[] StackEntries = new int[20];
    private int StackPointer = 0;
    public void push(int NewEntry) { ... }
    public int  pop()  { ... }
    public boolean isEmpty() { ... }
    public boolean isFull()  { ... }
}
StackType StackOne = new StackOfIntegers();
StackOne.push(106);  OldValue = StackOne.pop();
Class = ADT's Description

类 ≈ 抽象数据类型的描述

class 为 ADT(StackType)中声明的每个函数提供函数体,并包含实现所需的数据(数组 + 栈指针)——类的实例就称为对象 object

Class Extends ADT

类是抽象数据类型的扩展

特征与 ADT 本质上一样(数据 + 操作 + 隐藏),但类更进一步:支持继承 inheritance 等面向对象机制——所以说类是抽象数据类型的扩展

本节小结 · 8.6

对象是响应消息的实体,由类这个模板描述;类为 ADT 的每个函数提供实现,并私有地持有数据。类与 ADT 的区别:类是 ADT 的扩展——在数据 + 操作 + 隐藏之上,加入了继承等面向对象的能力。ADT 定契约(interface),类给实现(class),对象是干活的实例。

课堂练习 · 8.6(单选,参考答案见页尾)
Q11关于类(class)与抽象数据类型(ADT)的关系,正确的是?
答案 B。类在 ADT 之上加入继承等机制,是 ADT 的扩展——方向别记反。
Q12在面向对象范型中,对象(object)是?
答案 B。对象 = 响应消息的实体,由类模板描述;对象之间通过交互(发消息)完成任务。
Answer Key

课堂练习参考答案(共 12 题)

逐题一句话解析;错题请回到对应小节复习。速记:8.1→B C|8.2→B B|8.3→B B|8.4→B D|8.5→B C|8.6→B B

8.1 基本数据结构

Q1 B 栈的添加和删除只能在表头(栈顶)进行。

Q2 C 深度 = 根到叶子的最长路径上的节点数。

8.2 相关概念

Q3 B “用户”取决于视角:人、客户机或程序模块都行。

Q4 B 指针存的是编码地址,用来记录数据项的位置。

8.3 数据结构的实现

Q5 B 环状队列让队列首尾相接,不再漂出存储块。

Q6 B 终端节点两个指针都为 null——下面没有孩子了。

8.4 案例研究

Q7 B 目标更小 → 去左子树递归;更大 → 去右子树。

Q8 D 任务是搜索、按序打印、插入,没有“数笔画”。

8.5 定制的数据类型

Q9 B ADT 有数据 + 操作,是完整的数据类型。

Q10 C 类型是模板,DistManager 是按模板建的实例。

8.6 类和对象

Q11 B 类在 ADT 之上加入继承等机制,是其扩展。

Q12 B 对象 = 响应消息的实体,由类模板描述。

Glossary

全章总结与术语表

内存只有一排可寻址单元——数组、栈、队列、树都是模拟出来的抽象工具;ADT 和类把数据与操作打包成完整类型。下一章预告:数据库系统。

小节一句话总结
8.1 基本数据结构数组同类型、聚合可混合;栈 LIFO、队列 FIFO;树分层
8.2 相关概念结构都是模拟的抽象;静态易管、动态要收集垃圾;指针存地址
8.3 实现栈一个指针、队列两个指针成环;二叉树链式或 2n / 2n+1
8.4 案例研究二叉搜索树 + 递归搜索:小了往左、大了往右
8.5 定制类型用户定义类型只有数据;ADT 数据 + 操作 + 隐藏
8.6 类和对象类是 ADT 的扩展,对象是类的实例
数组 array聚合 aggregate字段 field列表 list stack入栈 push出栈 pop回溯 backtracking 队列 queue表头 / 表尾 head / tailtree节点 node 根 / 叶 root / leaf深度 depth二叉树 binary tree子树 subtree 分支 branch指针 pointer垃圾收集 garbage collection邻接表 contiguous list 环状队列 circular queue根指针 root pointer用户定义类型 user-defined data type 实例 instance抽象数据类型 ADT接口 interfaceclass对象 object