王道数据结构笔记
第一章 绪论
![img](https://i-blog.csdnimg.cn/blog_migrate/ca7d93221bd736c46d5d55ac0191ded0.png)
1.1 基本概念
数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别 和处理的符号的集合。
数据元素是数据的基本单位
一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位
数据结构是相互之间存在一种或多种特定关系的数据元素的集合。
数据对象是具有相同性质的数据元素的集合,是数据的一个子集。
数据类型 :一个值的集合和定义在集合上的一组操作的总称
原子类型 :值不可再分
结构类型:值可以再分
抽象数据类型 : 抽象数据组织及与之相关的操作
1.2 数据结构三要素
![image-20230727000113782](王道数据结构笔记.assets/image-20230727000113782.png)
逻辑结构 :数据元素之间的逻辑关系,与存储无关,独立于计算机(一个算法的设计)
线性结构:一对一
除了第一个元素,所有元素都有唯一前驱; 除了最后一个元素,所有元素都有唯一后继
树形结构: 一对多
网状/图状:多对多
集合: 同属一个集合
存储结构:数据结构在计算机中的表示,又称映像/物理结构 (一个算法的实现)
顺序存储:把逻辑上相邻的元素存储在物理位置 上也相邻的存储单元中,元素之间的关系由存储 单元的邻接关系来体现
优点: 随机存取,元素占用最少存储空间
缺点: 只能使用相邻的一整块存储单元,产生较多的外部碎片
链式存储:逻辑上相邻的元素在物理位置上可以 不相邻,借助指示元素存储地址的指针来表示元 素之间的逻辑关系
优点: 不会出现碎片现象
缺点: 存储指针占用额外的存储空间; 只能顺序存取
索引存储:建立附加 的索引表。索引表中的每项称为索引项,索引项 的一般形式是(关键字,地址)
优点: 检索速度快
缺点: 占用较多存储空间; 增加和删除数据要修改索引表,花费较多时间
散列存储:根据元素的关键字直接计算出该元素 的存储地址,又称哈希(Hash)存储
优点: 检索,增加和删除结点都很快
缺点: 若散列函数不好,出现元素存储单元冲突,会增加时间和空间的开销
数据的运算
运算的定义是针对逻辑结构的, 指出运算的功能
运算的实现是针对存储结构的,指出运算的具体操作步骤。
易错点
属于逻辑结构 有序表
循环队列是用顺序表表示的队列,是数据结构,不是抽象数据结构
不同结点的存储空间可以不连续,但结点内的存储空间必须连续
两种不同的数据结构,逻辑结构和物理结构可以完全相同,但数据的运算不同
1.3 算法的概念
算法 :对特定问题求解步骤的描述,是指令的有限序列,其中每条指令表示一个或多个操作
算法的特性
有穷性:一个算法必须总在执行有穷步之后结束,且每一步都可在有穷时间内完成
算法是有穷的,程序是无穷的。
确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出
可行性 :算法中描述的操作都可以通过已经实现的基本运算执行有限次来实现
输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合
输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量
好的算法
1. 正确性
2. 可读性
3. 健壮性:输入非法数据时,算法能适当地做出反应或进行处理,而不会产生莫名其妙的输出结果
4. 高效率与低存储量需求 (时间复杂度低、空间复杂度低)
1.4 算法效率的度量
算法的时间复杂度
定义:事前预估算法时间开销T(n)与问题规模n的关系
衡量算法随着问题规模增大,算法执行时间增长的快慢
同一个算法,实现的语言的级别越高级,执行效率越低
O(1) < O(log2n) < O(n) < O(nlog2n) < O(n2 ) < O(n3 ) < O(2n) < O(n!) < O(nn) 常对幂指阶
![img](https://i-blog.csdnimg.cn/blog_migrate/e942cc9d8eba207e14892d03247b1d0f.png)
算法的空间复杂度
衡量算法随着问题规模增大,算法所需空间增长的快慢
![img](https://i-blog.csdnimg.cn/blog_migrate/3d086891f45157d8d33c5d6419328f9e.png)
第二章 线性表
2.1 线性表的定义和基本操作
![img](https://i-blog.csdnimg.cn/blog_migrate/e9f5db7b8a9a2febdb1127907f6562a3.png)
线性表的定义
线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n = 0时线 性表是一个空表。若用L命名线性表,则其一般表示为 L = (a1, a2, … , ai , ai+1, … , an)
ai是线性表中的“第i个”元素线性表中的位序
a1是表头元素;an是表尾元素
除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅 有一个直接后继
线性表的基本操作
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个位置的元素的值。
& 表示C++中的引用,若传入指针型变量且在函数体内要进行改变,要用到指针变量的引用(C中用指针的指针也可以)
2.2 线性表的顺序表示
![img](https://i-blog.csdnimg.cn/blog_migrate/1fbdcaecb1512b35f6aa5c75c4ac1f63.png)
顺序表的定义
顺序表——用顺序存储的方式实现线性表
顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关 系由存储单元的邻接关系来体现
顺序表的实现 :静态分配、动态分配
动态分布语句:
L.data=(ElemType*)malloc(sizeof(ElemType)*InitSize)
//malloc函数申请一片连续的存储空间
//free函数释放原来的内存空间
动态分配不是链式存储,同样属于顺序存储结构,物理结构没有变化:随机存取方式,只是分配的空间大小可以在运行时决定
特点: 随机访问,存储密度高,插入和删除需要移动大量元素
顺序表的操作
1. **插入操作** 平均时间复杂度O(n)
2. **删除操作** 平均时间复杂度O(n)
![img](https://i-blog.csdnimg.cn/blog_migrate/9f7e064dfd4ccab34aed34ee027061cb.png)
-
查找操作:按值查找、按位查找
2.3 线性表的链式表示
单链表
![img](https://i-blog.csdnimg.cn/blog_migrate/3d0044da3a9522579a4a5145bcc609e8.png)
头指针和头结点的区别:(1)不管带不带头结点,头指针始终指向链表的第一个结点
(2)头结点是带头结点的链表中的第一个结点,通常不存储信息
引入头结点的优点:无论链表是否为空,头指针都指向头结点的非空指针,空表和非空表处理一致
![img](https://i-blog.csdnimg.cn/blog_migrate/273d4a12e8a173124a8bdf299a2cf146.png)
单链表的操作
建立单链表
核心:初始化操作、指定结点的后插操作
(1)头插法,链表的逆置
(2)尾插法,注意设置一个指向表尾结点的指针
插入结点操作
(1)按位序插入(带头结点)
(2)按为序插入(不带头结点)
(3)指定结点的前插操作: 先找到前一个结点,时间复杂度为O(n)
(4) 将前插操作转化为后插操作,然后交换两个结点的数据,时间复杂度为O(1)
删除结点操作
(1)按位序删除(带头结点)
(2)指定结点的删除:先找到前驱节点,再删除结点,O(n)
查找结点操作
(1)按位查找
(2)按值查找
![img](https://i-blog.csdnimg.cn/blog_migrate/c3c7e8bb86548b29bd39a20cac995af9.png)
双链表
![img](https://i-blog.csdnimg.cn/blog_migrate/d5077e1dfb60d61a4bf9144168229d3c.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/503e2b9c65ba9f77ae7ef330809fd5e1.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/85d395cb14acc4f6498f9cd05f054936.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/73b5320bee948d46a247b711508076d8.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/4ec0fde42037d673f4b0110aea6985a5.png)
循环链表
(1)循环单链表:表尾结点的next指针指向头结点
对单链表在表头和表尾操作时: 不设头指针仅设尾指针,效率更高
可以从任意一个结点开始遍历整个链表
(2)循环双链表:表头结点的prior指向表尾结点,表尾结点的next指向头结点
![img](https://i-blog.csdnimg.cn/blog_migrate/3599feb5cbbb01c67e87191c69172d95.png)
静态链表
用数组描述链式存储结构,也有数据域和指针域.指针是结点的相对地址(数组下标),又称游标
插入和删除只需要修改指针,不需要移动元素
![img](https://i-blog.csdnimg.cn/blog_migrate/bbf070f1103f4949c320c904b2b1523e.png)
顺序表和链表的比较
-
逻辑结构 都属于线性表,都是线性结构
-
存储结构 顺序表:顺序存储 链表:链式存储
-
基本操作–初始化
基本操作–增删
![img](https://i-blog.csdnimg.cn/blog_migrate/72b0d72e36f29a84092409cf712d20b6.png)
基本操作–查
![img](https://i-blog.csdnimg.cn/blog_migrate/33a3fa939227f262894d9a9f8195e831.png)
-
如何选择
(1)基于存储的考虑 :难以估计长度和存储规模时用链表,但链表的存储密度较低
(2)基于运算的考虑:经常做按序号访问数据元素用顺序表
(3)基于环境的考虑:较稳定选顺序表,动态性较强选链表
第三章 栈、队列和数组
3.1 栈
定义
只允许在一端进行插入或删除操作的线性表
特点:先进后出,后进先出
栈顶、栈底、空栈
基本操作
InitStack(&S):初始化栈。构造一个空栈 S,分配内存空间。
DestroyStack(&S):销毁栈。销毁并释放栈 S 所占用的内存空间。
Push(&S,x):进栈,若栈S未满,则将x加入使之成为新栈顶。
Pop(&S,&x):出栈,若栈S非空,则弹出栈顶元素,并用x返回。
GetTop(S, &x):读栈顶元素。若栈 S 非空,则用 x 返回栈顶元素
栈的顺序存储结构
实现
栈顶指针:S.top 栈顶元素:S.data[S.top]
进栈: 指针先加1,再送值到栈顶元素
出栈: 先取栈顶元素值,再将栈顶指针减1
共享栈
定义: 将两个栈的栈底设置在共享空间的两端,两个栈顶向中间延伸
判空: top0=-1 top1=MaxSize
判满: top1-top0=1
进栈: top0先加1再赋值,top1先减1再赋值,出栈相反
栈的链式存储结构
优点: 便于多个栈共享储存空间,提高其效率,不会栈满上溢
特点:所有操作在表头进行,通常没有头结点,将头指针作为栈顶指针,便于结点插入/删除
![img](https://i-blog.csdnimg.cn/blog_migrate/4f2c14ffc453303f31eb15d92c85e056.png)
3.2 队列
定义
队列(Queue)是只允许==在一端进行插入(入队),在另一端删除(出队)==的线性表
队头、队尾、空队列
特点:先进先出
队列的基本操作
InitQueue(&Q):初始化队列,构造一个空队列Q。
DestroyQueue(&Q):销毁队列。销毁并释放队列Q所占用的内存空间。
EnQueue(&Q,x):入队,若队列Q未满,将x加入,使之成为新的队尾。
DeQueue(&Q,&x):出队,若队列Q非空,删除队头元素,并用x返回。
GetHead(Q,&x):读队头元素,若队列Q非空,则将队头元素赋值给x
队列的顺序存储结构
实现
-
两个指针: front指示队头元素,rear指向队尾元素下一个位置
-
初始状态(队空): Q.front== Q.rear==0
-
进队: 先送值到队尾元素,再将队尾指针加1
-
出队: 先取队头元素值,再将队头指针加1
-
存在假溢出
队列的链式存储结构
适合数据元素变动较大的情形,不存在队满溢出,多个队列不存在存储分配不合
![img](https://i-blog.csdnimg.cn/blog_migrate/eb3ddf0d23802324afbffd214f52e280.png)
双端队列
只允许从两端插入、两端删除的线性表
输入受限的双端队列:只允许从一端插入、两端删除的线性表
输出受限的双端队列:只允许从两端插入、一端删除的线性表
![img](https://i-blog.csdnimg.cn/blog_migrate/3e568ec1679489fadee9f29c123546f5.png)
3.3 栈和队列的应用
3.3 栈和队列的应用
最后出现的左括号最先被匹配
-
设置一个空栈,顺序读入括号
-
若为 ) ,与栈顶 ( 配对出栈或者不合法
-
若为 ( ,作为新的更急迫的期待压入栈中
-
算法结束,栈为空,否则括号序列不匹
3.3.2 栈在表达式求值中的应用
![img](https://i-blog.csdnimg.cn/blog_migrate/266763f6121b19df6173e5a870682516.png)
中缀转后缀
中缀转后缀的手算方法
① 确定中缀表达式中各个运算符的运算顺序
② 选择下一个运算符,按照==「左操作数 右操作数 运算符」==的方式组合成一个新的操作数
③ 如果还有运算符没被处理,就继续 ②
“左优先”原则:只要左边的运算符能先计算,就优先算左边的 可保证运算顺序唯一
后缀表达式的手算方法: 从左往右扫描,每遇到一个运算符,就让运算符前面最近的两个操作数执行对应运算, 合体为一个操作数
中缀转后缀的机算方法
初始化一个栈,用于保存暂时还不能确定运算顺序的运算符。
从左到右处理各个元素,直到末尾。可能遇到三种情况:
① 遇到操作数。直接加入后缀表达式。
② 遇到界限符。遇到“(”直接入栈;遇到“)”则依次弹出栈内运算符并加入后缀表达式,直到 弹出“(”为止。注意:“(”不加入后缀表达式。
③ 遇到运算符。依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式, 若碰到“(” 或栈空则停止。之后再把当前运算符入栈
后缀表达式计算(算法实现)
用栈实现后缀表达式的计算:
①从左往右扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素(先出栈的为右操作数),执行相应运算,运算结果压回栈顶,回到①
中缀表达式计算(栈实现) 中缀转后缀+后缀表达式求值
初始化两个栈,操作数栈和运算符栈
若扫描到操作数,压入操作数栈
若扫描到运算符或界限符,则按照“中缀转后缀”相同的逻辑压入运算符栈(期间也会弹出 运算符,每当弹出一个运算符时,就需要再弹出两个操作数栈的栈顶元素并执行相应运算, 运算结果再压回操作数栈)
中缀转前缀
中缀转前缀的手算方法:
① 确定中缀表达式中各个运算符的运算顺序
② 选择下一个运算符,按照==「运算符 左操作数 右操作数」==的方式组合成一个新的操作数
③ 如果还有运算符没被处理,就继续 ②
“右优先”原则:只要右边的运算符能先计算,就优先算右边的
前缀表达式计算(算法实现)
用栈实现前缀表达式的计算:
①从右往左扫描下一个元素,直到处理完所有元素
②若扫描到操作数则压入栈,并回到①;否则执行③
③若扫描到运算符,则弹出两个栈顶元素(先弹出的为左操作数),执行相应运算,运算结果压回栈顶,回到①
3.3.3 栈在递归中的应用
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈存储: ① 调用返回地址 ② 实参 ③ 局部变量
递归 :可以把原始问题转换为属性相同,但规模较小的问题
两个条件 1.递归表达式(递归体) 2.边界条件(递归出口)
递归调用时,函数调用栈可称为“递归工作栈” 每进入一层递归,就将递归调用所需信息压入栈顶 每退出一层递归,就从栈顶弹出相应信
缺点:效率低,太多层递归可能会导 致栈溢出;可能包含很多重复计算
3.3.4 队列的应用
在层次遍历中的应用
- 树的遍历
- 图的广度优先遍历
在计算机系统中的应用
- FCFS 先来先服务
- 解决主机与外部设备之间速度不匹配的问题
- 解决由多用户引起的资源竞争问题
3.4 数组和特殊矩阵
3.4.1 数组
数组 :由n(n>=1)个相同类型的数据元素构成的有限序列,每个数据元素称为一个数组元素
数组是线性表的推广
数组地址计算
-
一维数组
-
二维数组–行优先
-
二维数组–列优先
3.4.2 特殊矩阵的压缩存储
压缩存储: 多个值相同的元素只分配一个空间,0不分配空间
对称矩阵的压缩存储
若 n 阶方阵中任意一个元素 ai,j都有 ai,j = aj,i 则该矩阵为对称矩阵
![img](https://i-blog.csdnimg.cn/blog_migrate/0a061b79c82152e2fc5a2ac70111ad49.png)
三角矩阵的压缩存储
下三角矩阵:除了主对角线和下三角区,其余的 元素都相同
上三角矩阵:除了主对角线和上三角区,其余的 元素都相同
![img](https://i-blog.csdnimg.cn/blog_migrate/81b1892d721db657253c66f7c997fd6d.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/6e02ca7779dd40c9756e238b2e83d369.png)
三对角矩阵的压缩存储
![img](https://i-blog.csdnimg.cn/blog_migrate/34e3a564b9de7e2a558337d9e5b571ab.png)
稀疏矩阵的压缩存储
压缩存储策略:
-
顺序存储——三元组 <行,列,值>
-
链式存储——十字链表法
![img](https://i-blog.csdnimg.cn/blog_migrate/235804e9929b319443793241cf6e4925.png)
第四章 串
4.1 定义和实现
4.1.1 定义
串,即字符串(String)是由零个或多个字符组成的有限序列。
T=‘iPhone 11 Pro Max?’
子串:串中任意个连续的字符组成的子序列。 Eg:’iPhone’,’Pro M’ 是串T 的子串
主串:包含子串的串。 Eg:T 是子串’iPhone’的主串
字符在主串中的位置:字符在串中的序号。 Eg:’1’在T中的位置是8(第一次出现)
子串在主串中的位置:子串的第一个字符在主串中的位置 。 Eg:’11 Pro’在 T 中的位置为
串的数据对象限定为字符集(如中文字符、英文字符、数字字符、标点字符等)
串的基本操作,如增删改查等通常以子串为操作对象
串的基本操作
StrAssign(&T,chars):赋值操作。把串T赋值为chars。
StrCopy(&T,S):复制操作。由串S复制得到串T。
StrEmpty(S):判空操作。若S为空串,则返回TRUE,否则返回FALSE。
StrLength(S):求串长。返回串S的元素个数。 ClearString(&S):清空操作。将S清为空串。
DestroyString(&S):销毁串。将串S销毁(回收存储空间)。
Concat(&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
![img](https://i-blog.csdnimg.cn/blog_migrate/6bfb31372ffb4b3d478773b35c382a2d.png)
4.1.2 串的存储结构
顺序存储
![img](https://i-blog.csdnimg.cn/blog_migrate/ea9635aa6ffff37b9ba6182cf6e3965b.png)
4.1.3 基本操作
-
求子串
-
比较
-
定位
![img](https://i-blog.csdnimg.cn/blog_migrate/682b40b544d16c9ced9de2b01c2519a9.png)
4.2 串的模式匹配
4.2.1 简单的模式匹配算法
串的模式匹配:在主串中找到与模式串相同的子串,并返回其所在位置
n为主串长度 m为模式串长度
朴素模式匹配算法 :将主串中所有长度为m的子串依次与模式串对比,直到找到一个完全匹配的或所有的子串都不匹配为止
当前子串匹配失败:主串指针i指向下一个子串的第一个位置,模式串指针j回到模式串的第一个位置
当前子串匹配成功:返回当前子串第一个字符的位置
直到匹配成功/匹配失败最多需要 (n-m+1)*m 次比较
最坏时间复杂度:O(nm)
4.2.2 KMP算法
![img](https://i-blog.csdnimg.cn/blog_migrate/9c8b953b86ce68e3176b48935657a98e.png)
最坏时间复杂度:O(m+n)
![img](https://i-blog.csdnimg.cn/blog_migrate/19202041f0f8efa669e97f7d8f99285f.png)
第五章 树与二叉树
5.1 树的基本概念
5.1.1 树的定义
树是n(n≥0)个结点的有限集合,n = 0时,称为空树,这是一种特殊情况。
在任意一棵非 空树中应满足:
1)有且仅有一个特定的称为根的结点。
2)当n > 1时,其余结点可分为m(m > 0)个互不相交的有限集合T1, T2,…, Tm,其中每个集 合本身又是一棵树,并且称为根结点的子树。
非空树的特性:
有且仅有一个根节点
没有后继的结点称为“叶子结点”(或终端结点)
有后继的结点称为“分支结点”(或非终端结点)
除了根节点外,任何一个结点都有且仅有一个前驱
每个结点可以有0个或多个后继。
树是一种递归定义的数据结构
5.1.2 基本术语
结点的度 一个结点的子结点个数
树的度 树中结点的最大度数
结点的深度 从根结点开始自顶向下逐层累加
结点的高度 从叶结点开始自底向上逐层累加
树的高度(深度) 树中结点的最大层数
两结点之间的路径 两结点之间经过的结点序列
路径长度 路径上经过的边的个数
注意 树中的分支是有向的(双亲指向孩子),路径从上向下,两个孩子之间不存在路径
有序树——逻辑上看,树中结点的各子树从左至右是有次序的,不能互换
无序树——逻辑上看,树中结点的各子树从左至右是无次序的,可以互换
**森林:**森林是m(m≥0)棵互不相交的树的集合
5.1.3 树的性质
-
结点数=总度数+1
-
度为m的树、m叉树 的区别
度为m的树第 i 层至多有 mi-1 个结点(i≥1)
m叉树第 i 层至多有 mi-1 个结点(i≥1)
高度为h的m叉树至少有 h 个结点
高度为h、度为m的树至少有 h+m-1 个结点
高度为h的m叉树至多有 (mh -1)/m-1 个结点
具有n个结点的m叉树的最小高度为 [logm(n(m - 1) + 1)]
5.2 二叉树的概念
5.2.1 二叉树的定义及主要特征
二叉树是n(n≥0)个结点的有限集合:
① 或者为空二叉树,即n = 0。
② 或者由一个根结点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树 又分别是一棵二叉树。
特点:①每个结点至多只有两棵子树 ②左右子树不能颠倒(二叉树是有序树)
二叉树与度为2的有序树的区别
度为2的树至少有3个结点,二叉树可为空
度为2的有序树的孩子的左右次序相对于另一个孩子无须区分左右
二叉树是有序树
特殊的二叉树
满二叉树:一棵高度为h,且含有2h - 1个结点的二叉树
树中的每层都含有最多的结点,只有最后一层有叶子结点 且不存在度为 1 的结点
满二叉树:当且仅当其每个结点都与高度为h的 满二叉树中编号为1~n的结点一一对应时,称为 完全二叉树
叶子结点只在最大的两层上 ,若有度为1的结点,只有一个,该结点只能是左孩子
二叉排序树
(1)左子树上所有结点的关键字小于根结点
(2)右子树上所有结点的关键字大于根结点
(3)左右子树又各是一颗二叉排序树
![img](https://i-blog.csdnimg.cn/blog_migrate/17132207e62abe047eeff72ceb53c81b.png)
-
平衡二叉树:树上任一结点的左子树和右子树的深度之差不超过1(搜索效率高)
5.2.2 二叉树的性质
设非空二叉树中度为0、1和2的结点个数分别为n0、n1和n2,则 n0 = n2 + 1 (叶子结点比二分支结点多一个)
二叉树第 i 层至多有 2i-1 个结点(i≥1)
m叉树第 i 层至多有 mi-1 个结点(i≥1)
高度为h的m叉树至多有 (mh -1)/m-1 个结点
高度为h的2叉树至多有2h-1个结点
完全二叉树的常见考点
具有n个(n > 0)结点的完全二叉树的高度h为log2(n + 1)或log2n + 1
对于完全二叉树,可以由的结点数 n 推出度为0、1和2的结点个数为n0、n1和n2
若完全二叉树有2k个(偶数)个结点,则 必有 n1=1, n0 = k, n2 = k-1
若完全二叉树有2k-1个(奇数)个结点,则 必有 n1=0, n0 = k, n2 = k-1
5.2.3 二叉树的存储结构
顺序存储
几个重要常考的基本操作:
i 的左孩子 ——2i
i 的右孩子 ——2i+1
i 的父节点—— i/2
i 所在的层次 —— log2(n + 1)或log2n+ 1
若完全二叉树中共有n个结点,则
判断 i 是否有左孩子? ——2i ≤ n
判断 i 是否有右孩子? ——2i+1 ≤ n
判断 i 是否是叶子/分支结点?——i > n/2
二叉树的顺序存储中,一定要把二叉 树的结点编号与完全二叉树对应起来
最坏情况:高度为 h 且只有 h 个结点的单 支树(所有结点只有右孩子),也至少需 要 2h-1 个存储单元
结论:二叉树的顺序存储结构,只适合存 储完全二叉树
链式存储
二叉链表3个域: data,lchild,rchild
n个结点的二叉链表有n+1个空链域(根结点不用指针)形成线索链表
5.3 二叉树的遍历和线索二叉树
5.3.1 二叉树的遍历
遍历:按照某种次序把所有结点都访问一遍
先序遍历:根左右(NLR)
![img](https://i-blog.csdnimg.cn/blog_migrate/dee31c425306f26b60a0415833dc2727.png)
中序遍历:左根右(LNR)
![img](https://i-blog.csdnimg.cn/blog_migrate/576b17872f345dd3582d84ae18551e60.png)
后序遍历:左右根(LRN)
![img](https://i-blog.csdnimg.cn/blog_migrate/9cfc5ed3ff9d42cb3b2fc1721d112c35.png)
层序遍历
算法思想:
①初始化一个辅助队列
②根结点入队
③若队列非空,则队头结点出队,访问该结点,并将其左、右孩子插入队尾(如果有的话)
④重复③直至队列为空
由遍历序列构造二叉树
若只给出一棵二叉树的 前/中/后/层 序遍历序列中的一种,不能唯一确定一棵二叉树
先序和中序
(1)先序中: 第一个为根结点
(2)中序中: 根结点分割成两个子序列,前左子树,后右子树
(3)先序中: 找到两个子序列,各自的第一个结点又是根结点
后序和中序 后序最后一个结点相当于先序第一个结点
![img](https://i-blog.csdnimg.cn/blog_migrate/c0b34e213ff01de069fb0584ff2dacbb.png)
层序和后序不可以
![img](https://i-blog.csdnimg.cn/blog_migrate/406bf46e97ad393580856708fbaa9591.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/b64f4321b4cbdaaebdf782b8fd897332.png)
5.3.2 线索二叉树 (没理解)
目的: 加快查找结点前驱和后继的速度
线索: 指向前驱和后继的指针
线索化: 对二叉树以某种次序遍历使其成为线索二叉树的过程
无左子树,令lchild指向前驱结点;无右子树,令rchild指向后继结点 前驱,后继由具体的遍历方式决定
![img](https://i-blog.csdnimg.cn/blog_migrate/ef83a1fee3c7f5a48d1c14f3bb5b09a6.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/8b3ff3d928e78564ecd38c6c5427a8eb.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/f2948a4103a49ebb3769d4cfbfc13790.png)
二叉树线索化
![img](https://i-blog.csdnimg.cn/blog_migrate/4920ab9ac8134b307588540aa0d7809f.png)
5.4 树、森林
5.4.1 树的存储结构
双亲表示法(顺序存储)
-
定义: 连续空间存储,每个结点增设一个伪指针,指示双亲在数组中位置,根结点下标为0,其伪指针为-1
-
特点: 可以很快得到双亲,但求孩子要遍历整个结构
孩子表示法(顺序+链式存储)
-
定义:顺序存储各个节点,每个结点中保存孩子 链表头指针
-
特点: 求孩子很方便,求双亲不方便
孩子兄弟表示法(链式存储)
-
定义: 左指针指向第一个孩子,右指针指向第一个兄弟,二叉链表作为存储结构
-
优点: 方便实现树转化为二叉树,易于查找孩子
-
缺点: 查找双亲麻烦,若增设parent指向双亲,会方便
5.4.2 树、森林和二叉树的转换
树转换为二叉树
左指针指向第一个孩子,右指针指向第一个兄弟,根没有兄弟,二叉树没有右子树
![img](https://i-blog.csdnimg.cn/blog_migrate/0d6796a004fc3b5faa1aa9d2d247bbc7.png)
森林转化为二叉树
森林中的树依次转化为二叉树,每棵二叉树的根依次作为上一颗二叉树的右子树
![img](https://i-blog.csdnimg.cn/blog_migrate/160aece5a4dd4471eaefcdf6dcb8b170.png)
二叉树转化为森林
- 二叉树的根及左子树作为第一棵树的二叉树形态,再转换为树(右孩子变为兄弟)
- 根的右子树及其左孩子作为第二棵树,右孩子作为第三棵树,反复下去
![img](https://i-blog.csdnimg.cn/blog_migrate/d6165f7cc00cfc9a3fda060fbe98db68.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/af98a4c44a26356f5633554b07d2d29b.png)
5.4.3 树和森林的遍历
树的先根遍历(深度优先遍历)
先访问根,再从左到右遍历每棵子树,与相应二叉树的先序序列相同
树的后根遍历(深度优先遍历)
从左到右遍历每棵子树,再访问根,与这棵树相应二叉树的 中序序列相同
树的层次遍历(广度优先遍历)
①若树非空,则根节点入队
②若队列非空,队头元素出队并访问,同 时将该元素的孩子依次入队
③重复②直到队列为空
森林的先序遍历==依次对各个子树进行先序遍历
若森林为非空,则按如下规则进行遍历:
(1)访问森林中第一棵树的根结点。
(2)先序遍历第一棵树中根结点的子树森林。
(3)先序遍历除去第一棵树之后剩余的树构成的森林。
森林的中序遍历==依次对各个子树进行后序遍历
若森林为非空,则按如下规则进行遍历:
(1) 中序遍历森林中第一棵树的根结点的子树森林。
(2) 访问第一棵树的根结点。
(3)中序遍历除去第一棵树之后剩余的树构成的森林
5.5 树与二叉树的应用
5.5.1 哈夫曼树和哈夫曼编码
结点的权:有某种现实含义的数值(如:表示结点的重要性等)
结点的带权路径长度:从树的根到该结点的路径长度(经过的边数)与该结点上权值的乘积
树的带权路径长度:树中所有叶结点的带权路径长度之和
定义: 在含有n个带权叶结点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树
哈夫曼树的构造
给定n个权值分别为w1, w2,…, wn的结点,构造哈夫曼树的算法描述如下:
(1)将这n个结点分别作为n棵仅含一个结点的二叉树,构成森林F。
(2)构造一个新结点,从F中选取两棵根结点权值最小的树作为新结点的左、右子树,并且将新 结点的权值置为左、右子树上根结点的权值之和。
(3)从F中删除刚才选出的两棵树,同时将新得到的树加入F中。
(4)重复步骤2)和3),直至F中只剩下一棵树为止。
特点
- 每个初始结点最终都成为叶结点,且权值越小的结点到根结点的路径长度越大
- 哈夫曼树的结点总数为2n − 1
- 哈夫曼树中不存在度为1的结点。
- 哈夫曼树并不唯一,但WPL必然相同且为最优
哈夫曼编码
**固定长度编码:**每个字符用相等长度的二进制位表示
可变长度编码:允 许对不同字符用不等 长的二进制位表示
前缀编码 :没有一个编码是另一个编码的前缀
构造哈夫曼编码:
(1)字符集中的每个字符作为一个叶子结点,各个字符出现的频度作为结点 的权值,根据之前介绍的方法构造哈夫曼树
(2)从根结点到叶子结点的路径上标记序列,0转向左孩子,1转向右孩子
![img](https://i-blog.csdnimg.cn/blog_migrate/f3ea42f79636f1f4485d3a4460308899.png)
5.5.2 并查集
![img](https://i-blog.csdnimg.cn/blog_migrate/d5953dcb85c3cb374d8cd031a3b29e4d.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/e4ca5f83e0bb0c2bfef81418f9e60f52.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/ee2bece43b66f84eaeeb54dcf0a801bf.png)
第六章 图
6.1 图的基本概念
图的定义
图G由顶点集V和边集E组成,记为G = (V, E),其中V(G)表示图G中顶点的有限非空集;E(G) 表示图G中顶点之间的关系(边)集合。若V = {v1, v2, … , vn},则用==|V|表示图G中顶点的个 数==,也称图G的阶,E = {(u, v) | uÎV, vÎV},用==|E|表示图G中边的条数==。
注意:线性表可以是空表,树可以是空树,但图不可以是空,即V一定是非空集 ,E可以是空集
有向图: 若E是有向边(也称弧)的有限集合时,则图G为有向图。 弧是顶点的有序对,记为==<v,w>,其中v、w是顶点,v称为 弧尾,w称为弧头==,称为从顶点v到顶点w的弧,也称 v邻接到w,或w邻接自v。 <v,w> ≠<w,v>
无向图: 若E是无向边(简称边)的有限集合时,则图G为无向图。边 是顶点的无序对,记为(v, w)或(w, v),因为==(v, w) = (w, v),其 中v、w是顶点==。可以说顶点w和顶点v互为邻接点。边(v, w) 依附于顶点w和v,或者说边(v, w)和顶点v、w相关联。
**简单图:**① 不存在重复边; ② 不存在顶点到自身的边
多重图:图G中某两个结点之间的边数多于 一条,又允许顶点通过同一条边和自己关联, 则G为多重图
顶点的度、入度、出度
无向图:顶点v的度是指依附于该顶点的边的条数,记为TD(v)。
无向图的全部顶点的度的和等于边数的2倍
有向图: 入度是以顶点v为终点的有向边的数目,记为ID(v);
出度是以顶点v为起点的有向边的数目,记为OD(v)。
==顶点v的度==等于其入度和出度之和,即TD(v) = ID(v) + OD(v)。
1
顶点-顶点的关系描述
路径——顶点vp到顶点vq之间的一条路径是指顶点序列 ,
回路——第一个顶点和最后一个顶点相同的路径称为回路或环
简单路径——在路径序列中,顶点不重复出现的路径称为简单路径。
简单回路——除第一个顶点和最后一个顶点外,其余顶点不重复出现的回路称为简单回路。
路径长度——路径上边的数目
点到点的距离——从顶点u出发到顶点v的最短路径若存在,则此路径的长度称为从u到v的距离。 若从u到v根本不存在路径,则记该距离为无穷(∞)
无向图中,若从顶点v到顶点w有路径存在,则称v和w是连通的
有向图中,若从顶点v到顶点w和从顶点w到顶点v之间都有路径,则称这两个顶点是强连通的
若图G中任意两个顶点都是连通的,则称图G为 连通图,否则称为非连通图
若图中任何一对顶点都是强连通的,则称此图为 强连通图
子图: 设有两个图G = (V, E)和G’ = (V’,E’),若V’是V的子集,且E’是 E的子集,则称G¢是G的子图。
生成子图:满足==V(G’) = V(G)==的子图G’
连通分量: 无向图中的极大连通子图称为连通分量。
极大连通子图:子图必须连通,且包含 尽可能多的顶点和边
强连通分量:有向图中的极大强连通子图称为有向图的强连通分量
连通图(无向)的生成树是包含图中全部顶点的一个极小连通子图
若图中顶点数为n,则它的生成树含有 n-1 条边。
在非连通图中,连通分量的生成树构成了非连通图的生成森林。
边的权、带权图/网
边的权——在一个图中,每条边都可以标上具有某种含义的数值,该数值称为该边的权值。
带权图/网——边上带有权值的图称为带权图,也称网。
带权路径长度——当图是带权图时,一条路径上所有边的权值之和,称为该路径的带权路径长度
特殊形态的图
无向完全图——无向图中任意两个顶点 之间都存在边
有向完全图——有向图中任意两个顶点 之间都存在方向相反的两条弧
边数很少的图称为稀疏图,反之称为稠密图
![img](https://i-blog.csdnimg.cn/blog_migrate/a6e39fdd9ab7d7173be2de090e6ae919.png)
树——不存在回路,且连通的无向图
n个顶点的树,必有n-1条边
有向树——一个顶点的入度为0、其余顶点的 入度均为1的有向图,称为有向树
![img](https://i-blog.csdnimg.cn/blog_migrate/3d5ef915e55b8d885563551a6b9cab3e.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/5ba4812ab2a283f5ed5488c7631f4379.png)
6.2 图的存储及基本操作
![img](https://i-blog.csdnimg.cn/blog_migrate/f0db616c0b40232b72812737bcadc077.png)
第i个结点的度 = 第i行(或第i列)的非零元素个数
第i个结点的出度 = 第i行的非零元素个数
第i个结点的入度 = 第i列的非零元素个数
第i个结点的度 = 第i行、第i列的非零元素个数之和
邻接矩阵法求顶点的度/出度/入度的时间复杂度为 O(|V|)
空间复杂度:O(|V|2 ) ——只和顶点数相关,和实际的边数无关
邻接矩阵法的性质
设图G的邻接矩阵为A(矩阵元素为0/1),则An 的元素An [i] [j]等于由顶点i到顶点j的长度为n的路径的数目
![img](https://i-blog.csdnimg.cn/blog_migrate/a0036f21f04ca9d33a398ec5e938b41e.png)
6.2.2 邻接表法
![img](https://i-blog.csdnimg.cn/blog_migrate/9d3f3350feb100cd39ce279e0369c630.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/3e9940e74d92f41ad9f359d7be180964.png)
6.2.3 邻接多重表
![img](https://i-blog.csdnimg.cn/blog_migrate/f9f8f9a7afe2bdda5da5f22629319b5d.png)
6.2.4 十字链表
![img](https://i-blog.csdnimg.cn/blog_migrate/bafacea97f0d7590fe60e119ecff6c33.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/cf0ede9f0b2119640ae77becacfe217d.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/e4082305816e1abfe9a9aa6cf8f444cf.png)
6.2.5 图的基本操作
• Adjacent(G,x,y):判断图G是否存在边或(x, y)。
• Neighbors(G,x):列出图G中与结点x邻接的边。
• InsertVertex(G,x):在图G中插入顶点x。
• DeleteVertex(G,x):从图G中删除顶点x。
• AddEdge(G,x,y):若无向边(x, y)或有向边不存在,则向图G中添加该边。
• RemoveEdge(G,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)或对应的权值。
• Set_edge_value(G,x,y,v):设置图G中边(x, y)或对应的权值为v。
6.3 图的遍历
6.3.1 广度优先遍历BFS
**步骤:**1. 找到与⼀个顶点相邻的所有顶点
-
标记哪些顶点被访问过
-
需要⼀个辅助队列
同⼀个图的邻接矩阵表示⽅式唯⼀,因此⼴度优先遍历序列唯⼀
同⼀个图邻接表表示⽅式不唯⼀,因此⼴度优先遍历序列不唯⼀
存在的问题:如果是⾮连通图,则⽆法遍历完所有结点
![img](https://i-blog.csdnimg.cn/blog_migrate/aadb7196888ac5206df9982f23b8306c.png)
性能分析 :空间复杂度: O(|V|)
时间复杂度:邻接表:O(|V|+|E|) 邻接矩阵:O(|V|²)
广度优先生成树
定义: 广度遍历过程中,得到的一颗遍历树
特点: 邻接矩阵中唯一,邻接表中不唯一
对⾮连通图的⼴度优先遍历,可得到⼴度优先⽣成森林
![img](https://i-blog.csdnimg.cn/blog_migrate/d5dcf6f668c9edf789d0561948a390bf.png)
6.3.2 深度优先遍历DFS
步骤:
-
首先访问起始顶点v
-
访问v的未访问过的任一邻接顶点w
-
再访问w的未访问过的任一邻接顶点w2
-
重复下去,直到不能继续向下访问,依次退回到最近被访问的顶点
存在的问题:如果是⾮连通图,则⽆法遍历完所有结点
性能分析:
空间复杂度:来⾃函数调⽤栈,最坏情况,递归深度为O(|V|)
时间复杂度=访问各结点所需时间+探索各条边所需时间
邻接表:O(|V|+|E|),邻接矩阵:O(|V|²)
同⼀个图的邻接矩阵表示⽅式唯⼀,因此深度优先遍历序列唯⼀,深度优先⽣成树也唯⼀
同⼀个图邻接表表示⽅式不唯⼀,因此深度优先遍历序列不唯⼀,深度优先⽣成树也不唯⼀
6.3.3 图的遍历与图的连通性
对⽆向图进⾏BFS/DFS遍历 调⽤BFS/DFS函数的次数=连通分量数
对于连通图,只需调⽤1次 BFS/DFS
对于强连通图,从任⼀结点出发都只需调⽤1次 BFS/DFS
6.4 图的应用
6.4.1 最小生成树
对于⼀个带权连通⽆向图G = (V, E),⽣成树不同,每棵树的权(即树中所有边上的权值 之和)也可能不同。设R为G的所有⽣成树的集合,若T为R中边的权值之和最⼩的⽣成 树,则T称为G的最⼩⽣成树(Minimum-Spanning-Tree, MST)。
• 最⼩⽣成树可能有多个,但边的权值之和总是唯⼀且最⼩的
• 最⼩⽣成树的边数 = 顶点数 - 1。砍掉⼀条则不连通,增加⼀条边则会出现回路
• 如果⼀个连通图本身就是⼀棵树,则其最⼩⽣成树就是它本身
• 只有连通图才有⽣成树,⾮连通图只有⽣成森林
Prim 算法(普⾥姆)
从某⼀个顶点开始构建⽣成树; 每次将代价最⼩的新顶点纳⼊⽣成 树,直到所有顶点都纳⼊为⽌。
步骤:
初始化: 先任选一个顶点作为初始顶点
循环(直到包含所有顶点): 再选择这个顶点的邻边中权值最小的边且不会构成回路
再选择这两个顶点的邻边中权值最小的边且不会构成回路
特点: 时间复杂度: O(|V|²),不依赖于|E|,适合边稠密的图
Kruskal 算法(克鲁斯卡尔)
每次选择⼀条权值最⼩的边,使这 条边的两头连通(原本已经连通的 就不选) 直到所有结点都连通
步骤:
初始化: 先包含所有的顶点,没有边
循环(直到成为一棵树): 按权值递增的顺序选择边且不构成回路,直到包含n-1条边
特点: 采用堆存放边集合,时间复杂度O(|E|log|E|),适合边稀疏而顶点较多的图
6.4.2 最短路径
BFS求⽆权图的单源最短路径
⽆权图可以视为⼀种特殊的带权图,只是每条边的权值都为1
BFS算法求单源最短路径只适⽤于⽆ 权图,或所有边的权值都相同的图
Dijkstra算法求单源最短路径
初始化: 集合S为{0},dist[]为初始顶点0到各个顶点的距离,没有为无穷 path[]中初始顶点0为-1(一直不变),0到其他点有距离为0,没有为无穷
在dist[]中选剩下值最小的点j,若dist[j]+arcs[j] [k] <dist[k],则更新dist[k] 在集合S中加入此点,若更新了dist[k],则令path[k]=j
再在集合S中剩下的点中重复操作,直到S包含所有点
单源时间复杂度为O(|V|²),所有结点对为O(|V|³)
Floyd算法法求各顶点间最短路径
递推产生一个n阶方阵序列,从A﹣¹开始到Aⁿ﹣¹
初始时: 若任意两个顶点存在边,权值作为最短路径,不存在为无穷
以后逐步在原路径中加入顶点k(k从0到n-1)作为中间顶点,若路径减少,则替换原路径
A(k)[i][j]: 从顶点i到顶点j,中间结点的序号不大于k的最短路径的长度
特点:
时间复杂度: O(|V|³)
允许带负权值的边,不允许包含负权值的边组成回路
适用于带权无向图,视为有往返二重边的有向图
6.4.3 有向无环图DAG
有向⽆环图:若⼀个有向图中不存在环,则称为有向⽆环图,简称DAG图
结题步骤:
Step 1:把各个操作数不重复地排成⼀排
Step 2:标出各个运算符的⽣效顺序(先 后顺序有点出⼊⽆所谓)
Step 3:按顺序加⼊运算符,注意“分层”
Step 4:从底向上逐层检查同层的运算符 是否可以合体
6.4.4 拓扑排序
AOV⽹(Activity On Vertex NetWork,⽤顶点表示活动的⽹): ⽤DAG图(有向⽆环图)表示⼀个⼯程。顶点表示活动,有向边表示活动Vi必须先于活动Vj进⾏
拓扑排序:在图论中,由⼀个有向⽆环图 的顶点组成的序列,当且仅当满⾜下列条 件时,称为该图的⼀个拓扑排序: ① 每个顶点出现且只出现⼀次。 ② 若顶点A在序列中排在顶点B的前⾯,则 在图中不存在从顶点B到顶点A的路径。
或定义为:拓扑排序是对有向⽆环图的顶 点的⼀种排序,它使得若存在⼀条从顶点A 到顶点B的路径,则在排序中顶点B出现在 顶点A的后⾯。每个AOV⽹都有⼀个或多个 拓扑排序序列。
拓扑排序的实现:
① 从AOV⽹中选择⼀个没有前驱==(⼊度为0)==的顶点并输出。
② 从⽹中删除该顶点和所有以它为起点的有向边。
③ 重复①和②直到当前的AOV⽹为空或当前⽹中不存在⽆前驱的顶点为⽌。
对⼀个AOV⽹,如果采⽤下列步骤进⾏排序,则称之为逆拓扑排序:
① 从AOV⽹中选择⼀个没有后继(出度为0)的顶点并输出。
② 从⽹中删除该顶点和所有以它为终点的有向边。
③ 重复①和②直到当前的AOV⽹为空。
6.4.5 关键路径
AOE⽹ (Activity On Edge NetWork):在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如 完成活动所需的时间),称之为⽤边表示活动的⽹络,简称AOE
AOE⽹具有以下两个性质:
① 只有在某顶点所代表的事件发⽣后,从该顶点出发的各有向边所代表的活动才能开始;
② 只有在进⼊某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发⽣。 另外,有些活动是可以并⾏进⾏的
在AOE⽹中仅有⼀个⼊度为0的顶点,称为开始顶点(源点),它表示整个⼯程的开始; 也仅有⼀个出度为0的顶点,称为结束顶点(汇点),它表示整个⼯程的结束。
从源点到汇点的有向路径可能有多条,所有路径中,具有最⼤路径⻓度的路径称为 关键路径,⽽把关键路径上的活动称为关键活动
求关键路径步骤
① 求所有事件的最早发⽣时间 ve( ) --决定了所有从vk开始的活动能够开⼯的最早时间
② 求所有事件的最迟发⽣时间 vl( ) --它是指在不推迟整个⼯程完成的前提下,该事件最迟必须发⽣的时间
③ 求所有活动的最早发⽣时间 e( ) – 指该活动弧的起点所表⽰的事件的最早发⽣时间
④ 求所有活动的最迟发⽣时间 l( ) --它是指该活动弧的终点所表示事件的最迟发⽣时间与该活动所需时间之差
⑤ 求所有活动的时间余量 d( ) –时间余量d(i)=l(i)-e(i)
d(i)=0的活动就是关键活动, 由 关键活动可得关键路径
求所有事件的最早发⽣时间 ve( )
从源点开始往后加上权值,取不同路径的最大值
求所有事件的最迟发⽣时间 vl( )
Vl(汇点)=Ve(汇点),从汇点往前依次减去权值,取不同路径最小值
求所有活动的最早发⽣时间 e( )
若边表⽰活动ai,则有e(i) = ve(k)
求所有活动的最迟发⽣时间 l( )
若边表⽰活动ai,则有l(i) = vl(j) - Weight(vk, vj)
求所有活动的时间余量 d( )
d(i) = l(i) - e(i)
若关键活动耗时增加,则整个⼯程的⼯期将增⻓ 缩
短关键活动的时间,可以缩短整个⼯程的⼯期
当缩短到⼀定程度时,关键活动可能会变成⾮关键活动
第七章 查找
7.1 查找的基本概念
查找 —— 在数据集合中寻找满⾜某种条件的数据元素的过程称为查找
查找表(查找结构)—— ⽤于查找的数据集合称为查找表,它由同⼀类型的数据元素(或记录)组成
关键字 —— 数据元素中唯⼀标识该元素的某个数据项的值,使⽤基于关键字的查找,查找结果应该是 唯⼀的
对查找表的常⻅操作
①查找符合条件的数据元素 ②插⼊、删除某个数据元素
只需进⾏操作① —— 静态查找表 仅关注查找速度
也要进⾏操作② —— 动态查找表 除了查找速度,也要关注 插/删操作是否⽅便实现
适合静态查找表: 顺序查找,折半查找,散列查找
适合动态查找表: 二叉排序树,散列查找,二叉平衡树和B树都是二叉排序树的改进
查找算法的评价指标
查找⻓度——在查找运算中,需要对⽐关键字的次数称为查找⻓度
平均查找⻓度(ASL, Average Search Length)—— 所有查找过程中进⾏关键字的⽐较次数的平均值
7.2 顺序查找和折半查找
7.2.1 顺序查找
顺序查找,⼜叫“线性查找”,通常⽤于线性表
ASL成功=(n+1)/2 ASL失败=n+1
7.2.2 折半查找(二分查找)
折半查找,⼜称“⼆分查找”,仅适⽤于有序的顺序表。
折半查找判定树的构造
如果当前low和high之间有奇数个元素,则 mid 分隔后,左右两部分元素个数相等
如果当前low和high之间有偶数个元素,则 mid 分隔后,左半部分⽐右半部分少⼀个元素
折半查找的判定树中,若 mid = ⌊(low + high)/2],则对于任何⼀个结点,必有: 右⼦树结点数-左⼦树结点数=0或1
折半查找的判定树⼀定是平衡⼆叉树
折半查找的判定树中,只有最下⾯⼀层是不满的 因此,元素个数为n时树⾼h = ⌈log2(n + 1)⌉
折半查找的时间复杂度 = O(log2n)
7.2.3 分块查找
分块查找,⼜称索引顺序查找,算法过程如下: ①在索引表中确定待查记录所属的分块(可顺序、可折半) ②在块内顺序查找
特点:块内⽆序、块间有序
算法思想:
分为若干子块,块内可以无序,块之间有序
第一块中最大关键字<第二块中所有记录,以此类推
建立一个索引表,含有各块最大关键字和各块第一个元素的地址,按关键字有序排列
若索引表中不包含⽬标关键字,则折半查找索引表最终停在 low>high,要在low所指分块中查找
low超出索引表范 围,查找失败
设索引查找和块内查找的平均查找⻓度分别为LI、LS,则分块查找的平均查找⻓度为 ASL=LI + LS
7.3 树型查找
7.3.1 二叉排序树BST
二叉排序树,又称二叉查找树(BST,Binary Search Tree)
二叉排序树可用于元 素的有序组织、搜索
具有如下性质:左子树上所有结点的关键字均小于根结点的关键字; 右子树上所有结点的关键字均大于根结点的关键字。 左子树和右子树又各是一棵二叉排序树。
左子树结点值 < 根结点值 < 右子树结点值
进行中序遍历,可以得到一个递增的有序序列
二叉排序树的删除:① 若被删除结点z是叶结点,则直接删除,不会破坏二叉排序树的性质。
② 若结点z只有一棵左子树或右子树,则让z的子树成为z父结点的子树,替代z的位置
③ 若结点z有左、右两棵子树,则令z的直接后继(或直接前驱)替代z,然后从二叉排序树中删去这 个直接后继(或直接前驱),这样就转换成了第一或第二种情况
查找效率分析:
平均查找长度ASL=(每层个数*对应层数)/总个数
最坏情况: 类似有序单链表O(n)
最好情况: 平衡二叉树O(㏒₂n)
查找过程: 与二分查找类似,但二分查找的判定树唯一
7.3.2 平衡二叉树AVL
平衡二叉树(Balanced Binary Tree),简称平衡树(AVL树)——树上任一结点的左子树和右子树的 高度之差不超过1
结点的平衡因子=左子树高-右子树高
平衡二叉树的插入:从插入点往回 找到第一个不 平衡结点,调 整以该结点为 根的子树,每次调整的对象都是==“最小不平衡子树”==
LL平衡旋转(右单旋转)。由于在结点A的左孩子(L)的左子树(L)上插入了新结点,A的平衡因子 由1增至2,导致以A为根的子树失去平衡,需要一次向右的旋转操作。将A的左孩子B向右上旋转代替A成 为根结点,将A结点向右下旋转成为B的右子树的根结点,而B的原右子树则作为A结点的左子树
RR平衡旋转(左单旋转)。由于在结点A的右孩子(R)的右子树(R)上插入了新结点,A的平衡因 子由-1减至-2,导致以A为根的子树失去平衡,需要一次向左的旋转操作。将A的右孩子B向左上旋转代替 A成为根结点,将A结点向左下旋转成为B的左子树的根结点,而B的原左子树则作为A结点的右子树
LR平衡旋转(先左后右双旋转)。由于在A的左孩子(L)的右子树(R)上插入新结点,A的平衡因 子由1增至2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先左旋转后右旋转。先将A结点 的左孩子B的右子树的根结点C向左上旋转提升到B结点的位置,然后再把该C结点向右上旋转提升到A结 点的位置
RL平衡旋转(先右后左双旋转)。由于在A的右孩子(R)的左子树(L)上插入新结点,A的平衡 因子由-1减至-2,导致以A为根的子树失去平衡,需要进行两次旋转操作,先右旋转后左旋转。先将A 结点的右孩子B的左子树的根结点C向右上旋转提升到B结点的位置,然后再把该C结点向左上旋转提升 到A结点的位置
含有n个结点的平衡二叉树的最大深度为O(log2n) ,平衡二叉树的平均查找长度为O(log2n)
![img](https://i-blog.csdnimg.cn/blog_migrate/053372763cc4ec13b9bf9363c901229e.png)
7.3.3 红黑树
平衡二叉树 AVL:插入/删除 很容易破坏“平衡”特性,需要频繁调整树的形态。如:插入操作导致不 平衡,则需要先计算平衡因子,找到最小不平衡子树(时间开销大),再进行 LL/RR/LR/RL 调整
红黑树 RBT:插入/删除 很多时候不会破坏“红黑”特性,无需频繁调整树的形态。即便需要调整,一 般都可以在常数级时间内完成
平衡二叉树:适用于以查为主、很少插入/删除的场景
红黑树:适用于频繁插入、删除的场景,实用性更强
红黑树是二叉排序树 左子树结点值 ≤ 根结点值 ≤ 右子树结点值
定义
①每个结点或是红色,或是黑色的
②根节点是黑色的
③叶结点(外部结点、NULL结点、失败结点)均是黑色的
④不存在两个相邻的红结点(即红结点的父节点和孩子结点均是黑色)
⑤对每个结点,从该节点到任一叶结点的简单路径上,所含黑结点的数 目相同(每个结点的左右子树中黑结点的层数是相等)
结点的黑高 bh —— 从某结点出发(不含该结点)到达任一空叶结点的路径上黑结点总数
性质: 从根节点到叶结点的最长路径不大于最短路径的2倍,有n个内部节点的红黑树高度 h ≤ 2log2(n+1)
红黑树查找操作时间复杂度 = O(log2n)
红黑树的插入
插入过程
![img](https://i-blog.csdnimg.cn/blog_migrate/94895827597e1ce7e48d996147658844.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/efa5a3a3d7a0aa2e740c2f5fe89fb7ba.png)
7.4 B树和B+树
7.4.1 B树
B树,⼜称多路平衡查找树,B树中所有结点的孩⼦个数的最⼤值称为B树的阶,通常⽤m表示。⼀棵m阶B树 或为空树,或为满⾜如下特性的m叉树:
1)树中每个结点⾄多有m棵⼦树,即⾄多含有m-1个关键字。
2)若根结点不是终端结点,则⾄少有两棵⼦树。
3)除根结点外的所有⾮叶结点⾄少有m/2 棵⼦树,即⾄少含有 m/2-1个关键字。
4)所有的叶结点都出现在同⼀层次上,并且不带信息(可以视为外部结点或类似于折半查找判定树的查找失 败结点,实际上这些结点不存在,指向这些结点的指针为空)
![img](https://i-blog.csdnimg.cn/blog_migrate/4da33e2ed85c403af25b4cc2d182724e.png)
5)所有非叶结点结构:关键字递增排列,左子树所有数<对应关键字,右子树所有数>对应关键字
m阶B树的核⼼特性:
1) 根节点的⼦树数∈[2, m],关键字数∈[1, m-1]。 其他结点的⼦树数∈[ , m];关键字数∈[ -1, m-1]
2)对任⼀结点,其所有⼦树⾼度都相同
3)关键字的值:⼦树0<关键字1<⼦树1<关键字2<⼦树2<…. (类⽐⼆叉查找树 左<中<右)
B树的高度 算B树的⾼度不包括叶⼦结点(失败结点)
![img](https://i-blog.csdnimg.cn/blog_migrate/b6abb3a5c623ae2dd886bb73f3f434e5.png)
B树的插入
一定插入在最低层的某个非叶结点内
在插⼊key后,若导致原结点关键字数超过上限,则从中间位置( m/2)将其中的关键字分为两部分,左部分包 含的关键字放在原结点中,右部分包含的关键字放到新结点中,中间位置( m/2)的结点插⼊原结点的⽗结点
若此时导致其⽗结点的关键字个数也超过了上限,则继续进⾏这种分裂操作,直⾄这个过程传到根结点为⽌,进 ⽽导致B树⾼度增1
![img](https://i-blog.csdnimg.cn/blog_migrate/983a23c37c7a7d348ee00cc4d5e533ca.png)
B树的删除
结点关键字个数 ⌈m/2⌉ − 1 ≤n≤m-1
若被删除关键字在终端节点,则直接删除该关键字(要注意节点关键字个数是否低于下限 ⌈m/2⌉ − 1)
若被删除关键字在⾮终端节点,则⽤直接前驱或直接后继来替代被删除的关键字
直接前驱:当前关键字左侧指针所指⼦树中“最右下”的元素
直接后继:当前关键字右侧指针所指⼦树中“最左下”的元素
兄弟够借。若被删除关键字所在结点删除前的关键字个数低于下限,且与此结点右(或左)兄弟结 点的关键字个数还很宽裕,则需要调整该结点、右(或左)兄弟结点及其双亲结点(⽗⼦换位法
父子换位法: 兄弟结点中的一个关键字进入父结点,父结点中的一个关键字进入被删结点,然后删 除关键字
本质:要永远保证 ⼦树0<关键字1<⼦树1<关键字2<⼦树2
兄弟不够借。若被删除关键字所在结点删除前的关键字个数低于下限,且此时与该结点相邻的左、右兄弟结 点的关键字个数均=⌈m/2⌉ − 1,则将关键字删除后与左(或右)兄弟结点及双亲结点中的关键字进⾏合并
在合并过程中,双亲结点中的关键字个数会减1。若其双亲结点是根结点且关键字个数减少⾄0(根结点关键 字个数为1时,有2棵⼦树),则直接将根结点删除,合并后的新结点成为根;若双亲结点不是根结点,且关 键字个数减少到 ,则⼜要与它⾃⼰的兄弟结点进⾏调整或合并操作,并重复上述步骤,直⾄符合B 树的要求为⽌
![img](https://i-blog.csdnimg.cn/blog_migrate/781d3f3873312db094a264b8aaa7e98c.png)
7.4.2 B+树
⼀棵m阶的B+树需满⾜下列条件:
1)每个分⽀结点最多有m棵⼦树(孩⼦结点)。
2)⾮叶根结点⾄少有两棵⼦树,其他每个分⽀结点⾄少有 棵⼦树。
3)结点的⼦树个数与关键字个数相等。(与B树的最大区别)
4)所有叶结点包含全部关键字及指向相应记录的指针,叶结点中将关键字按⼤⼩顺序排列,并且相邻叶结点按⼤⼩顺序相互链接起来。
5)所有分⽀结点中仅包含它的各个⼦结点中关键字的最⼤值及指向其⼦结点的指针。
B+树的查找:从最小关键字顺序查找/从根结点多路查找
B+树中,⽆论查找成功与否,最终⼀定都要⾛到最下⾯⼀层结点
![img](https://i-blog.csdnimg.cn/blog_migrate/6eba39d4715c1459c4089f90ae1f568e.png)
7.5 散列表
7.5.1 散列表的基本概念
散列表(Hash Table),⼜称哈希表。是⼀种数据结构,
特点:数据元素的关键字与其 存储地址直接相关
若不同的关键字通过散列函数映射到同⼀个值,则称它们为“同义词”
通过散列函数确定的位置已经存放了其他元素,则称这种情况为“冲突”
处理冲突的⽅法——拉链法(⼜称链接法、链地址法):把所有“同义词”存储在⼀个链表中
7.5.2 散列函数的构造方法
除留余数法 —— H(key) = key % p
P的选取: 不大于m(散列表长)但最接近或等于m的质数
直接定址法 —— H(key) = key 或 H(key) = a*key + b
其中,a和b是常数。这种⽅法计算最简单,且不会产⽣冲突。它适合关键字的分布基本连续的情 况,若关键字分布不连续,空位较多,则会造成存储空间的浪费
数字分析法 —— 选取数码分布较为均匀的若⼲位作为散列地址
设关键字是r进制数(如⼗进制数),⽽r个数码在各位上出现的频率不⼀定相同,可能在某些位 上分布均匀⼀些,每种数码出现的机会均等;⽽在某些位上分布不均匀,只有某⼏种数码经常出 现,此时可选取数码分布较为均匀的若⼲位作为散列地址。这种⽅法适合于已知的关键字集合, 若更换了关键字,则需要重新构造新的散列函数
平⽅取中法——取关键字的平⽅值的中间⼏位作为散列地址
具体取多少位要视实际情况⽽定。这种⽅法得到的散列地址与关键字的每位都有关系,因此使得 散列地址分布⽐较均匀,适⽤于关键字的每位取值都不够均匀或均⼩于散列地址所需的位数
折叠法——关键字分割成位数相同的几部分,取叠加和作为散列地
适用于位数很多且每位上数字分布大致均匀
7.5.3 处理冲突的方法
⽤拉链法(⼜称链接法、链地址法)处理“冲突”:把所有“同义词”存储在⼀个链表中
开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,⼜向它的⾮同义词表项开 放。
其数学递推公式为:Hi = (H(key) + di) % m
i = 0, 1, 2,…, k(k≤m - 1),m表示散列表表⻓;di为增量序列;i 可理解为“第i次发⽣冲突“
①线性探测法—— di = 0, 1, 2, 3, …, m-1;即发⽣冲突时,每次往后探测相邻的下⼀个单元是否为空
线性探测法很容易造成同义词、⾮同义词的“聚集(堆积)”现象,严重影响查找效率
产⽣原因——冲突后再探测⼀定是放在某个连续的位置
②平⽅探测法。当di = 02, 12, -12, 22, -22, …, k2, -k2时,称为平⽅探测法,⼜称⼆次探测法其中k≤m/2
优点: 可以避免出现堆积问题,缺点: 只能探测一半单元
③伪随机序列法。di 是⼀个伪随机序列,如 di= 0, 5, 24, 11, …
![img](https://i-blog.csdnimg.cn/blog_migrate/e91df64b0f0409a774e4659271177689.png)
注意:
-
不能随便物理删除已有元素,会截断其他相同散列地址元素的查找地址
-
可做删除标记,逻辑删除
-
副作用: 多次删除后,表面上散列表很满,其实还有很多位置未用,需定期维护
7.5.4 散列查找及性能分析
查找效率:
取决于: 散列函数,处理冲突的方法,装填因子
装填因子(α): 定义一个表的装满程度 α=表中记录数n/散列表长度m
平均查找长度依赖于α,不直接依赖于n或m
易错点:
K个同义词采用线性探测填入散列表,需要探测K(K+1)/2次
冲突产生的概率与装填因子的大小成正比 越满越容易冲突
不能用随机数函数构造散列函数,无法进行正常的查找
注意点:
ASL成功 查找次数=冲突次数+1
根据散列函数确定一共需要的查找的位置,对每个位置查找直到为空时结束,不为空时用相应的冲突 处理方法再进行查找,为空时也需要比较一次
第八章 排序
8.1 排序的基本概念
排序(Sort),就是重新排列表中的元素,使表中的元素满⾜按关键字有序的过程。
算法的稳定性。若待排序表中有两个元素Ri和Rj,其对应的关键字相同即keyi = keyj,且在排序 前Ri在Rj的前⾯,若使⽤某⼀排序算法排序后,Ri仍然在Rj的前⾯,则称这个排序算法是稳定 的,否则称排序算法是不稳定的。
8.2 插入排序
8.2.1 直接插入排序
算法思想:每次将⼀个待排序的记录按其关键字⼤⼩插⼊到前⾯已排好序的⼦序列中, 直到全部记录插⼊完成
![img](https://i-blog.csdnimg.cn/blog_migrate/4a7721e22d53cf803ea974837ea31464.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/00f2d2445add90018620018715fe96e0.png)
空间复杂度:O(1)
最好时间复杂度—— O(n)
最坏时间复杂度——O(n2)
平均时间复杂度:O(n2)
8.2.2 折半插入排序
先⽤折半查找找到应该插⼊的位置,再移动元素
当 low>high 时折半查找停⽌,应将 [low, i-1] 内的元素全部右移,并将 A[0] 复制到 low 所指位置
当 A[mid]==A[0] 时,为了保证算法的“稳定性”,应继续在 mid 所指位置右边寻找插⼊位置
![img](https://i-blog.csdnimg.cn/blog_migrate/21b7b1fe6fb0e1a60990d1be5b94a283.png)
仅减少了比较元素的次数,移动的次数并未改变
![img](https://i-blog.csdnimg.cn/blog_migrate/f986c9ce03daf71269f377e5eb509835.png)
8.2.3 希尔排序
**希尔排序:**先将待排序表分割成若⼲形如 L[i, i + d, i + 2d,…, i + kd] 的“特殊”⼦表,对各个⼦表 分别进⾏直接插⼊排序。缩⼩增量d,重复上述过程,直到d=1为⽌。
![img](https://i-blog.csdnimg.cn/blog_migrate/95234b89b98c1414d4c0af57a1e21324.png)
空间复杂度:O(1)
时间复杂度:和增量序列 d1, d2, d3… 的选择有关,⽬前⽆法⽤数学⼿段证明确切的时间复杂度
稳定性:不稳定
适⽤性:仅适⽤于顺序表,不适⽤于链表
![img](https://i-blog.csdnimg.cn/blog_migrate/e94a36b262f384778bf7660404d9500b.png)
8.3 交换排序
8.3.1 冒泡排序
**冒泡排序:**从后往前(或从前往后)两两⽐较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序 列⽐较完。
实现步骤:
从最后一个元素开始,两两相邻进行比较,若为逆序,则交换它们
一趟冒泡,结果将最小的元素交换到第一个位置
下一趟冒泡,前一趟确定的最小元素不再参与,待排序列减少一个元素
每趟冒泡的结果是序列中最小元素放到最终位置,最多n-1此完成
![img](https://i-blog.csdnimg.cn/blog_migrate/4174db6e8c66e6154849d469f47528e3.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/822842b8ed3b5af0945ac6657c40ea53.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/a376219bcbcbd3de7977a965afcb16c8.png)
8.3.2 快速排序
更⼩的元素都交换到左边 更⼤的元素都交换到右边
实现步骤:
每次取当前表中第一个元素作为基准pivot(枢轴值)对表进行划分
i指向第一个元素(基准),j指向最后一个元素
先从j开始,从后往前找到第一个比基准小的元素,j指向此元素位置,用此元素替换掉i所指元素
再从i开始,从前往后找到第一个比基准大的元素,i指向此元素位置,用此元素替换掉j所指元素
再次从j开始,循环往复,直到i与j接触停止,将基准值放到接触位置,将序列划分为两块,前面小于基准值,后面大于基准值
分别取两个子序列的第一个元素作为基准值,重复操作
![img](https://i-blog.csdnimg.cn/blog_migrate/8d2f01549c3c5d5c05dfa0a2d76d7568.png)
时间复杂度=O(n*递归层数)
最好时间复杂度=O(nlog2n)
最坏时间复杂度=O(n2)
空间复杂度=O(递归层数)
最好空间复杂度=O(log2n)
最坏空间复杂度=O(n)
快速排序是所有内部排序算法中 平均性能最优的排序算法
稳定性:不稳定
8.4 选择排序
8.4.1 简单选择排序
选择排序:每⼀趟在待排序元素中选取关键字最⼩(或最⼤)的元素加⼊有序⼦序列
简单选择排序: 每⼀趟在待排序元素中选取关键字最⼩的元素加⼊有序⼦序列
![img](https://i-blog.csdnimg.cn/blog_migrate/a3944e198b5584efedf549b7b4e66974.png)
空间复杂度:O(1)
时间复杂度=O(n2)
稳定性:不稳定
适⽤性:既可以⽤于顺序表,也可⽤于链表
![img](https://i-blog.csdnimg.cn/blog_migrate/5534f9529e4548f68685352282f688e8.png)
8.4.2 堆排序
若n个关键字序列L[1…n] 满⾜下⾯某⼀条性质,则称为堆(Heap):
① 若满⾜:L(i)≥L(2i)且L(i)≥L(2i+1) (1 ≤ i ≤n/2)—— ⼤根堆(⼤顶堆)
② 若满⾜:L(i)≤L(2i)且L(i)≤L(2i+1) (1 ≤ i ≤n/2)—— ⼩根堆(⼩顶堆)
![img](https://i-blog.csdnimg.cn/blog_migrate/594512bcf7920750268246cbf2720325.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/9ebdbee1a980ff99fd92704d0ceacff6.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/93f5a8acbb11d11f0d13d8e5ed15196f.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/20d9a0c2b8fc0e22272a50dda686eab6.png)
堆排序的时间复杂度 = O(n) + O(nlog2n) = O(nlog2n)
堆排序的空间复杂度 = O(1)
稳定性: 不稳定
![img](https://i-blog.csdnimg.cn/blog_migrate/37c353d85c961d77b21f80a2172dc012.png)
8.4.3 堆的插入和删除
8.5 归并排序和基数排序
8.5.1 归并排序
归并:把两个或多个已经有序的序列合并成⼀个
![img](https://i-blog.csdnimg.cn/blog_migrate/1f08df8465dba0f25b505614b05e1b35.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/3bbececd36cae4a66668aeaa631017e0.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/05d2579f0cd5b32f20637cb4888060cc.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/9258f0be4a59da039238a9face9897e4.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/683cf0f1bf2a833a800ab629149dd700.png)
8.5.2 基数排序
![img](https://i-blog.csdnimg.cn/blog_migrate/2fa238d687ac412741ba115e664e620a.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/f63dc6f6927d74f7eb4c769812692e6e.png)
空间复杂度 = O®
时间复杂度=O(d(n+r))
稳定性:稳定
基数排序擅⻓解决的问题:
①数据元素的关键字可以⽅便地拆分为 d 组,且 d 较⼩
②每组关键字的取值范围不⼤,即 r 较⼩
③数据元素个数 n 较⼤
![img](https://i-blog.csdnimg.cn/blog_migrate/88728ac53e1987c728d2212223d7b874.png)
8.6 内部排序算法比较与应用
![img](https://i-blog.csdnimg.cn/blog_migrate/45771064efb72a7b62c651c9823257b0.png)
8.7 外部排序
8.7.1 外部排序的基本概念
外部排序:数据元素太多,⽆法⼀次全部读⼊内存进⾏排序
**“归并排序”**要求各个⼦序列有序,每次读⼊ 两个块的内容,进⾏内部排序后写回磁盘
外部排序时间开销=读写外存的时间+内部排序所需时间+内部归并所需时间
![img](https://i-blog.csdnimg.cn/blog_migrate/f7260aa6e86d399f9d6a5479139ec4dc.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/d55e9e362dc4e2327c574ba292188547.png)
![img](https://i-blog.csdnimg.cn/blog_migrate/1a49d8dce11a9134544e67f547056b8c.png)
8.7.2 败者树
k路平衡归并:①最多只能有k个段归并为⼀个; ②每⼀趟归并中,若有 m 个归并段参与归并,则经过这⼀趟处理得到⌈m/k⌉个新的归并段
败者树——可视为⼀棵完全⼆叉树(多 了⼀个头头)。k个叶结点分别是当前 参加⽐较的元素,⾮叶⼦结点⽤来记忆 左右⼦树中的“失败者”,⽽让胜者往上 继续进⾏⽐较,⼀直到根结点
叶结点 当前参加比较的记录
内部结点 记忆左右子树中失败者的序号,让胜者继续向上比较,直到根结点
根结点 当前的最小数/最大数的序号,不是数值本身(胜者)
![img](https://i-blog.csdnimg.cn/blog_migrate/572fdd9a44bac1e8921063fae3042045.png)
利用败者树得到最小值序号后,取出最小值数,在其位置加入下一个关键字,继续比较,构造败者树
使用败者树后,比较次数与m无关,可以增大m来减少归并树的高度
m并不是越大越好 m增大,输入缓冲区增加,其容量减少,内外存交换数据的次数增大
8.7.3 置换选择排序
设初始待排⽂件为FI,初始归并段输出⽂件为FO,内存⼯作区为WA,FO和WA的初始状态为 空,WA可容纳w个记录。置换-选择算法的步骤如下:
1)从FI输⼊w个记录到⼯作区WA。
2)从WA中选出其中关键字取最⼩值的记录,记为MINIMAX记录。
3)将MINIMAX记录输出到FO中去。
4)若FI不空,则从FI输⼊下⼀个记录到WA中。
5)从WA中所有关键字⽐MINIMAX记录的关键字⼤的记录中选出最⼩关键字记录,作为新的 MINIMAX记录。
6)重复3)~5),直⾄在WA中选不出新的MINIMAX记录为⽌,由此得到⼀个初始归并段,输 出⼀个归并段的结束标志到FO中去。
7)重复2)~6),直⾄WA为空。由此得到全部初始归并段。
8.7.4 最佳归并树
思想:让记录少的归并段最先归并,记录多的最晚归并
重要结论:归并过程中的磁盘I/O次数 = 归并树的WPL * 2
要让磁盘I/O次数最少, 就要使归并树WPL最⼩ ——哈夫曼树!
注意:对于k叉归并,若初始归并段的数量⽆法构成严格的 k 叉归并树, 则需要补充⼏个⻓度为 0 的“虚段”,再进⾏ k 叉哈夫曼树的构造。
m叉哈夫曼树:
叶结点 参加归并的一个初始归并段
叶结点的权值 初始归并段中的记录数
叶结点到根结点的路径长度 归并趟数
非叶结点 归并生成的新归并段
归并树的带权路径长度 总读记录数
![img](https://i-blog.csdnimg.cn/blog_migrate/12ca507105ba7381d9cc7fd78c70676c.png)