数据结构
第一章:绪论
数据结构概念
数据基本概念
数据是信息的载体,是描述客观事物属性的数、字符以及所有能输入到计算机并且被计算机程序识别和处理的符号的集合,数据是计算机程序加工的原料
数据元素、数据项
数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。
一个数据元素可由若干个数据项组成,数据项是构成数据元素的不可分割的最小单位
数据结构和数据对象
结构
各个元素之间的关系
数据结构
相互之间存在一种或者多种特定关系的数据元素的集合数据对象
具有相同性质的数据元素的集合,是数据的一个子集
数据结构三要素
逻辑结构(数据元素之间的逻辑关系是什么)
-
集合
各个元素同属于一个集合,别无其它关系 -
线性结构
数据元素之间是一对一的关系。
除了第一个元素,其它所有结构都有唯一的前驱;
除了最后一个元素,所有元素都有唯一后继 -
树形结构
数据元素之间是
一对多的关系 -
图状结构(网状结构)
数据元素之间是
多对多的关系

数据的物理结构(存储结构,计算机表示数据元素的逻辑关系)
顺序存储
把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现
链式存储
逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系
索引存储
在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)
散列存储
根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储
-
数据的物理结构的特点
- 若采用顺序存储,则各个数据元素在物理上必须是连续的;若采用非顺序存储,则各个元素在物理上可以是离散的。
- 数据的存储结构会影响存储空间分配的方便程度
- 数据的存储结构会影响对数据运算的速度
数据的运算
- 概念
施加在数据上的运算包括运算的定义和实现。
运算的定义是针对逻辑结构,指出运算的功能;
运算的实现是针对存储结构,指出运算的具体操作步骤。
数据类型和抽象数据类型
- 数据类型
- 定义
数据类型是一个值的集合和定义在此集合上的一组操作的总称。 - 原子类型
这个值不可再分的数据类型
比如bool类型和int类型 - 结构类型
其值可以再分为若干成分(分量)的数据类型
比如C语言的结构体
- 定义
- 抽象数据类型 (ADT)
- 定义
抽象数据组织及与之相关的操作
使用数学化的语言定义数据的逻辑结构、定义运算,与具体实现无关 - 涉及
只涉及逻辑结构和数据的运算,不参与物理结构(存储结构)
- 定义
小结
- 在探讨一种数据结构时
- 定义逻辑结构(数据元素之间的关系)
- 定义数据的运算(针对现实需求,应该对这种逻辑结构进行什么样的运算)
- 确定某种存储结构,实现数据结构,并实现一些对数据结构的基本运算
算法概念
算法基本概念
程序=数据结构+算法
其中数据结构用于把现实世界的问题信息化,将信息存进计算机。同时实现对数据结构的基本操作。
算法用于处理这些信息以解决实际问题
算法的五大特性
-
有穷性
一个算法必须总在执行有穷步之后结束,并且每一步都可以在 有穷的事件内完成
算法是有穷的,但是 程序可以是无穷的 -
确定性
算法中的每一条指令必须有确切的含义对于 相同的输入只能得出相同的输出 -
可行性
算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现 -
输入
一个算法有零个或者多个输入,这些输入取自于某个特定的对象的集合
注意:随机数的输入是电脑 -
输出
一个算法有一个或者多个输出,这些输出是与输入有某种特定关系的量
好算法的特质(设计算法的目标)
-
正确性
算法应该能够正确地解决问题 -
可读性
算法应具有良好的可读性,以帮助人们理解
算法可以用伪代码描述甚至是可以用文字描述,重要的没有歧义地描述出解决问题的步骤 -
健壮性
输入非法数据的时候,算法能适当地作出反应或者调整,而不会产生莫名其妙的结果 -
高效率和低存储需求- 高效率
执行速度快,时间复杂度低 - 低存储需求
不占用内存,空间复杂度低
- 高效率
小结


第二章:线性表

线性表
线性表(Linear List)是具有相同数据类型的\(n(n≥0)\)个数据元素的有限序列,其中\(n\)为表长,当\(n = 0\)时线性表是一个空表
若用\(L\)命名线性表,则其一般表示为 \(L = (a_1, a_2, … , a_i, a_{i+1}, … , a_n)\)
\(a_1\)是表头元素 \(a_n\)是表尾元素
除第一个元素外,每个元素有且仅有一个直接前驱
除最后一个元素外,每个元素有且仅有一个直接后继
线性表的基本操作
InitList(&L):初始化表。构造一个空的线性表L,分配内存空间。
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间。
ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数。
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值。
Empty(L):判空操作。若L为空表,则返回true,否则返回false。
C++引用关键字
&
什么时候要传入参数的引用“&”: 对参数的修改结果需要“带回来”,是引用类型而不是值类型

顺序表
顺序表定义
用顺序存储的方式实现线性表顺序存储,把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
顺序表的实现
| 顺序表的实现-静态分配 | 顺序表的实现-动态分配 |
|---|---|
![]() |
![]() |
![]() |
![]() |
如果“数组”存满了怎么办:
直接放弃
顺序表的特点
顺序表的实现:
- 随机访问,即可以在\(O(1)\)时间内找到第
i个元素。 - 存储密度高,每个结点只存储数据元素
- 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)
- 插入、删除操作不方便,需要移动大量元素
顺序表的插入、删除
| 顺序表的插入 | 顺序表的删除 |
|---|---|
![]() |
![]() |
增加
i的合法性判断:

| 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 |
|---|---|---|
| \(O(1)\) | \(O(n)\) | \(O(n)\) |
顺序表的查找
| 顺序表的按位查找 | 顺序表的按值查找 |
|---|---|
![]() |
![]() |
- 正是如此,在初始化顺序表时候malloc需要强制转换为与数据元素的数据类型相对应的指针
时间复杂度=O(1)- 随机存取:由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第
i个元素,
| 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 |
|---|---|---|
| \(O(1)\) | \(O(n)\) | \(O(n)\) |
单链表
单链表定义

带头结点的&不带头结点单链表
| 带头结点 | 不带头结点 |
|---|---|
![]() |
![]() |
区别:
- 不带头结点,写代码更麻烦
- 对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑
- 对空表和非空表的处理需要用不同的代码逻辑
- 一般使用的都是带头结点的单链表

