原发布于7月21日,因期末考试影响这篇没有学完,现开始继续更新
代码部分一些内容进行了省略,
数据结构 C语言版
绪论
基本概念
- 数据 Data:客观事物,所有能输入到计算机中并被计算机程序处理的符号的总称
- 数据元素 Data Element:数据基本单位,整体
- 数据项 Data Item:有独立含义,不可分割
- 数据对象 Data Object:性质相同的数据元素集合,数据的子集
数据结构
逻辑结构
- 集合
- 线性
- 树
- 图、网状
- 线性结构:线性表、栈和队列、字符串、数组、广义表(线性表的推广
- 非线性结构:树、二叉树、有向图、无向图
存储结构
也称物理结构
- 顺序存储结构:借助元素在存储器中的相对位置来表示数据元素之间的逻辑关系,数组
- 链式存储结构:给每个结点附加指针字段,用于存储后继元素的存储地址,指针
数据类型和抽象数据类型
- 数据类型 Data Type:一个值的集合和定义在这个值集上的一组操作的总称
- 抽象数据类型 Abstract Data Type,ADT:用户定义,包括:数据对象、数据对象上关系的集合、对数据对象的基本操作的集合
ADT 抽象数据类型名
{
数据对象:<数据对象的定义>
数据关系:<数据关系的定义>
基本操作:<基本操作的定义>
} ADT 抽象数据类型名
其中,基本操作的定义格式为
1 | 基本操作名(参数表) |
e.g. 复数
定义部分
ADT Complex
{
数据对象:D = {e1, e2 | e1, e2 ∈ R}
数据关系:S = {(e1, e2) | e1 是复数实部,e2 是复数虚部}
基本操作:
Create(&C, x, y)
操作结果:构造复数C,其实部和虚部分别赋以参数x, y
GetReal(C)
初始条件:C已存在
操作结果:返回C的实部值
GetImage(C)
初始条件:C已存在
操作结果:返回C的虚部值
Add(C1, C2)
初始条件:C1, C2是复数
操作结果:返回C1和C2的和
Sub(C1, C2)
初始条件:C1, C2是复数
操作结果:返回C1和C2的差
} ADT Complex表示部分
1
2
3
4
5typedef struct
{
float Realpart;
float Imagepart;
} Complex;实现部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27void Create(&Complex C, float x, float y)
{ // 构造一个复数
C.Realpart = x;
C.Imagepart = y;
}
float GetReal(Complex C)
{ // 取C的实部
return C.Realpart;
}
float GetImag(Complex C)
{ // 取C的虚部
return C.Imagepart;
}
Complex Add(Complex C1, Complex C2)
{ // 求两个复数的和
Complex sum;
sum.Realpart = C1.Realpart + C2.Realpart;
sum.Imagepart = C1.Imagepart + C2.Imagepart;
return sum;
}
Complex Sub(Complex C1, Complex C2)
{ // 求两个复数的差
Complex difference;
difference.Realpart = C1.Realpart - C2.Realpart;
difference.Imagepart = C1.Imagepart - C2.Imagepart;
return difference;
}
算法和算法分析
算法的特性
- 有穷性
- 确定性
- 可行性
- 输入
- 输出
评价优劣的基本标准
- 正确性
- 可读性
- 健壮性(对输入非法数据的处理反应
- 高效性
时间复杂度
- 问题规模:问题的输入量,问题大小的本质表示,一般用整数n
- 语句频度 Frequency Count:一条语句的重复执行次数
数量级 Order of Magnitude 为 O
渐进时间复杂度,简称时间复杂度 Time ComplexityT(n) = O(f(n))
旧博客:Time O
最好、最坏和平均时间复杂度
- 平均时间复杂度:在所有可能情况下,按照输入实例以等概率出现时,算法计算量的加权平均值
空间复杂度
渐进空间复杂度 Space ComplexityS(n) = O(f(n))
线性表
定义和特点
定义
- 由 n 个数据特性相同的元素构成的有限序列
- n = 0 时为空表
非空线性表的特点
- 存在唯一一个被称作“第一个”的数据元素
- 存在唯一一个被称作“最后一个”的数据元素
- 除第一个之外,结构中的每个数据元素均只有一个前驱
- 除最后一个之外,结构中的每个数据元素均只有一个后驱
类型定义
ADT List
{
数据对象:D = {ai | ai∈ElemSet, i=1, 2, …, n, n ≥ 0}
数据关系:R = {(ai-1, ai) | ai-1, ai∈D, i=2, …, n}
基本操作:
InitList(&L)
操作结果:构造一个空的线性表 L
Destroylist(&L)
初始条件:线性表 L 已存在
操作结果:销毁线性表 L
ClearList(&l)
初始条件:线性表 L 已存在
操作结果:将 L 重置为空表
ListEmpty(L)
初始条件:线性表 L 已存在
操作结果:若 L 为空表,则返回 true,否则返回 false
ListLength(L)
初始条件:线性表 L 已存在
操作结果:返回 L 中数据元素个数
GetElem(L, i, &e)
初始条件:线性表 L 已存在,且 1 ≤ i ≤ ListLength(L)
操作结果:用 e 返回 L 中第 i 个数据元素的值
LocateElem(L, e)
初始条件:线性表 L 已存在
操作结果:返回 L 中第1个值与 e 相同的元素在 L 中的位置,若数据不存在,返回值为0
PriorElem(L, cur_e, &pre_e)
初始条件:线性表 L 已存在
操作结果:若 cur_e 是 L 的数据元素,且不是第一个,则用 pre_e 返回其前驱,否则操作失败,pre_e 无定义
NextElem(L, cur_e, &next_e)
初始条件:线性表 L 已存在
操作结果:若 cur_e 是 L 的数据元素,且不是最后一个,则用 next_e 返回其后继,否则操作失败,next_e 无定义
ListInsert(&L, i, e)
初始条件:线性表 L 已存在,且 1 ≤ i ≤ ListLength(L) + 1
操作结果:在 L 中第 i 个位置之前插入新的数据元素 e, L 的长度加1
ListDelete(&L, i)
初始条件:线性表 L 已存在且非空,且 1 ≤ i ≤ ListLength(L)
操作结果:在 L 中第 i 个位置之前插入新的数据元素 e,L的长度都加1
TraverseList(L)
初始条件:线性表 L 已存在
操作结果:对线性表 L 进行遍历,在遍历的过程中
} ADT List
顺序表示
每个存储元素占 l 个存储单元
LOC(ai) = LOC(a1) + (i - 1) * l
只要确定了存储线性表的起始位置,线性表中任一数据元素都可以随机存储,所以线性表的顺序存储结构是一种随机存储的存储结构
1 |
|
初始化
1 | Status InitList(SqList &L) |
取值
1 | Status GetElem(SqList L, int i, ElemType &e) |
查找
1 | int LocateElem(SqList L, ElemType e) |
O(n)
插入
1 | Status ListInsert(SqList &L, int i, ElemType e) |
O(n)
删除
1 | Status ListDelete(SqList &L, int i) |
O(n)
链式表示
用一组任意的存储单元存储线性表的数据元素(可以不连续)
结点包括数据域和指针域,指针域中存储的信息称作指针或链
根据链表结点所含指针个数、指针指向和指针连接方式,将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等,其中除单链表、循环链表、双向链表,其他用于树和图等结构
1 | typedef struct LNode |
通常习惯上用 LinkList
定义单链表,强调定义的是某个单链表的头指针
用 LNode *
定义指向单链表任意结点的指针变量
通常增加一个多余的头结点
- 便于首元结点的处理,对首元结点的操作与其他数据元素相同,无需特殊处理
- 便于空表和非空表的统一处理:无需判断头指针是否为 nullptr
- 首元结点:链表中存储第一个数据元素 ai 的结点
- 头结点:首元结点之前附设的一个结点,其指针域指向首元结点,数据域可以不存储任何信息,也可以存储与数据元素类型相同的其他附加信息
- 头指针:指向链表的第一个结点
单链表是非随机存取的存储结构,要取得第 i 个数据元素必须从头指针出发顺链进行寻找,也称为顺序存取的存取结构
初始化
1 | Status InitList(LinkList &L) |
取值
1 | Status GetElem(LinkList L, int i, ElemType &e) |
O(n)
查找
1 | LNode *LocateElem(LinkList L, ElemType e) |
O(n)
插入
1 | Status ListInsert(LinkList &L, int i, ElemType e) |
O(n)
删除
1 | Status ListDelete(LinkList &L, int i) |
O(n)
创建
前插法
- 创建一个只有头结点的空链表
- 根据待创建的元素个数 n 循环 n 次执行
- 生成一个新结点
*p
- 输入元素值赋给新结点
*p
的数据域 - 将新结点
*p
插入到头结点之后
- 生成一个新结点
逆位序输入数据
1 | void CreateList_H(LinkList &L, int n) |
O(n)
后插法
- 创建一个只有头结点的空链表
- 尾指针 r 初始化,指向头结点
- 根据创建链表包括的元素个数 n 循环 n 次执行
- 生成一个新结点
*p
- 输入元素值赋给新结点
*p
的数据域 - 将新结点
*p
插入到尾结点*r
之后 - 尾指针
r
指向新的尾结点*p
- 生成一个新结点
1 | void CreateList_R(LinkList &L, int n) |
循环链表
Circular Linked List
表中最后一个结点的指针域指向头结点
终止条件
1 | p != L || p->next != L |
合并两个表时
1 | p = B->next->next; |
O(1)
双向链表
1 | typedef struct DuLNode |
插入
1 | Status ListInsert_DuL(DuLinkList &L, int i, ElemType e) |
删除
1 | Status ListDelete_DuL(DuLinkList &L, int i) |
顺序表和链表的比较
空间性能
- 存储空间分配
- 存储密度大小
存储密度 = 数据元素本身占用的存储量 / 结点结构占用的存储量
时间性能
- 存取元素的效率
- 插入和删除操作的效率
线性表的应用
合并(去重)
- 分别获取 LA 表长 m 和 LB 表长 n
- 从 LB 中第1个数据元素开始,循环 n 次执行、
- 从 LB 中查找第 i (1 ≤ i ≤ n)个数据元素赋给 e
- 在 LA 中查找元素 e,如果不存在,则将 e 插在表 LA 的最后
1 | void MergeList(List &LA, List LB) |
O(m * n)
合并(有序表 不去重)
顺序有序表
- 创建一个表长为 m+n 的空表 LC
- 指针 pc 初始化,指向 LC 的第一个元素
- 指针 pa 和 pb 初始化,分别指向 LA 和 LB 的第一个元素
- 当指针 pa 和 pb 均未达到相应表尾时,则依次比较 pa 和 pb 所指向的元素值,从 LA 或 LB 中拿较小的结点插入到 LC 的最后
- 如果 pb 已到达 LB 的表尾,依次将 LA 的剩余元素插入 LC 的最后
- 如果 pa 已到达 LA 的表尾,依次将 LB 的剩余元素插入 LC 的最后
1 | void MergeList_Sq(SqList LA, SqList LB, SqList &LC) |
O(m + n)
O(m + n)
链式有序表
- 指针 pa 和 pb 初始化,分别指向 LA 和 LB 的第一个结点
- LC 的结点取值为 LA 的头结点
- 指针 pc 初始化,指向 LC 的头结点
- 当指针 pa 和 pb 均未到达相应的表尾时,则依次比较 pa 和 pb 所指向的元素值,从 LA 或 LB 中拿较小的结点插入到 LC 的最后
- 将非空表的剩余段插入到 pc 所指结点之后
- 释放 LB 的头结点
1 | void MergeList_L(LinkList &LA, LinkList &LB, LinkList &LC) |
O(m + n)
O(1)
栈和队列
定义和特点
- 栈
栈 stack 仅在表尾进行插入或删除操作的线性表
表尾称为 栈顶 top,表头称为 栈底 bottom,不含元素的空表称为 空栈
后进先出 First In First Out (LIFO)
- 队列
队列 queue 是 先进先出 First In First Out (FIFO) 的线性表
队列中,允许插入的一端称为 队尾 rear,允许删除的一段称为 队头 front
栈的表示和操作
ADT Stack
{
数据对象:D = {ai | ai∈ElemSet, i = 1, 2, …, n, n ≥ 0}
数据关系:R = {i - 1, ai> | ai - 1, ai∈D, i = 2, …, n}
an为栈顶,ai为栈底
基本操作:
InitStack(&S)
操作结果:构造一个空栈 S
DestroyStack(&S)
初始条件:栈 S 已存在
操作结果:栈 S 被销毁
ClearStack(&S)
初始条件:栈 S 已存在
操作结果:栈 S 清为空栈
StackEmpty(S)
初始条件:栈 S 已存在
操作结果:若 S 为空栈,返回 true,否则返回 false
StackLength(S)
初始条件:栈 S 已存在
操作结果:返回 S 的元素个数,即栈的长度
GetTop(S)
初始条件:栈 S 已存在且非空
操作结果:返回 S 的栈顶元素,不修改栈顶指针
Push(&S, e)
初始条件:栈 S 已存在
操作结果:插入 e 为新的栈顶元素
Pop(&S, &e)
初始条件:栈 S 已存在且非空
操作结果:删除 S 的栈顶元素,并用 e 返回其值
StackTraverse(S)
初始条件:栈 S 已存在且非空
操作结果:从栈底到栈顶依次对 S 的每个数据元素进行访问
} ADT Stack
顺序栈
1 |
|
初始化
- 为栈动态分配一个最大容量为 MAXSIZE 的数组空间,使 base 指向栈底
- 栈顶指针 top 初始为 base,即为空栈
- stacksize 置为 MAXSIZE
1 | Status InitStack(SqStack &S) |
入栈
- 判断是否已满,若满返回 ERROR
- 将新元素压入栈顶,栈顶指针加1
1 | Status Push(SqStack &S, SElemType e) |
出栈
- 判断栈是否空,若空返回 ERROR
- 栈顶指针减1,栈顶元素出栈
1 | Status Pop(SqStack &S, SElemType &e) |
取栈顶元素
- 当栈非空时,返回栈顶元素的值
1 | SelemType GetTop(SqStack S) |
链栈
1 | typedef struct StackNode |
初始化
- 没必要设头结点,直接将栈顶指针置空
1 | Status InitStack(LinkStack &S) |
入栈
不需要判断是否满
- 为入栈元素 e 分配空间,用指针 p 指向
- 将新结点数据域置为 e
- 将新结点插入栈顶
- 修改栈顶指针为 p
1 | Status Push(LinkStack &S, SElemType e) |
出栈
- 判断栈是否为空,若空返回 ERROR
- 将栈顶元素赋给 e
- 临时保存栈顶元素的空间,以备释放
- 修改栈顶指针,指向新的栈顶元素
- 释放原栈顶元素的空间
1 | Status Pop(LinkStack &S, SElemType &e) |
取栈顶元素
1 | SElemType GetTop(LinkStack S) |
栈与递归
分治法
- 能将一个问题转变成一个新问题,解法相同或类同,不同的仅是处理的对象,并且这些处理对象更小且变化有规律
- 可以通过转化而使问题简化
- 必须有一个明确的递归出口,或称为递归的边界
1 | void p(参数表) |
如果当递归结束条件成立,只执行 return
操作时,分治法可以化简为
1 | void p(参数表) |
e.g. 遍历输出链表
1 | void TraverseList(LinkList p) |
e.g. 汉诺塔问题
问题描述
- 每次只能移动一个圆盘
- 圆盘可以插在 A、B、C 中任一塔座上
- 任何时刻都不能将一个较大的圆盘压在较小的圆盘之上
问题分析
设 A 柱上最初的盘子总数为 n,则当 n=1 时,只要将编号为 1 的圆盘从塔座 A 直接移至 C 上即可;否则,执行以下- 用 C 柱做过渡,将 A 柱上的 (n-1) 个盘子移到 B 柱上
- 将 A 柱上最后一个盘子直接移到 C 柱上
- 用 A 柱做过渡,将 B 柱上的 (n-1) 个盘子移到 C 柱上
算法步骤
- 如果 n=1,则直接将编号为 1 的圆盘从 A 移到 C,递归结束
- 否则
- 递归,将 A 上编号为 1 至 n-1 的圆盘移到 B,C 做辅助塔
- 直接将编号为 n 的圆盘从 A 移到 C
- 递归,将 B 上编号为 1 至 n-1 的圆盘移到 C,A 做辅助塔
算法描述
1 | int m = 0; |
队列的表示和操作
ADT Queue
{
数据对象:D = {ai | ai∈ElemSet, i = 1, 2, …, n, n ≥ 0}
数据关系:R = {i-1, ai> | ai-1, ai∈D, i = 2, …, n}
约定其中 ai 端为队列头,an 为队列尾
基本操作:
InitQueue(&Q)
操作结果:构造一个空队列 Q
DestroyQueue(&Q)
初始条件:队列 Q 已存在
操作结果:队列 Q 被销毁
ClearQueue(&Q)
初始条件:队列 Q 已存在
操作结果:将 Q 清为空队列
QueueEmety(Q)
初始条件:队列 Q 已存在
操作结果:若 Q 为空队列,则返回 true,否则返回 false
QueueLength(Q)
初始条件:队列 Q 已存在
操作结果:返回 Q 的元素个数,即队列的长度
GetHead(Q)
初始条件:Q 为非空队列
操作结果:返回 Q 的队头元素
EnQueue(&Q, e)
初始条件:队列 Q 已存在
操作结果:插入元素 e 为 Q 的新队尾元素
DeQueue(&Q, &e)
初始条件:Q 为非空队列
操作结果:删除 Q 的队头元素,并用 e 返回其值
QueueTraverse(Q)
初始条件:Q 已存在且非空
操作结果:从队头到队尾,依次对 Q 的每个数据元素访问
} ADT Queue
循环队列——队列的顺序表示和实现
1 |
|
初始化时,令 front = rear = 0
;每当有新的队尾元素时,尾指针 rear 加一;每当删除队列队头元素时,头指针 front 加一
- 假溢出:溢出 base
- 解决方案:通过取模实现头尾指针的循环
- 同时带来的问题:循环队列不能以头、尾指针的值是否相同来判别队列是否为空
- 处理方法
少用一个元素空间,即队列空间大小为 m 时,有 m-1 个元素就认为是队满。这样判断队空的条件不变,即当头、尾指针的值相同时,则认为队空;而当尾指针在循环意义上加 1
后是等于头指针,则认为队满
- 队空的条件:
Q.front == Q.rear
- 队满的条件:
(Q.rear + 1) % MAXQSIZE == Q.front
设一个标志位以区别队列是空还是满
初始化
- 为队列分配一个最大容量为 MAXQSIZE 的数组空间,base 指向数据空间的首地址
- 头指针和尾指针置为零,表示队列为空
1 | Status InitQueue(SqQueue &Q) |
求队列长度
1 | int QueueLength(SqQueue Q) |
入队
- 判断队列是否满,若满返回 ERROR
- 将新元素插入队尾
- 队尾指针加 1
1 | Status EnQueue(SqQueue &Q, QElemType e) |
出队
- 判断队列是否为空,若空返回 ERROR
- 保存队头元素
- 队头指针加 1
1 | Status DeQueue(SqQueue &Q, QElemType &e) |
取队头元素
1 | QElemType GetHead(SqQueue Q) |
链队——队列的链式表示和实现
1 | typedef struct QNode |
初始化
- 生成新结点作为头结点,队头和队尾指针指向此结点
- 头结点的指针域置空
1 | Status InitQueue(LinkQueue &Q) |
入队
- 为入队元素分配结点空间,用指针 p 指向
- 将新结点数据域置为 e
- 将新结点插入到队尾
- 修改队尾指针为 p
1 | Status EnQue(LinkQueue &Q, QElemType e) |
出队
- 判断队列是否为空,若空返回 ERROR
- 临时保存队头元素的空间,以备释放
- 修改头结点的指针域,指向下一个结点
- 判断出队元素是否为最后一个元素,若是,则将队尾指针重新赋值,指向头结点
- 释放原队头元素的空间
1 | Status DeQueue(LinkQueue &Q, QElemType &e) |
取队头元素
1 | QElemType GetHead(LinkQueue Q) |
串、数组和广义表
串
串 string (或字符串)由零个或多个字符组成的有限序列,一般记为
s = “aia2 … an“ (n ≥ 0)
串中字符的数目 n 称为串的 长度
零个字符的串称为 空串 null string ø
串中任意个连续的字符组成的子序列称为该串的 子串,包含子串的串称为 主串
通常称字符在序列中的序号为该字符在串中的 位置
两串 相等,当且仅当两个串的值相等
由一个或多个空格组成的串 " "
称为 空格串 blank string
抽象类型定义
ADT String
{
数据对象:D = {ai | ai∈CharacterSet, i = 1, 2, …, n, n≥0}
数据关系:R1 = {(ai-1, ai) | ai-1, ai∈D, i = 2, …, n}
基本操作:
StrAssign(&T, chars)
初始条件:chars 是字符串常量
操作结果:生成一个其值等于 chars 的串 T
StrCopy(&T, S)
初始条件:串 S 已存在
操作结果:由串 S 复制得串 T
StrEmpty(S)
初始条件:串 S 已存在
操作结果:若 S 为空串,则返回 true,否则返回 false
StrCompare(S, T)
初始条件:串 S 和串 T 已存在
操作结果:若 S > T,则返回值 > 0;若 S = T,则返回值 = 0;若 S < T,则返回值 < 0
StrLength(S)
初始条件:串 S 已存在
操作结果:返回 S 的元素个数,称为串的长度
ClearString(&S)
初始条件:串 S 已存在
操作结果:将 S 清为空串
Concat(&T, S1, S2)
初始条件:串 S1 和 S2 已存在
操作结果:用 T 返回由 S1 和 S2 联接而成的新串
SubString(&Sub, S, pos, len)
初始条件:串 S 已存在,1 ≤ pos ≤ StrLength(S) 且 0 ≤ len ≤ StrLength(S) - pos + 1
操作结果:用 Sub 返回串 S 的第 pos 个字符起长度为 len 的字串
Index(S, T. pos)
初始条件:串 S 和 T 已存在,T 是非空串,1 ≤ pos ≤ StrLength(S)
操作结果:若主串 S 中存在和串 T 值相同的子串,则返回它在主串中 S 中第 pos 个字符之后第一次出现的位置;否则函数值为 0
Replace(&S, T, V)
初始条件:串 S T V 已存在,T 是非空串
操作结果:用 V 替换主串 S 中出现的所有与 T 相等的不重叠的子串
StrInsert(&S, pos, T)
初始条件:串 S 和 T 已存在,1 ≤ pos ≤ StrLength(S) + 1
操作结果:在串 S 的第 pos 个字符之前插入串 T
StrDelete(&S, pos, len)
初始条件:串 S 已存在,1 ≤ pos ≤ StrLength(S) - len + 1
操作结果:从串 S 中删除第 pos 个字符起长度为 len 的子串
DestroyString(&S)
初始条件:串 S 已存在
操作结果:串 S 被销毁
} ADT String
存储结构
顺序存储
1 | // 定长 |
1 | // 堆 |
链式存储
1 |
|
模式匹配算法
子串的定位运算通常称为串的 模式匹配 或 串匹配
BF 算法
- 分别利用计数指针 i 和 j 指示主串 S 和模式 T 中当前正待比较的字符位置,i 初始值为 pos,j 初始值为 1
- 如果两个串均未比较到串尾,即 i 和 j 均分别小于等于 S 和 T 的长度时,循环之行
- S.ch[i] 和 T.ch[j] 比较,若相等,则 i 和 j 分别指示串中下个位置,继续比较后续字符
- 若不等,指针后退重新开始匹配,从主串的下一个字符 (i = i - j + 2) 起再重新和模式的第一个字符 (j = 1) 比较
- 如果 j > T.length,说明模式 T 中第一个字符相等的字符在主串 S 中的序号 (i - T.length);否则称匹配不成功,返回 0
1 | int Index_BF(SString S, SString T, int pos) |
最好情况 O(m + n)
最坏情况 O(m * n)
KMP 算法
Knuth, Morris, Pratt 共同设计实现的
改进在于,每当一趟匹配过程中出现字符比较不等时,不需回溯 i 指针,而是利用已经得到的“部分匹配”的结果将模式向右“滑动”尽可能远的一段距离后,继续进行比较
O(m + n)
数组
类型定义
类型相同
可以通过下标访问数据元素
当每个元素处于 n (n ≥ 1) 个关系中,称为 n 维数组
ADT Array
{
数据对象: ji = 0, ..., bi - 1, i = 1, 2, ..., n, D = {aj1j2...jn | n (> 0) 称为数组的维数,bi 是数组第 i 维的长度,ji 是数组元素第 i 维的下标,aj1j2...jn ∈ Elemset}
数据关系: R = {R1, R2, ..., Rn}
基本操作:
InitArray(&A, n, boundi, ..., doundn)
操作结果:若维数 n 和各维长度合法,则构造相应的数组 A,并返回 OK
DestroyArray(&A)
操作结果:销毁数组 A
Value(A, &e, indexl, ..., indexn)
初始条件:A 是 n 维数组,e 为元素变量,随后是 n 个下标值
操作结果:若各下标不超界,则 e 赋值为所指定的 A 的元素值,并返回 OK
Assign(&A, e, indexl, ..., indexn)
初始条件:A 是 n 维数组,e 为元素变量,随后是 n 个下标值
操作结果:若下标不越界,则将 e 的值赋给所指定的 A 的元素,并返回 OK
} ADT Array
顺序存储
一旦建立了数组,结构中的数据元素个数和元素之间的关系就不再发生变动
在大部分语言中,都是以行序为主序的存储结构,而在如 FORTRAN 语言中,以列序为主序
LOC(i, j) = LOC(0, 0) + (n x i + j)L
特殊矩阵的压缩存储
- 对称矩阵
- 三角矩阵
- 对角矩阵
- 稀疏矩阵(较复杂
广义表
定义
线性表的推广,也称为列表
LS = (a1, a2, …, an),其中 ai可以是单个元素,也可以是广义表,分别称为广义表 LS 的原子和子表
习惯上,用大写字母表示广义表的名称,用小写字母表示原子
定义是一个递归的过程
重要结论:
- 广义表的元素可以是子表,子表的元素还可以是子表
- 广义表可以为其他广义表所共享(指可以不必列出子表的值,而是通过子表的名称来引用
- 广义表可以是一个递归的表,即可以是本身的一个子表
最重要的两个运算:
- 取表头 GetHead(LS): 取出的表头为非空广义表的第一个元素,可以是单原子,也可以是一个子表
- 取表尾 GetTail(LS): 取出的表尾为除去表头之外,由其余元素构成的表,即一定是一个广义表
() 和 (()) 不同
存储结构
通常采用链式存储结构
头尾链表的存储结构
- 表结点:标志域、指示表头的指针域、指示表尾的指针域
- 原子结点标志域、值域
1 | typedef enum{ATOM, LIST} ElemTag; // ATOM == 0: 原子, LIST == 1: 子表 |
- 除空表的表头指针为空外,对任何非空广义表,其表头指针均指向一个表结点,且该结点中的 hp 域指示广义表表头(或为原子结点,或为表结点),tp 域指向广义表表尾(除非表尾为空,则指针为空,否则必为表结点
- 容易分清列表中原子和子表所在层次
- 最高层的表结点个数即为广义表的长度
扩展线性链表的存储结构
无论是原子结点还是表结点均由三个域组成,原子结点增加一个 tp
树和二叉树
定义
树
树 Tree 是 n (n ≥ 0) 个结点的有限集,或为空树 (n = 0),或为非空树
对于非空树 T:
- 有且仅有一个称之为根的结点
- 除根结点外的其余结点可分为 m (m > 0) 个互不相交的有限集 T1, T2, …, Tm,其中每个集合本身又是一棵树,并称为根的子树 SubTree
基本术语
- 结点:树中的一个独立单元,包含一个数据元素及若干指向其子树的分支
- 结点的度:结点拥有的子树数
- 树的度:树内各结点度的最大值
- 叶子:度为 0 的结点(或称为终端结点
- 非终端结点:度不为 0 的结点(或称为分支结点,除根结点外,非终端结点也称为内部结点
- 双亲和孩子:结点的子树的根称为该结点的孩子,相应的,该结点称为孩子的双亲
- 兄弟:同一个双亲的孩子之间互称兄弟
- 祖先:从根到该结点所经分支上的所有节点
- 子孙:以某结点为根的子树中的任一结点
- 层次:结点的层次从根开始定义起,根为第一层
- 堂兄弟:双亲在同一层的结点互为堂兄弟
- 树的深度:树中结点的从最大层次(或称为树的高度
- 有序树和无序树:从左到右是有次序的(不能交换),称为有序树,最左边的子树的根称为第一个孩子
- 森林:m (m ≥ 0) 棵互不相交的树的集合。对树中的每个结点而言,其子树的集合即为森林
就逻辑结构而言,任何一棵树都是一个二元组 Tree = (root, F)
二叉树
二叉树 Binary Tree 是 n (n ≥ 0) 个结点所构成的集合
对于非空树 T:
- 有且仅有一个称之为根的结点
- 除根结点之外的其余结点分为两个互不相交的子集 T1 和 T2,分别称为 T 的左子树和右子树,且 T1 和 T2 本身又都是二叉树
与树的区别:
- 二叉树每个结点至多只有两颗子树(即不存在度大于 2 的结点
- 二叉树的子树有左右之分,其次序不能任意颠倒
抽象数据类型定义
ADT Tree
{
数据对象:D 是具有相同特性的数据元素的集合
基本操作:
InitTree(&T)
操作结果:构造空树 T
DestroyTree(&T)
初始条件:树 T 存在
操作结果:销毁树 T
CreateTree(&T, definition)
初始条件:definition 给出树 T 的定义
操作结果:按 definition 构造树 T
Cleartree(&T)
初始条件:树 T 存在
操作结果:将树 T 清为空树
TreeEmpty(T)
初始条件:树 T 存在
操作结果:若 T 为空树,则返回 true,否则 false
TreeDepth(T)
初始条件:树 T 存在
操作结果:返回 T 的深度
Root(T)
初始条件:树 T 存在
操作结果:返回 T 的根
Value(T, cur_e)
初始条件:树 T 存在,cur_e 是 T 中某个结点
操作结果:返回 cur_e 的值
Assign(T, cur_e, value)
初始条件:树 T 存在,cur_e 是 T 中某个结点
操作结果:结点 cur_e 赋值为 value
Parent(T, cur_e)
初始条件:树 T 存在,cur_e 是 T 中某个结点
操作结果:若 cur_e 是 T 的非根结点,则返回它的双亲,否则函数值为空
LeftChild(T, cur_e)
初始条件:树 T 存在,cur_e 是 T 中的某个结点
操作结果:若 cur_e 是 T 的非叶子结点,则返回它的最左孩子,否则返回空
RightSibling(T, cur_e)
初始条件:树 T 存在,cur_e 是 T 中某个结点
操作结果:若 cur_e 有右兄弟,则返回它的右兄弟,否则函数值为空
InsertChild(&T, p, i, c)
初始条件:树 T 存在,p 指向 T 中某个结点,1 ≤ i ≤ p 所指结点的度 +1,非空树 c 与 T 不相交
操作结果:插入 c 为 T 中 p 所指结点的第 i 棵子树
DeleteChild(&T, p, i)
初始条件:树 T 存在,p 指向 T 中某个结点,1 ≤ i ≤ p 所指结点的度
操作结果:删除 T 中 p 所指结点的第 i 棵子树
TraverseTree(T)
初始条件:树 T 存在
操作结果:按某种次序对 T 的每个结点访问一次
} ADT Tree
ADT BinaryTree
{
数据对象:D 是具有相同特性的数据元素的集合
基本操作:
InitBiTree(&T)
操作结果:构造空二叉树 T
DestroyBiTree(&T)
初始条件:二叉树 T 存在
操作结果:销毁二叉树 T
CreateBiTree(&T, definition)
初始条件:definition 出二叉树 T 的定义
操作结果:按 definition 构造二叉树 T
ClearBiTree(&T)
初始条件:二叉树存在
操作结果:将二叉树 T 清为空树
BiTreeEmpty(T)
初始条件:二叉树 T 存在
操作结果:若 T 为空二叉树,则返回 true,否则 false
BiTreeDepth(T)
初始条件:二叉树 T 存在
操作结果:返回 T 的深度
Root(T)
初始条件:二叉树 T 存在
操作结果:返回 T 的根
Value(T, e)
初始条件:二叉树 T 存在,e 是 T 中某个结点
操作结果:返回 e 的值
Assign(T, &e, value)
初始条件:二叉树 T 存在,e 是 T 中某个节点
操作结果:结点 e 赋值为 value
Parent(T, e)
初始条件:二叉树 T 存在,e 是 T 中某个结点
操作结果:若 e 是 T 的非根结点,则返回它的双亲,否则返回空
LeftChild(T, e)
初始条件:二叉树 T 存在,e 是 T 中某个结点
操作结果:返回 e 的左孩子,若 e 无左孩子,则返回空
RightChild(T, e)
初始条件:二叉树 T 存在,e 是 T 中某个结点
操作结果:返回 e 的右孩子,若 e 无右孩子,则返回空
LeftSibling(T, e)
初始条件:二叉树 T 存在,e 是 T 中某个结点
操作结果:返回 e 的左兄弟,若 e 是 T 的左孩子或无左兄弟,则返回空
RightSibling(T, e)
初始条件:二叉树 T 存在,e 是 T 中某个结点
操作结果:返回 e 的右兄弟,若 e 是 T 的右孩子或无右兄弟,则返回空
InsertChild(&T, p, LR, c)
初始条件:二叉树 T 存在,p 指向 T 中某个结点,LR 为 0 或 1,非空二叉树 c 与 T 不相交且右子树为空
操作结果:根据 LR 为 0 或 1,插入 c 为 T 中 p 所指结点的左或右子树,p 所指结点的原有左或右子树则成为 c 的右子树
DeleteChild(&T, p, LR)
初始条件:二叉树 T 存在,p 指向 T 中某个结点,LR 为 0 或 1
操作结果:根据 LR 为 0 或 1,删除 T 中 p 所指结点的左或右子树
PreOrderTraverse(T)
初始条件:二叉树 T 存在
操作结果:先序遍历 T,对每个结点访问一次
InOrderTraverse(T)
初始条件:二叉树 T 存在
操作结果:中序遍历 T,对每个结点访问一次
PostOrderTraverse(T)
初始条件:二叉树 T 存在
操作结果:后序遍历 T,对每个结点访问一次
LevelOrderTraverse(T)
初始条件:二叉树 T 存在
操作结果:层序遍历 T,对每个结点访问一次
} ADT BinaryTree
二叉树性质
- 在二叉树的第 i 层上至多有 2i - 1 个结点 (i ≥ 1)
- 深度为 k 的二叉树至多有 wk - 1 个结点 (i ≥ 1)
- 对任何一棵二叉树 T,如果其终端结点数 n0,度为 2 的结点数为 n2,则 n0 = n2 + 1
满二叉树:深度为 k 且含有 2k - 1 个结点的二叉树
- 特点:
- 每一层上的结点数都是最大结点数,即每一层 i 的结点数都具有最大值 2i - 1
- 特点:
完全二叉树:深度为 k 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应
- 特点:
- 叶子结点只可能在层次最大的两层上出现
- 对任一结点,若其右分支下的子孙的最大层次为 l,则其左分支下的子孙的最大层次必为 l 或 l + 1
- 特点:
- 具有 n 个结点的完全二叉树的深度为 ⌊log2n⌋ + 1
- 如果对一棵有 n 个结点的完全二叉树的结点按层序编号,则对任一结点 i (1 ≤ i ≤ n) 有
- 如果 i = 1,则结点 i 是二叉树的根,无双亲;如果 i > 1,则其双亲 PARENT(i) 是结点 ⌊i / 2⌋
- 如果 2i > n,则结点 i 无左孩子(结点 i 为叶子结点);否则其左孩子 LCHILD(i) 是结点 2i
- 如果 2i + 1 > n,则结点 i 无右孩子;否则其右孩子 RCHILD(i) 是结点 2i + 1
二叉树存储结构
顺序存储结构
1 |
|
仅适用于完全二叉树,因为在最坏的情况下,一个深度为 k 且只有 k 个结点的单支树(树中不存在度为 2 的结点)却需要长度为 2k - 1 的一维数组
链式存储结构
二叉链表包含:
- 数据域
- 左右指针域
三叉链表增加一个指向双亲结点的指针域
在含有 n 个结点的二叉链表中有 n + 1 个空链域,可以利用这些空链域存储其他有用信息,从而得到线索链表
1 | typedef struct BiTNode |
二叉树遍历和线索二叉树
遍历
traversing binary tree
- DLR
- LDR
- LRD
- DRL
- RDL
- RLD
先序遍历
若二叉树为空,则空操作;否则
- 访问根结点
- 先序遍历左子树
- 先序遍历右子树
中序遍历
若二叉树为空,则空操作;否则
- 中序遍历左子树
- 访问根结点
- 中序遍历右子树
后序遍历
若二叉树为空,则空操作;否则
- 后序遍历左子树
- 后序遍历右子树
- 访问根结点
中序遍历
递归算法
1 | void InOrderTraverse(BiTree T) |
非递归算法
- 初始化一个空栈 S,指针 p 指向根结点
- 申请一个结点空间 q,用来存放栈顶弹出的元素
- 当 p 非空或者栈 S 非空时,循环执行以下操作:
- 如果 p 非空,则将 p 进栈,p 指向该结点的左孩子
- 如果 p 为空,则弹出栈顶元素并访问,将 p 指向该结点的右孩子
1 | void InOrderTraverse(BiTree T) |
时间空间都为 O(n)
先序遍历的顺序创建二叉链表
- 扫描字符序列,读入字符 ch
- 如果 ch 是一个 ‘#’ 字符,则表明该二叉树为空,即 T 为 NULL;否则执行以下操作
- 申请一个结点空间 T
- 将 ch 赋给
T->data
- 递归创建 T 的左子树
- 递归创建 T 的右子树
1 | void CreateBiTree(BiTree &T) |
复制二叉树
如果是空树,递归结束,否则执行以下操作:
- 申请一个新结点空间,复制根结点
- 递归复制左子树
- 递归复制右子树
1 | void Copy(BiTree T, BiTree &NewT) |
计算二叉树深度
如果是空树,递归结束,深度为 0,否则执行以下操作:
- 递归计算左子树的深度记为 m
- 递归计算右子树的深度记为 n
- 如果 m 大于 n,二叉树的深度为 m + 1,否则为 n + 1
1 | int Depth(BiTree T) |
统计二叉树中结点的个数
1 | int NodeCount(BiTree T) |
线索二叉树
如果有 n 个结点的二叉链表中必定存在 n + 1 个空链域,因此可以充分利用这些空链域来存放结点的前驱和后继信息
试做如下规定:
若结点有左子树,则其 lchild 域指示其左孩子,否则令 lchild 域指示其前驱
若结点有右子树,则其 rchild 域指示其右孩子,否则令 rchild 域指示其后继
为了避免混淆,需要改变结点结构,增加两个标志域,
lchild | LTag | data | RTag | rchild |
---|
其中
LTag =
- 0 lchild 域表示结点的左孩子
- 1 lchild 域表示结点的前驱
RTag
同理
1 | typedef struct BiThrNode |
- 线索链表:以这种结点结构构成的二叉链表
- 线索:其中指向结点前驱和后继的指针
- 线索二叉树 (Threaded Binary Tree):加上线索的二叉树
- 线索化:对二叉树以某种次序遍历使其次序遍历使其变为线索二叉树的过程
树和森林
树的存储结构
双亲表示法
以一组连续的存储单元存储树的结点,每个结点除了数据域 data 外,还附设一个 parent 域用以指示其双亲结点的位置
孩子表示法
由于树中每个结点可能有多棵子树,则可用多重链表,即每个结点有多个指针域
若不存储 degree,则多重链表中的结点是同构的。会有很多空链域,空间较浪费。一棵有 n 个结点度为 k 的树中必有 n(k - 1) + 1 个空链域
若存储 degree,则多重链表中的结点是不同构的。虽然节约存储空间,但操作不方便
另一种办法是,把每个结点的孩子结点排列起来,看成是一个线性表,且以单链表做存储结构,则 n 个结点有 n 个孩子链表(叶子的孩子链表为空表)。而 n 个头指针又组成一个线性表,为了便于查找,可采用顺序存储结构