建立数据结构、抽象数据类型与算法分析的统一框架,掌握逻辑结构、存储结构,以及时间复杂度和空间复杂度的基本分析方法。
# 一、数据结构的基本概念
# 1. 数据、数据元素与数据项
- 数据:能够被计算机识别、存储和处理的符号集合。
- 数据元素:数据的基本单位,通常作为整体参与处理;也称记录、结点等。
- 数据项:构成数据元素的不可分割的最小单位。例如学生记录中的学号、姓名、成绩都是数据项。
- 数据对象:性质相同的数据元素的集合,是数据的一个子集。
例如,学生信息管理系统中,“全体学生记录” 是数据对象;一条学生记录是数据元素;其中的学号是数据项。不要把 “数据项” 和 “数据元素” 混为一谈。
# 2. 数据结构
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。它研究的不是单独的数据值,而是:
- 数据元素之间有什么关系;
- 这些关系如何存入计算机;
- 在这些数据上允许并应如何实现哪些操作。
可以概括为:
1 | flowchart TB |
# 逻辑结构
逻辑结构只描述元素间的逻辑关系,与计算机内存中的具体摆放位置无关。
| 类型 | 关系特征 | 典型例子 |
|---|---|---|
| 集合结构 | 元素同属一个集合,通常无明确的前驱、后继关系 | 集合、并查集所处理的元素集合 |
| 线性结构 | 除首尾外,每个元素有唯一前驱和唯一后继 | 线性表、栈、队列、串 |
| 树形结构 | 元素之间是一对多的层次关系 | 树、二叉树、堆 |
| 图状或网状结构 | 元素之间可以是多对多关系 | 图、状态转移网络 |
“线性” 指逻辑关系,不等于物理地址连续。链表的结点可分散存放,却仍是线性结构;二叉树若用数组存储,地址连续,却仍是树形结构。
从后续章节的视角,可把常见结构进一步对应为:
| 结构类别 | 常见结构 | 典型性质或用途 |
|---|---|---|
| 线性结构 | 数组、链表 | 数组连续存储、便于随机访问;链表以链接组织、便于已定位处插删 |
| 线性结构 | 栈、队列、双端队列 | 分别遵循后进先出、先进先出、两端可操作的规则 |
| 树形结构 | 二叉树、二叉搜索树、堆、平衡树 | 分层组织;堆常用于优先队列,平衡树控制查找高度 |
| 图状结构 | 有向图、无向图 | 可用邻接矩阵或邻接表存储,常用于路径和网络建模 |
| 散列结构 | 散列表 | 由散列函数确定位置,常用于字典和缓存 |
# 存储结构
存储结构也称物理结构,描述逻辑关系在计算机中的表示方式。
| 存储方式 | 核心思想 | 典型特征 |
|---|---|---|
| 顺序存储 | 用地址相邻的存储单元保存逻辑上相邻的元素 | 可按下标随机访问 |
| 链式存储 | 用结点中的指针或下标显式表示逻辑关系 | 结点地址可不连续 |
| 索引存储 | 除数据本身外,建立附加索引表 | 通过索引加速定位 |
| 散列存储 | 根据关键字经散列函数确定存储位置 | 查找效率通常较高 |
顺序存储与链式存储是本章线性表的两种重点实现。它们只规定存储组织方式,不规定对象一定在栈、堆或静态区:数组和结点实际位于何处取决于它们是局部对象、静态对象还是动态分配对象。
# 3. 数据类型与抽象数据类型
数据类型规定值的集合以及可施加于这些值上的操作集合,例如 C 语言中的 int、char。
抽象数据类型(Abstract Data Type, ADT)从用户视角定义数据对象、关系和操作,而不暴露具体存储实现。可以把线性表抽象地写为:
同一个 ADT 可以有多种实现。例如线性表既可由数组实现,也可由链表实现;用户调用的操作语义相同,时间和空间代价却可能不同。
数据结构提供组织数据的 “容器”,算法提供在该容器上解决问题的 “方法”。两者必须配合:例如数组实现栈能常数时间访问栈顶,链表实现队列并维护队尾指针可使入队、出队均为常数时间,堆可实现优先队列,图配合广度优先搜索可处理最短路径问题。
# 二、算法及其特性
# 1. 算法的定义
算法是为解决特定问题而规定的一组有限、明确、可执行的步骤。程序是算法在某种程序设计语言中的实现;同一算法可以有多种程序实现。
一个算法通常具有下列特性:
- 有穷性:执行有限步后必须终止。
- 确定性:每一步含义明确;给定相同输入时,执行路径和结果应确定。
- 可行性:每一步都能在有限时间内由基本操作完成。
- 有输入:可以有零个或多个输入。
- 有输出:至少有一个输出。
- 独立性:算法的思想不依赖某一种特定编程语言或机器。
“有穷性” 不等于程序运行得快;只要能在有限步骤后结束,即使代价极大,仍可能是算法。反之,未保证终止的循环不能称为满足定义的算法。
# 2. 评价算法的标准
考研分析中最核心的是正确性、可读性、健壮性、时间效率和空间效率。
- 正确性:对所有合法输入都产生满足规格的输出。
- 可读性:结构清晰,便于理解、验证和维护。
- 健壮性:面对非法输入或边界条件时能作出合理处理。
- 高效率与低存储量:用时间复杂度、空间复杂度描述随输入规模增长的代价。
例题 1.1:算法特性的判断
判断下列描述是否符合算法的有穷性要求。
- 从 1 开始不断输出自然数;
- 在有序数组中不断缩小查找区间,区间为空时返回失败;
- 对任意正整数反复执行某个未证明一定终止的规则,直到得到 1。
【解】
- 不符合:过程不会终止。
- 符合:每一步都严格缩小有限区间,最多执行有限次。
- 不能直接断定符合:只有能证明对全部输入均有限步终止,才满足有穷性。
# 三、时间复杂度
# 1. 基本思想
设输入规模为 ,算法执行基本操作的次数为 。时间复杂度关注 充分大时 的增长量级,而非某台机器上的秒数。
若存在正常数 ,使得对任意 都有
则记为
实际分析中通常执行三步:
- 确定输入规模 与基本操作;
- 写出基本操作的执行次数 ;
- 忽略常数系数和低阶项,只保留最高阶增长量级。
例如 ,则 。
# 2. 常见数量级
从增长较慢到较快,常见量级为:
| 量级 | 常见来源 |
|---|---|
| 常数次赋值、比较、按下标访问 | |
| 每次把问题规模缩小为原来的一部分,如二分查找 | |
| 一次线性扫描 | |
| 分治的每层线性处理、每层数对数 | |
| 两层规模均为 的嵌套循环 | |
| 、 | 枚举所有子集、枚举全排列 |
若两段循环前后顺序执行,复杂度相加后取最高阶;若一段循环嵌套在另一段内,才通常相乘。不能只要看到两个循环就机械写成 。
# 3. 循环与递归的分析
1 | int sum_pair(int a[], int n) { |
两段循环依次执行,基本操作次数为 ,故时间复杂度是 。
1 | int count_pairs(int n) { |
内层循环总次数为
故时间复杂度为 。
对于递归,除循环次数外还要写出递推式。例如二分查找满足
而每次只减少一个元素的递归满足
例题 1.2:含对数循环的复杂度
分析下列代码的时间复杂度。
1 | int f(int n) { |
【解】 外层循环中 依次为 ,执行次数为 ;每次内层执行 次。因此
# 4. 最好、最坏与平均情况
对输入规模相同的不同实例,基本操作次数可能不同。
- 最好情况时间复杂度:所有规模为 的输入中,执行次数的下界。
- 最坏情况时间复杂度:所有规模为 的输入中,执行次数的上界;题目未特别说明时通常按最坏情况分析。
- 平均情况时间复杂度:在给定输入概率分布下的期望执行次数,必须说明概率模型。
例如顺序查找目标值:首元素命中是 ,末元素命中或不存在是 ;在等概率成功查找模型下,平均比较次数约为 ,量级仍为 。
# 四、空间复杂度
空间复杂度 描述算法运行过程中所需额外存储空间随 的增长量级。讨论题目时应先说明口径:有些教材把输入数据本身也计入总空间;算法分析中更常考察辅助空间。
从组成上看,空间可分为与 无关的固定部分(程序代码、常量和有限个简单变量等)以及随输入规模变化的可变部分(输入数据、辅助数组、栈、队列和递归调用栈等)。取渐近量级时只保留增长最快的部分。
| 情形 | 辅助空间 |
|---|---|
| 只使用有限个标量变量 | |
| 新建长度为 的数组 | |
| 新建 矩阵 | |
| 二分递归的调用栈 | |
| 每次递归只缩小一个元素的调用栈 |
递归调用栈不能遗漏。例如递归二分查找每层只保存常数个变量,但递归深度为 ,故辅助空间为 ;改写为迭代版本后可降为 。
# 五、考场检查清单
- 先分清逻辑结构与存储结构,不能把 “连续地址” 误当作 “线性结构”。
- 写复杂度时先找规模 与基本操作,再数执行次数。
- 多段顺序代码取相加后的最高阶;嵌套结构再考虑相乘或求和。
- 递归题同时写时间递推式和递归深度,后者决定调用栈空间。
- 描述增长上界;比较算法时必须在相同规模和相同代价模型下进行。