单链表的插入操作
按位序插入
| 按位序插入(带头结点) | 按位序插入(不带头结点) |
|---|---|
![]() |
![]() |
按位序插入(不带头结点)
单链表插入函数:ListInsert(&L,i,e):
插入操作,在表L中的第i个位置上插入指定元素e
-
找到第
i-1个结点,将新结点插入其后 -
若带有头结点,插入更加方便,头结点可以看作“
第0个”结点,直接做上面的操作即可 -
若插在表中则与插在表头一样进行操作,可以插入成功
-
若插在表尾则
s->next为NULL(在表的定义时规定的),可以插入成功 -
若
i插在表外(i>Lengh)则p指针指向NULL(While循环一直指向p->next)不能插入成功
| 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 |
|---|---|---|
| \(O(1)\) | \(O(n)\) | \(O(n)\) |
按位序插入(不带头结点)
单链表插入函数:ListInsert(&L,i,e):
插入操作,在表L中的第i个位置上插入指定元素e
不存在“第0个结点,因此i=1时需要特殊处理- 找到第
i-1个结点,将新结点插入其后 - 若
i!=1则处理方法和带头结点相同 - 值得注意的是
int j =1而非带头结点的0(带头结点的头结点为第0个结点)
结论:不带头结点写代码更不方便,推荐用带头结点
指定结点插入操作
| 指定结点的前插操作 | 指定结点的后插操作 |
|---|---|
![]() |
![]() |
指定结点的前插操作注意事项
因为仅知道指定结点的信息和后继结点的指向,因此无法直接获取到前驱结点
方法1:获取头结点然后再一步步找到指定结点的前驱 |
方法2:将新结点先连上指定结点p的后继,接着指定结点p连上新结点s,将p中元素复制到s中,将p中元素覆盖为要插入的元素e |
|---|---|
![]() |
![]() |
单链表的删除
按位序删除(带头结点)
单链表删除函数:ListDelete(&L,i,&e)
- 删除操作,删除表L中第
i个位置的元素,并用e返回删除元素的值。 - 找到第
i-1个结点,将其指针指向第i+1个结点,并释放第i个结点

指定结点的删除
-
删除结点p,需要修改其前驱结点的next指针,和指定结点的前插操作一样
-
方法1:传入头指针,循环寻找p的前驱结点
-

-
方法2:偷天换日,类似于结点前插的实现
-

如果要删除的结点p是最后一个结点:
- 只能从表头开始依次寻找
p的前驱,时间复杂度\(O(n)\) - 这就体现了单链表的局限性:无法逆向检索,某些时候不太方便
单链表的查找
| 按位查找 | 按值查找 |
|---|---|
![]() |
![]() |
按位查找
-
GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。 -
实际上单链表的插入中找到
i-1部分就是按位查找i-1个结点 -
如果
i=0则直接不满足j<i则指针p直接返回头结点L -
如果
i超界则当时p指向了NULL,指针p返回NULL -
平均时间复杂度:\(O(n)\)
按值查找
单链表按值查找函数:LocateElem(L,e);
-
在表
L中查找具有给定关键字值的元素。 -
能找到的情况:
p指向了e值对应的元素,返回该元素 -
不能找到的情况:
p指向了NULL,指针p返回NULL -
平均时间复杂度:\(O(n)\)
求表的长度:

- 平均时间复杂度:\(O(n)\)
单链表的建立
尾插法
| 方法1:套用之前学过的位序插入,每次都要从头开始往后面遍历,时间复杂度为\(O(n^2)\) | 方法2:增加一个尾指针r,每次插入都让r指向新的表尾结点,时间复杂度为\(O(n)\) |
|---|---|
![]() |
![]() |
头插法
- 每次插入元素都插入到单链表的表头
- 头插法和之前学过的单链表后插操作是一样的,可以直接套用
L->next=NULL;可以防止野指针

总结:
- 头插法、尾插法:核心就是初始化操作、指定结点的后插操作
- 注意设置一个指向表尾结点的指针
- 头插法的重要应用:链表的逆置
双链表
为什么要要使用双链表
- 单链表:无法逆向检索,有时候不太方便
- 双链表:可进可退,但是存储密度更低一丢丢

双链表的初始化(带头结点)

双链表的插入

- 小心如果p结点为最后一个结点产生的空指针问题,因此循环链表应运而生(详见后面的循环链表插入删除)
- 注意指针的修改顺序
双链表的删除

双链表的遍历

循环链表
循环单链表与单链表的区别:
| 单链表 | 循环单链表 |
|---|---|
表尾结点的next指针指向NULL |
表尾结点的next指针指向头结点 |
| 从一个结点出发只能找到后续的各个结点 | 从一个结点出发可以找到其他任何一个结点 |
循环单链表初始化

- 从头结点找到尾部,时间复杂度为\(O(n)\)
- 如果需要频繁的访问表头、表尾,可以让
L指向表尾元素(插入、删除时可能需要修改L) - 从尾部找到头部,时间复杂度为\(O(1)\)
循环双链表与双链表的区别
| 双链表 | 循环双链表 |
|---|---|
表头结点的prior指向NULL |
表头结点的prior指向表尾结点 |
表尾结点的next指向NULL |
表尾结点的next指向头结点 |
循环双链表的初始化

循环链表的插入

- 对于双链表来说如果p结点为最后一个结点,因为
next结点为null,p->next->prior=s会产生的空指针问题 - 循环链表规避因为最后结点的
next结点为头结点因此不会发生问题
循环链表的删除
与循环链表的插入相同

注意点:
写代码时候注意以下几点,以此规避错误:
- 如何判空
- 如何判断结点
p是否是表尾/表头元素(后向/前向遍历的实现核心) - 如何在表头、表中、表尾插入/删除一个结点
静态链表
静态链表定义
-
分配一整片连续的内存空间,各个结点集中安置
-
每个结点由两部分组成:
data(数据元素)和next(游标) -
0号结点充当“头结点”,不具体存放数据
-
游标为
-1表示已经到达表尾 -
游标充当“指针”,表示下个结点的存放位置,下面举一个例子:
-
每个数据元素
4B,每个游标4B(每个结点共8B),设起始地址为addr,e1的存放地址为addr\(+ 8×2\)(游标值)
定义静态链表
| 方法1 | 方法2 |
|---|---|
![]() |
![]() |
基本操作
初始化
- 把
a[0]的next设为-1 - 把其他结点的
next设为一个特殊值用来表示结点空闲,如-2
顺序表和链表的比较
逻辑结构
都属于线性表,是线性结构
| 类型 | 顺序表 | 单链表 |
|---|---|---|
功能 |
每个结点中只存放数据元素 | 每个结点除了存放数据元素外,还要存储指向下一个结点的指针 |
优点 |
可随机存取,存储密度高 | 不要求大片连续空间,改变容量方便 |
缺点 |
要求大片连续空间,改变容量不方便 | 存储密度低,不可随机存取,要耗费一定空间存放指针 |
基本操作
创建
销毁
增删
查找
使用条件
开放式问题的解题思路
问题: 请描述顺序表和链表的
bla bla bla…实现线性表时,用顺序表还是链表好?
- 顺序表和链表的逻辑结构都是线性结构,都属于线性表。
- 但是二者的存储结构不同,顺序表采用顺序存储…(特点,带来的优点缺点);链表采用链式存储…(特点、导致的优缺点)。
- 由于采用不同的存储方式实现,因此基本操作的实现效率也不同。
- 当初始化时…;当插入一个数据元素时…;当删除一个数据元素时…;当查找一个数据元素时…
第三章:栈和队列
栈
基本概念
栈的定义:
- 栈(Stack)是只允许在一端进行插入或删除操作的线性表
- 逻辑结构:与普通线性表相同
- 数据的运算:插入、删除操作有区别
- 栈顶:允许插入和删除的一端,对应元素被称为栈顶元素
- 栈底:不允许插入和删除的一端,对应元素被称为栈底元素
- 特点:后进先出
Last In First Out(LIFO)
栈的基本操作:
InitStack(&S):初始化栈。构造一个空栈S,分配内存空间。
DestroyStack(&S):销毁栈。销毁并释放栈S所占用的内存空间。
Push(&S,x):进栈,若栈S未满,则将x加入使之成为新栈顶。
Pop(&S,&x):出栈,若栈S非空,则弹出栈顶元素,并用x返回。
GetTop(S, &x):读栈顶元素。若栈S非空,则用x返回栈顶元素
StackEmpty(S):判断栈空。若S为空,则返回true,否则返回false。
出栈顺序数量:
n个不同元素进栈,出栈元素不同排列的个数为:\(\frac{1}{\mathrm{n}+1} C_{2 n}^{n}\)(卡特兰(Catalan)数)
栈的存储结构
| 顺序栈的定义和初始化 | 链栈的定义和初始化 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
注意:也可以让栈顶指针top先指向0,每次进栈S.top++,出栈--S.top
共享栈:
- 使用静态数组要求提前规定好栈的大小,容易造成内存资源的浪费因此共享栈应运而生
- 两个栈共享同一片空间,
0、1号栈朝着同一方向进栈 - 栈满的条件:
top0 + 1 == top1

栈的链式存储结构
栈的链式存储实质
- 进栈:头插法建立单链表,也就是对头结点的后插操作
- 出栈:单链表的删除操作,对头结点的“后删”操作
- 推荐使用不带头结点的链栈
- 创销增删查的操作参考链表
| 顺序栈 | 链栈 |
|---|---|
![]() |
![]() |
队列
队列的基本概念
队列的定义:
- 栈(Stack)是只允许在一端进行插入或删除操作的操作受限的线性表
- 队列(Queue)是只允许在一端进行插入,在另一端删除的线性表
- 队头:允许删除的一端,对应的元素被称为队头元素
- 队尾:允许插入的一端,对应的元素被称为队尾元素
- 队列的特点:先进先出
First In First Out(FIFO)
队列的基本操作
InitQueue(&Q):初始化队列,构造一个空队列Q。
DestroyQueue(&Q):销毁队列。销毁并释放队列Q所占用的内存空间。
EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回。
GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x。
QueueEmpty(Q):判队列空,若队列Q为空返回true,否则返回false。
队列的顺序存储结构
队列&循环队列的定义和初始化
| 队列的定义和初始化循环 | 循环队列的定义和初始化 |
|---|---|
![]() |
![]() |
![]() |
![]() |

入队操作
-
通过取余操作,只要队列不满,就可以一直利用之前已经出队了的空间,逻辑上实现了循环队列的操作
-
于是,队列已满的条件:队尾指针的再下一个位置是队头,即
(Q.rear+1)%MaxSize==Q.front; -
代价:牺牲了一个存储单元,因为如果
rear和front相同,与判空的条件相同了
判断队列已满/已空
| 判断队列已满/已空 |
|---|
![]() |
![]() |
![]() |
队列的链式存储结构
| 队列的链式存储结构(带头结点) | 队列的链式存储结构(不带头结点) |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
队列满的条件
- 顺序存储:预分配的空间耗尽时队满
- 链式存储:一般不会队满,除非内存不足
- 因此一般不用考虑队满
双端队列
定义
- 双端队列:只允许从两端插入、两端删除的线性表
- 输入受限的双端队列:只允许从一端插入、两端删除的线性表
- 输出受限的双端队列:只允许从两端插入、一端删除的线性表
- 不管是怎么样的双端队列实际都是栈和队列的变种
考点
- 判断输出序列合法性
- 在栈中合法的输出序列,在双端队列中必定合法
栈在括号匹配中的应用
括号匹配问题
- 若有括号无法被匹配则出现编译错误
- 遇到左括号就入栈
- 遇到右括号,就“消耗”一个左括号

代码实现

栈在表达式求值中的应用
算数表达式
-
由三个部分组成:操作数、运算符、界限符
-
我们平时写的算术表达式都是中缀表达式
-
如何可以不用界限符也能无歧义地表达运算顺序
-
Reverse Polish notation(逆波兰表达式=后缀表达式)
-
Polish notation(波兰表达式=前缀表达式)
中缀、后缀、前缀表达式

中缀转后缀的方法
| 中缀转后缀的方法(手算) | 中缀表达式转后缀表达式(机算用栈实现) |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
中缀转后缀的方法(手算)
- 确定中缀表达式中各个运算符的运算顺序
- 选择下一个运算符,按照「左操作数右操作数运算符」的方式组合成一个新的操作数
- 如果还有运算符没被处理,就继续第二步
- 注意:运算顺序不唯一,因此对应的后缀表达式也不唯一
- “左优先”原则:只要左边的运算符能先计算,就优先算左边的,保证手算和机算是一致的
中缀表达式转后缀表达式(机算,用栈实现)
- 初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
- 从左到右处理各个元素,直到末尾。可能遇到三种情况:
- 遇到操作数。直接加入后缀表达式。
- 遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到弹出“(”为止。注意:“(”不加入后缀表达式。
- 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,若碰到“(”或栈空则停止。之后再把当前运算符入栈。
- 按上述方法处理完所有字符后,将栈中剩余运算符依次弹出,并加入后缀表达式。
后缀表达式的计算
| 后缀表达式的计算(手算 | 后缀表达式的计算(机算,用栈实现) |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
后缀表达式的计算(手算)
-
从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算,合体为一个操作数
-
注意:两个操作数的左右顺序
-
特点:最后出现的操作数先被运算,LIFO(后进先出),可以使用栈来完成这个步骤
-
“左优先”原则:只要左边的运算符能先计算,就优先算左边的
后缀表达式的计算(机算,用栈实现)
-
从左往右扫描下一个元素,直到处理完所有元素
-
若扫描到操作数则压入栈,并回到第一步;否则执行第三步
-
若扫描到运算符,则弹出两个栈顶元素,执行相应运算,运算结果压回栈顶,回到第一步
-
注意:先出栈的是“右操作数”
-
若表达式合法,则最后栈中只会留下一个元素,就是最终结果
-
后缀表达式适用于基于栈的编程语言(stack-orientedprogramming language),如:Forth、PostScript)(了解)
中缀转前缀的方法
| 中缀表达式转前缀表达式(手算) | 中缀表达式的计算(机算,用栈实现) |
|---|---|
![]() |
![]() |
![]() |
![]() |
中缀表达式转前缀表达式(手算)
- 确定中缀表达式中各个运算符的运算顺序
- 选择下一个运算符,按照「运算符左操作数右操作数」的方式组合成一个新的操作数
- 如果还有运算符没被处理,就继续第二步
- “右优先”原则:只要右边的运算符能先计算,就优先算右边的
中缀表达式的计算(机算,用栈实现)
- 中缀表达式的计算=中缀转后缀+后缀表达式求值,两个算法的结合
- 用栈实现中缀表达式的计算:
- 初始化两个栈,操作数栈和运算符栈
- 若扫描到操作数,压入操作数栈
- 若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算,运算结果再压回操作数栈)
栈与队列的应用
栈的应用 |
队列的应用 |
|---|---|
| 递归中的应用 | 树的层次遍历 图的广度优先遍历 操作系统中的应用 |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
特殊矩阵的压缩储存
一维数组的存储结构:
-
起始地址:
LOC -
各数组元素大小相同,且物理上连续存放。
-
数组元素
a[i]的存放地址=LOC + i * sizeof(ElemType)
二维数组的存储结构:
-
分为行优先和列优先,本质就是把二维的逻辑视角转换为内存中的一维储存
-
M行N列的二维数组b[M][N]中,若按行优先存储,则b[i][j]的存储地址=
LOC + (i*N + j) * sizeof(ElemType) -
M行N列的二维数组b[M][N]中,若按列优先存储,则b[i][j]的存储地址=
LOC + ( j*M+ i ) * sizeof(ElemType) -
二维数组也有随机存储的特性
普通矩阵的存储:
-
可用二维数组存储
-
注意:描述矩阵元素时,行、列号通常从1开始;而描述数组时通常下标从0开始
-
某些特殊矩阵可以压缩存储空间(比如对称矩阵)
对称矩阵的压缩存储:
-
若n阶方阵中任意一个元素ai,j都有ai,j = aj,i则该矩阵为对称矩阵
-
普通存储:n*n二维数组
-
压缩存储策略:只存储主对角线+下三角区(或主对角线+上三角区),按行优先原则将各元素存入一维数组中
-
数组大小应为多少:(1+n)*n/2
-
站在程序员的角度,对称矩阵压缩存储后怎样才能方便使用:可以实现一个“映射”函数矩阵下标->一维数组下标
-
按行优先的原则,ai,j是第几个元素:
\[k=\left\{\begin{array}{ll} \frac{i(i-1)}{2}+j-1, & i\geq j(\text { 下三角区和主对角线元素) } \\ \frac{j(j-1)}{2}+i-1, & i<j\left(\text { 上三角区元素 } a_{i j}=a_{j i}\right) \end{array}\right. \]
三角矩阵的压缩存储:
-
下三角矩阵:除了主对角线和下三角区,其余的元素都相同
-
上三角矩阵:除了主对角线和上三角区,其余的元素都相同
-
压缩存储策略:按行优先原则将橙色区元素存入一维数组中,并在最后一个位置存储常量c
-
下三角矩阵,按行优先的原则,ai,j是第几个元素:
\[k=\left\{\begin{array}{ll} \frac{i(i-1)}{2}+j-1, & i \geqslant j \text { (下三角区和主对角线元素) } \\ \frac{n(n+1)}{2}, & i<j \text { (上三角区元素) } \end{array}\right. \] -
上三角矩阵,按行优先的原则,ai,j是第几个元素:
\[k=\left\{\begin{array}{ll} \frac{(i-1)(2 n-i+2)}{2}+(j-i), & i \leqslant j(\text { 上三角区和主对角线元素) } \\ \frac{n(n+1)}{2}, & i>j(\text { 下三角区元素) } \end{array}\right. \]
三对角矩阵的压缩存储:
-
三对角矩阵,又称带状矩阵:当|i - j|>1时,有ai,j = 0 (1≤ i, j ≤n)
-
压缩存储策略:按行优先(或列优先)原则,只存储带状部分
-
按行优先的原则,ai,j是第几个元素:
三对角矩阵 

稀疏矩阵的压缩存储:
-
稀疏矩阵:非零元素远远少于矩阵元素的个数
-
压缩存储策略1:顺序存储——三元组\(<i(行),j(列),v(值)>\),失去了数组随机存储的特性
-
压缩存储策略2:链式存储,十字链表法

第四章:串
串的定义
串也叫做字符串,是由零个活着多个字符组成的有限序列
如果字符串的长度为0,那么这个字符串也叫做空串
有的编程语言使用单引号,也有的使用双引号
- 字串
串中任意个连续的字符组成的子序列 - 主串
包含字串的串 - 字符在主串中的位置
字符在串中的符号 - 主串在自串中的位置
子串的第一个字符在主串中的位置
注意位序从1开始而不是从0开始
串也是一种特殊的线性表,数据元素之间呈线性关系;串的基本操作,如增删改查等通常以子串为操作对象
串的基本操作
StrAssign(&T,chars):赋值操作,把串T赋给chars
StrCopy(&T,S);赋值操作。由串s复制得到串T
StrEmpty(S):判空操作。若s为空串,则返回true,否则返回false
StrLength(S):求串长,返回串S地元素个数
ClearString(&s):清空操作,将S清为空串
DestroyString(&S):销毁串,将串S销毁(回收存储空间)
Contact(&T,S1,S2):串链接,用T返回由S1和S2联结而成的新串
SubString(&Sub,S,pos,len):求子串。用Sub返回串S的第pos个字符起长度为len的子串
Index(S,T):定位操作。若主串S中存在与串T值相同的子串,则返回它在主串S中第一次出现的位置,否则函数值为0
StrCompare(S,T):比较操作,若S>T,则返回值>0;若S=T,则返回值=0;若S<T,返回值<0
第五章:树与二叉树
树&二叉树基本概念
| 树基本概念 | 二叉树基本概念 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
相关定义
祖先结点:从一个结点往上一直走到的根结点叫做祖先结点
子孙结点:从一个结点出发,下面的结点都是子孙结点
双亲结点(父结点):一个结点的直接前驱结点
孩子结点:一个结点的直接后继结点
兄弟结点:同属于一个父结点的结点
堂兄弟结点:(一般来说不同属于一个父结点但是)同一层的结点
两结点的路径:只能同下往上(或者从上往下)的线段个数,注意,如果需要拐弯的情况则称没有路径
路径长度:经过几条边
结点的层次(深度):从上往下数的层数
结点的高度:从下往上数(越往上高度越高)
树的深度:整个数的层数
结点的度:对于这个结点总共有几个孩子(分支),度为0的结点也就是叶子结点
数的度:各个结点的度的最大值
注意深度和高度、层次一般从1开始,除非题意要求从0开始
有序树和无序树
- 有序树:树中结点各子树从左往右是
有次序的,不能交换 - 无序树:树中结点各子树从左往右是
没有次序的,可以交换
森林
表示有n棵互不相交的树组成的集合
森林和树可以互相表示
树的性质
结点数=总度数+1(这里的总度数表示每个结点度的和)
度为m的树和m叉树的区别
| 度为m的树 | m叉树 |
|---|---|
| 任意结点的度≤m(最多m个孩子) | 任意结点的度≤m(最多m个孩子) |
| 至少有一个结点的度=m(有m个孩子) | 允许所有的结点的度都<m |
| 一定是非空树,至少有m+1个结点 | 可以是空树 |
度为m的树第i层至多有mi-1个结点
高度为h的m叉树至多有\(\frac{m^h-1}{m-1}\) 个结点(使用等比数列计算得到)
高度为h,的m叉树至少有h个结点(每层只有一个结点)
高度为h,度为m的树至少有h+m-1个结点(只有一层有m个结点,其它层均为1个结点)
具有n个结点的m叉树最小高度为\(⌈log_m(n(m-1)+1)⌉\)
二叉树的性质
| 二叉树的性质 | 完全二叉树的性质 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
几种特殊的二叉树
| 满二叉树 | 完全二叉树 |
|---|---|
![]() |
![]() |
| 二叉排序树 | 平衡二叉树 |
|---|---|
![]() |
![]() |
树与二叉树概念总结
| 树 | 二叉树 |
|---|---|
![]() |
![]() |
二叉树存储结构
| 二叉树顺序存储 | 二叉树链式存储 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
二叉树的先中后序遍历
树的遍历
遍历:按照某种次序把所有结点都访问一遍
层序遍历:基于树的层次特性确定的次序规则
先/中/后序遍历:基于树的递归特性确定的次序规则
二叉树的遍历
二叉树的递归特性:
-
要么是个空二叉树
-
要么就是由“根结点+左子树+右子树”组成的二叉树
先序遍历:根左右(NLR)
中序遍历:左根右(LNR)
后序遍历:左右根(LRN)


中序遍历

后序遍历

二叉树遍历总结
-
空间复杂度为\(O(h)\),\(h\)为树的高度
-
每个结点都会被路过3次
求树的深度
-
是后序遍历的变种
-
先后访问左右儿子,得出对应深度返回左右儿子深度更高的那个就是树的深度
二叉树的层序遍历
| 算法思想 | 代码实现 |
|---|---|
![]() |
![]() |
由遍历序列构造二叉树
结论:前序、后序、层序序列的两两组合无法唯一确定一颗二叉树(必须与中序结合才能确定一颗二叉树)
通过两种遍历序列确定二叉树:
前序+中序

中序+后序

层序+中序

线索二叉树的概念
中序遍历的问题
如何找到指定结点p在q 中序遍历序列中的前驱?
如何找到p的中序后继?
能否从一个指定结点开始中序遍历?
完成上述需求的思路:
-
从根结点出发,重新进行一次中序遍历,指针q记录当前访问的结点,指针pre记录上一个被访问的结点
-
当q == p时,pre为前驱
-
当pre == p时,q为后继
缺点:找前驱、后继很不方便; 操作必须从根开始
中序线索二叉树
线索二叉树的存储结构

先/中/后序线索二叉树同理
三种线索二叉树的对比

二叉树的线索化
通过头结点找到中序前驱
中序二叉树线索化
| 中序二叉树线索化 | 中序二叉树线索化 |
|---|---|
![]() |
![]() |
先序二叉树线索化
| 先序二叉树线索化 | 先序二叉树线索化 |
|---|---|
![]() |
![]() |
- 由于先序遍历先遍历根结点然后再遍历左结点,若左孩子为空,通过线索化后会指回前驱结点(他的根结点)
- 这时在此访问左孩子时候会又访问回根结点,因此需要增加一个判断来确定左孩子不是真正的左孩子而是线索化后的前驱结点
后序二叉树线索化


在线索二叉树中找前驱后驱
中序线索二叉树找中序前驱后继
| 中序线索二叉树找中序前驱 | 中序线索二叉树找中序后继 |
|---|---|
![]() |
![]() |
![]() |
![]() |
先序线索二叉树找先序前驱后继
| 先序线索二叉树找先序前驱 | 先序线索二叉树找先序后继 |
|---|---|
![]() |
![]() |

在先序线索二叉树中找到指定结点*p的先序后继next
-
若p->rtag == 1,则next = p->rchild
-
若p->rtag == 0,说明p必定有右孩子
-
若p有左孩子,则先序后继为左孩子
-
若p没有左孩子,则先序后继为右孩子
-
在先序线索二叉树中找到指定结点*p的先序前驱pre
-
若p->ltag == 1,则pre = p->lchild
-
若p->ltag == 0,说明p必定有左孩子
-
先序遍历中,左右子树中的结点只可能是根的后继,不可能是前驱
-
方法1:用土办法从头开始先序遍历
-
方法2:可以改用三叉链表以找到父结点
-
后序线索二叉树找后序前驱后继
| 后序线索二叉树找后序前驱 | 后序线索二叉树找后序后继 |
|---|---|
![]() |
![]() |

在后序线索二叉树中找到指定结点*p的后序前驱pre
-
若p->ltag == 1,则pre = p->lchild
-
若p->ltag == 0,说明p必有左孩子
-
若p有右孩子,则后序前驱为右孩子
-
若p没有右孩子,则后序前驱为左孩子
-
在后序线索二叉树中找到指定结点*p的后序后继next
-
若p->rtag == 1,则next = p->rchild
-
若p->rtag == 0,说明p必定有右孩子
-
后序遍历中,左右子树中的结点只可能是根的前驱,不可能是后继
-
方法1:用土办法从头开始先序遍历
-
方法2:可以改用三叉链表以找到父结点
-

树的储存结构
双亲表示法(顺序存储)
每个结点中保存指向双亲的“指针”,data,parrent
根结点固定存储在0,-1表示没有双亲
新增数据元素,无需按逻辑上的次序存储,只需说明新增元素的data,parrent即可
删除数据元素
-
方案1:把要删除的数据元素data设为空,parent设为-1
-
方案2:将数组尾部的数据元素覆盖要删除的数据元素
查询数据元素
-
优点:查指定结点的双亲很方便
-
缺点:查指定结点的孩子只能从头遍历
孩子表示法(顺序+链式存储)
优点:找孩子很方便
缺点:找双亲(父节点) 不方便,只能遍历每个链表 适用于“找孩子”多,“找父亲”少的应用场景。
如:服务流程树
| 双亲表示法(顺序存储) | 孩子表示法(顺序+链式存储) |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
孩子兄弟表示法(链式存储)★★★

规则:
- 左指针指向第一个孩子
- 右指针指向自己的第一个兄弟
森林和二叉树的转换
本质:用二叉链表存储森林
规则:
- 各个树的根结点视为兄弟关系
左指针指向第一个孩子
- 右指针指向自己的第一个兄弟


树和森林的遍历
树的先根遍历
-
先根遍历:若树非空,先访问根结点,再依次对每棵子树进行先根遍历。
-
树的先根遍历序列与这棵树相应二叉树的先序序列相同。

树的后根遍历
-
后根遍历:若树非空,先依次对每棵子树进行后根遍历,最后再访问根结点
-
树的后根遍历序列与这棵树相应二叉树的中序序列相同
-
也被称为深度优先遍历

树的层次遍历
用队列实现,又被称为广度优先遍历
步骤
- 若树非空,则根结点入队
若队列非空,队头元素出队并访问,同时将该元素的孩子依次入队
- 重复第二步直到队列为空

森林的先序遍历:
若森林为非空,则按如下规则进行遍历:
-
访问森林中第一棵树的根结点。
-
先序遍历第一棵树中根结点的子树森林。
-
先序遍历除去第一棵树之后剩余的树构成的森林。
效果等同于依次对各个树进行先根遍历
用孩子兄弟表示法转换为二叉树,效果等同于依次对二叉树的先序遍历
| 森林的先序遍历 | |
|---|---|
![]() |
![]() |
森林的中序遍历:
若森林为非空,则按如下规则进行遍历:
-
中序遍历森林中第一棵树的根结点的子树森林。
-
访问第一棵树的根结点。
-
中序遍历除去第一棵树之后剩余的树构成的森林。
效果等同于依次对各个树进行后根遍历
用孩子兄弟表示法转换为二叉树,效果等同于依次对二叉树的中序遍历
| 森林的中序遍历 | |
|---|---|
![]() |
![]() |
哈夫曼树
带权路径长度:
-
结点的权:有某种现实含义的数值(如:表示结点的重要性等)
-
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
-
树的带权路径长度:树中所有叶结点的带权路径长度之和(WPL, Weighted Path Length)
哈夫曼树的定义:
在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树

哈夫曼树的构造:

哈夫曼编码:


并查集

定义:
并查集(Disjoint Set)是逻辑结构集合的一种具体实现,只进行“并”和“查”两种基本操作
并查集的基本操作:

初始化:

S[]实际上就是树的双亲表示法,里面的值就是自己对应根结点的下标
并、查:
时间复杂度分析:
-
Find操作最坏时间复杂度O(n)
-
Union操作时间复杂度O(1)
Union操作的优化:
-
在每次Union操作构建树的时候,尽量让树不长高
-
用根结点的绝对值表示树的结点总数(根结点从-1改成-(树的总结点))
-
Union操纵,让小树合并到大树
-
该方法构造的树高不超过\([log_2n]+1\)
-
Find最坏时间复杂度变为\(O(log_2n)\)


第六章:图
图的基本概念
图的定义
- 图
G由顶点集V和边集E组成,记为\(G = (V, E)\),其中\(V(G)\)表示图G中顶点的有限非空集; - \(E(G)\)表示图G中顶点之间的关系(边)集合。
- 若\(V =\) {\(v_1, v_2, … , v_n\)},则用\(|V|\)表示图G中顶点的个数,也称
图G的阶,$E = \({\)(u, v) | u∈V, v∈V\(},用\)|E|$表示图G中边的条数。 - 注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集

无向图、有向图

简单图、多重图

顶点的度、入度、出度

顶点-顶点的关系描述
| 顶点之间有可能不存在路径 | 有向图的路径也是有向的 |
|---|---|
![]() |
![]() |
-
路径:顶点vp到顶点vp之间的一条路径是指顶点序列,\(v_{p}, v_{i_{1}}, v_{i_{2}}, \cdots, v_{i_{m}}, v_{q}\)
-
回路:第一个顶点和最后一个顶点相同的路径称为回路或环
-
简单路径:在路径序列中,顶点不重复出现的路径称为简单路径。
-
简单回路:除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
-
路径长度:路径上边的数目
-
点到点的距离:从顶点
u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。若从u到v根本不存在路径,则记该距离为无穷\((∞)\)。 -
无向图中,若从顶点v到顶点w有路径存在,则称v和w是
连通的 -
有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是
强连通的
连通图、强连通图

研究图的局部——子图
| 无向图 | 有向图 |
|---|---|
![]() |
![]() |
连通分量

无向图中的极大联通子图称为连通分量- 子图必须连通,且包含尽可能多的顶点和边
强连通分量

-
有向图中的极大强联通子图称为有向图的强连通分量 -
子图必须强连通,同时保留尽可能多的边
生成树

-
连通图的生成树是包含图中全部顶点的一个极小连通子图
-
边尽可能的少,但要保持连通
-
若图中顶点数为n,则它的生成树含有n-1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路。
生成森林

在非连通图中,连通分量的生成树构成了非连通图的生成森林。
边的权、带权图/网

-
边的权:在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。 -
带权图/网:边上带有权值的图称为带权图,也称网。 -
带权路径长度:当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度
几种特殊形态的图
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
-
无向完全图:无向图中任意两个顶点之间都存在边
-
有向完全图:有向图中任意两个顶点之间都存在方向相反的两条弧
-
边数很少的图称为稀疏图反之称为稠密图
-
没有绝对的界限,一般来说\(|E| < |V|log|V|\)时,可以将G视为稀疏图
-
树:不存在回路,且连通的无向图
-
有向树:一个顶点的入度为0、其余顶点的入度均为1的有向图,称为有向树
- n个顶点的树,必有n-1条边。
-
n个顶点的图,若\(|E|>n-1\),则一定有回路
图的概念总结
| 总结 | 常考内容 |
|---|---|
![]() |
![]() |
图的存储结构
邻接矩阵法
定义
-
无向图:
- 第i个结点的
度=第i行(或第i列)的非零元素个数
- 第i个结点的
-
有向图:
-
第i个结点的
出度=第i行的非零元素个数 -
第i个结点的
入度=第i列的非零元素个数
-
-
第i个结点的
度=第i行、第i列的非零元素个数之和 -
邻接矩阵法
求顶点的度、出度、入度的时间复杂度为\(O(|V|)\)
邻接矩阵法存储带权图(网)

邻接矩阵法的性能分析
-
空间复杂度:\(O(|V|^2)\) ——只和顶点数相关,和实际的边数无关
-
适合用于存储稠密图
-
无向图的邻接矩阵是对称矩阵,可以压缩存储(只存储上三角区/下三角区)
邻接矩阵法的性质
| 邻接矩阵法的性质 | 邻接矩阵法的性质 |
|---|---|
![]() |
![]() |
邻接矩阵法要点回顾
• 如何计算指定顶点的度、入度、出度(分无向图、有向图来考虑)?时间复杂度如何?
• 如何找到与顶点相邻的边(入边、出边)?时间复杂度如何?
• 如何存储带权图?
• 空间复杂度——\(O(|V|^2)\),适合存储稠密图
• 无向图的邻接矩阵为对称矩阵,如何压缩存储?
• 设图G的邻接矩阵为A(矩阵元素为0/1),则An的元素An[i][j]等于由顶点i到顶点j的长度为n
的路径的数目
邻接表法
为什么要使用邻接表
-
邻接矩阵是使用数组实现的顺序存储,空间复杂度高,不适合存储稀疏图
-
邻接表是顺序+链式存储
定义:

-
其实这和树的孩子表示法是类似的,孩子表示法:顺序存储各个结点,每个结点中保存孩子链表头指针
-
边结点的数量是\(2|E|\),整体空间复杂度为\(O(|V| + 2|E|)\)
-
边结点的数量是\(|E|\),整体空间复杂度为\(O(|V| + |E|)\)
-
只要确定了顶点编号,图的邻接矩阵表示方式唯一,图的邻接表表示方式并不唯一
邻接矩阵和邻接表的对比

| 邻接表 | 邻接矩阵 | |
|---|---|---|
| 空间复杂度 | 无向图\(O(|V|+2|E|)\);有向图\(O(|V| +|E|)\) | \(O(|V|^2)\) |
| 适合用于 | 存储稀疏图 | 存储稠密图 |
| 表示方式 | 不唯一 | 唯一 |
| 计算度/出度/入度 | 计算有向图的度、入度不方便,其余很方便 |
必须遍历对应行或列 |
| 找相邻的边 | 找有向图的入边不方便,其余很方便 |
必须遍历对应行或列 |
十字链表、邻接多重表
关系:
-
十字链表储存有向图
-
邻接多重表储存无向图
十字链表存储有向图:

-
空间复杂度:\(O(|V|+|E|)\)
-
如何找到指定顶点的所有出边?顺着绿色线路找
-
如何找到指定顶点的所有入边?顺着橙色线路找
邻接矩阵、邻接表存储有向图无向图
| 存储有向图 | 存储无向图 |
|---|---|
![]() |
![]() |
邻接多重表储存无向图

总结
| 邻接矩阵 | 邻接表 | 十字链表 | 邻接多重表 | |
|---|---|---|---|---|
| 空间复杂度 | \(O(|V|^2)\) | 无向图\(O(|V|+2|E|)\) 有向图\(O(|V| +|E|)\) | \(O(|V|+|E|)\) | \(O(|V|+|E|)\) |
| 找相邻边 | 遍历对应行或列 时间复杂度为0(|V|) | 找有向图的入边必须遍 历整个邻接表 | 很方便 | 很方便 |
| 删除边或顶点 | 删除边很方便,删除顶 点需要大量移动数据 | 无向图中删除边或顶点 都不方便 | 很方便 | 很方便 |
| 适用于 | 稠密图 | 稀疏图和其他 | 只能存有向图 | 只能存无向图 |
| 表示方式 | 唯一 | 不唯一 | 不唯一 | 不唯一 |
图的基本操作
-
Adjacent(G,x,y):判断图G是否存在边<x, y>或(x, y)。
-
Neighbors(G,x):列出图G中与结点x邻接的边。
-
InsertVertex(G,x):在图G中插入顶点x。
-
DeleteVertex(G,x):从图G中删除顶点x。
-
AddEdge(G,x,y):若无向边(x, y)或有向边<x, y>不存在,则向图G中添加该边。
-
RemoveEdge(G,x,y):若无向边(x, y)或有向边<x, y>存在,则从图G中删除该边。
-
FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。 -
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。 -
Get_edge_value(G,x,y):获取图G中边(x, y)或<x, y>对应的权值。
-
Set_edge_value(G,x,y,v):设置图G中边(x, y)或<x, y>对应的权值为v。
-
注意:上面的操作都只针对邻接矩阵和邻接表
图的广度优先遍历(BFS)
树和图的广度优先遍历:
-
树的广度优先遍历:通过根结点,可以找到下一层的结点2,3,4.通过234又可以找到再下一层的结点5678
-
若树非空,则根结点入队
-
若队列非空,队头元素出队并访问,同时将该元素的孩字依次入队
-
重复第二步直到队列为空
-
-
图的
广度优先遍历类似于树的广度优先遍历(层序遍历) -
区别:
-
树不存在“回路”,搜索相邻的结点时,不可能搜到已经访问过的结点
-
图搜索相邻的顶点时,有可能搜到已经访问过的顶点
-
代码实现:
-
找到与一个顶点相邻的所有顶点
-
FirstNeighbor(G,x):求图G中顶点x的第一个邻接点,若有则返回顶点号。若x没有邻接点或图中不存在x,则返回-1。 -
NextNeighbor(G,x,y):假设图G中顶点y是顶点x的一个邻接点,返回除y之外顶点x的下一个邻接点的顶点号,若y是x的最后一个邻接点,则返回-1。 -
使用上面两个基本操作
-
-
标记哪些顶点被访问过
-

-
都初始化为false
-
-
需要一个辅助队列
| 顶点被访问 | 辅助队列 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
遍历序列的可变性:

从
顶点1出发得到的⼴度优先遍历序列:
\(1,2,5,6,3,7,4,8\)
从
顶点3出发得到的⼴度优先遍历序列:
\(3,4,6,7,8,2,1,5\)
算法存在的问题和解决方案:
如果是非连通图,则无法遍历完所有结点
| 算法存在的问题 | 解决方案 |
|---|---|
![]() |
![]() |
| 复杂度分析 | |
|---|---|
![]() |
![]() |
广度优先生成树&森林:
| 广度优先生成树 | 广度优先生成森林 |
|---|---|
![]() |
![]() |
-
广度优先生成树由广度优先遍历过程确定。
-
由于邻接表的表示方式不唯一,因此基于邻接表的广度优先生成树也不唯一
-
对非连通图的广度优先遍历,可得到广度优先生成森林

图的深度优先遍历(DFS)
树和图的深度优先遍历:
-
树的深度优先遍历(先根、后根)
-
从根结点出发,能往更深处走就尽量往深处走。
-
每当访问一个结点的时候,要检查是否还有与当前结点相邻的且没有被访问过的结点,如果有的话就往下一层钻。
-
-
图的深度优先遍历类似于树的先根遍历。
-
图的深度优先遍历是递归实现的,广度优先办理是队列实现的
图的深度优先遍历:

算法存在的问题和解决方案:
如果是非连通图,则无法遍历完所有结点

复杂度分析:
| 空间复杂度 | 时间复杂度 |
|---|---|
![]() |
![]() |
深度优先序列:

-
和图的邻接表是一个原理,树中各个孩子结点在邻接表中出现的顺序是可变的
-
因此如果采用这种数据结构存储树,那么可能会有不同的遍历序列
深度优先生成树:
- 同一个图的
邻接矩阵表示方式唯一,因此深度优先遍历序列唯一,深度优先生成树也唯一 - 同一个图
邻接表表示方式不唯一,因此深度优先遍历序列不唯一,深度优先生成树也不唯一 - 对无向图进行BFS/DFS遍历
![]() |
![]() |
图的遍历与图的连通性:
-
调用BFS/DFS函数的次数=连通分量数
-
对于
连通图,只需调用1次BFS/DFS -
对
有向图进行BFS/DFS遍历 -
调用BFS/DFS函数的次数要具体问题具体分析,若起始顶点到其他各顶点都有路径,则只需调用1次BFS/DFS函数
-
对于
强连通图,从任一结点出发都只需调用1次BFS/DFS

最小生成树
生成树:

- 连通图的
生成树是包含图中全部顶点的一个极小连通子图。 - 若图中顶点数为
n,则它的生成树含有n-1条边。对生成树而言,若砍去它的一条边,则会变成非连通图,若加上一条边则会形成一个回路
| 广度优先生成树 | 深度优先生成树 | |
|---|---|---|
![]() |
![]() |
![]() |
最小生成树(最小代价树):
- 对于一个带权连通无向图G = (V, E),生成树不同,每棵树的权(即树中所有边上的权值之和)也可能不同
- 设R为G的所有生成树的集合,若T为R中边的权值之和最小的生成树,则T称为G的最小生成树(Minimum-Spanning-Tree, MST)
- 最小生成树可能有多个,但边的权值之和总是唯一且最小的
- 最小生成树的边数 = 顶点数 - 1
- 砍掉一条则不连通,增加一条边则会出现回路
- 如果一个连通图本身就是一棵树,则其最小生成树就是它本身
- 只有连通图才有生成树,非连通图只有生成森林

Prim 算法(普里姆)
算法思想:
-
从某一个顶点开始构建生成树;
-
每次将代价最小的新顶点纳入生成树,直到所有顶点都纳入为止。
-
时间复杂度:\(O(|V|^2)\),适合用于边稠密图
数据结构
| Prim 算法 | (普里姆) | |
|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
实现步骤:
-
从\(V_0\)开始,总共需要
n-1轮处理 -
循环遍历所有个结点,找到
lowCost最低的,且还没加入树的顶点,isJoin对应结点标记为true -
再次循环遍历,更新还没加入的各个顶点的
lowCost值 -
重复上面步骤,直到所有结点都加入树,生成的树即为最小生成树
Kruskal 算法(克鲁斯卡尔)
算法思想:
-
每次选择一条权值最小的边,使这条边的两头连通(原本已经连通的就不选)
-
直到所有结点都连通
-
时间复杂度:\(O( |E|log_2|E| )\),适合用于边稀疏图
数据结构
| Kruskal 算法 | (克鲁斯卡尔) | |
|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
让各条边按照权值顺序排序
实现步骤:
-
共执行 e 轮,每轮判断两个顶点是否属于同一集合
-
检查第e条边的两个顶点是否连通(是否属于同一个集合)
-
若不联通则连起来
-
若联通则不操作
-
重复上面的步骤直到所有边都被遍历过
最短路径问题之Dijkstra算法
Dijkstra简介

BFS算法的局限性

算法思想
| 实现 | 初始:从\(V_0\)开始,初始化三个数组信息 |
|---|---|
![]() |
![]() |
| 实现 | 第1轮:循环遍历所有结点,找到还没确定最短路径,且dist 最小的顶点\(V_i\),令final[i]=ture |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
| … | … |
![]() |
![]() |
如何使用数组信息
\(V0\)到\(V2\)的最短(带权)路径长度为:dist[2] = 9
通过path[]可知,\(V0\)到\(V2\)的最短(带权)路径:
\(V2<-- V1<-- V4<--V0\)
实现流程
- 初始:从V0开始,初始化三个数组信息
- 循环遍历所有结点,找到还没确定最短路径,且dist 最小的顶点Vi,令final[i]=ture。
- 检查所有邻接自 Vi 的顶点,若其 final 值为false,则更新 dist 和 path 信息
- 重复上述步骤,知道所有结点的final都标记为true
代码实现思路:

用于负权值带权图

最短路径问题之Floyd算法
Robert W. Floyd简介

算法思想
-
Floyd算法:求出每一对顶点之间的最短路径
-
使用动态规划思想,将问题的求解分为多个阶段
-
对于n个顶点的图G,求任意一对顶点 \(Vi —> Vj\) 之间的最短路径可分为如下几个阶段:
-
初始:不允许在其他顶点中转,最短路径是?
-
0:若允许在 \(V_0\) 中转,最短路径是? -
1:若允许在 \(V_0\)、\(V_1\) 中转,最短路径是? -
2:若允许在 \(V_0\)、\(V_1\)、\(V_2\) 中转,最短路径是? -
…
-
n-1:若允许在 \(V_0\)、\(V_1\)、\(V_2\) …… \(V_{n-1}\) 中转,最短路径是?
-
实现步骤
| 步骤 | 实现 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
代码实现

注意点
- Floyd算法可以用于负权图
- Floyd 算法
不能解决带有“负权回路”的图(有负权值的边组成回路),这种图有可能没有最短路径(每走一圈路径越小)

最短路径问题总结

有向无环图(DAG图)的描述表达式
定义

若一个有向图中不存在环,则称为有向无环图,简称DAG图(Directed Acyclic Graph)
例子
| 转换前 | 转换后 |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
解题方法
- 把各个操作数不重复地排成一排
- 标出各个运算符的生效顺序(先后顺序有点出入无所谓)
- 按顺序加入运算符,注意“分层”
- 从底向上逐层检查同层的运算符是否可以合体
拓扑排序
AOV网
AOV网(Activity On Vertex NetWork,用顶点表示活动的网)
用DAG图(有向无环图)表示一个工程。顶点表示活动,有向边\(<V_i, V_j>\)表示活动Vi必须先于活动\(V_j\)进行
拓扑排序
拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
-
每个顶点出现且只出现一次。
-
若顶点A在序列中排在顶点B的前面,则在图中不存在从顶点B到顶点A的路径。
或定义为:
- 拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点A到顶点B的路径,则在排序中顶点B出现在顶点A的后面。
每个AOV网都有一个或多个拓扑排序序列。
拓扑排序的实现
-
从AOV网中选择一个没有前驱(入度为0)的顶点并输出。
-
从网中删除该顶点和所有以它为起点的有向边。
-
重复前面的步骤直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。
代码实现

逆拓扑排序
逆拓扑排序的实现
对一个AOV网,如果采用下列步骤进行排序,则称之为逆拓扑排序:
-
从AOV网中选择一个没有后继
(出度为0)的顶点并输出。 -
从网中删除该顶点和所有以它为终点的有向边。
-
重复上面步骤直到当前的AOV网为空。
-
用邻接表实现会更简单一些
-
使用逆邻接表:邻接表的顶点对应储存的信息是指向该顶点的边的信息
-
使用深度优先算法实现逆拓扑排序,顶点输出的序列就是逆拓扑排序序列
-
DFS实现逆拓扑排序:在顶点退栈前输出

关键路径
AOE网
- 在带权有向图中,以
顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成活动所需的时间) - 称之为用边表示活动的网络,简称
AOE网 (Activity On Edge NetWork)
\(AOE\)网具有以下
两个性质:
- 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
- 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
另外,有些活动是可以并行进行的
关键路径
求关键路径的步骤
关键活动、关键路径的特性
-
若关键活动耗时增加,则整个工程的工期将增长
-
缩短关键活动的时间,可以缩短整个工程的工期
-
当缩短到一定程度时,关键活动可能会变成非关键活动
-
可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。


























































































































































































浙公网安备 33010602011771号