数据结构与算法
2022/10/24 14:35 数据结构的基本的知识点可以参考我的语雀笔记,语雀上面的知识点介绍的特别浅(当时是根据王道视频课和《大话数据结构》等教材整理的,但那会毕竟刚开始写博客写的比较差);这篇文章主要做一个进阶学习,参考书籍是程洁的《大话数据结构》,在原有笔记的基础上增加、补充了一些缺失的概念(这篇博客可以看作是对语雀的补充和拓展);
第一章 绪论
首先声明,这篇博客主要代码都是C语言(因为参考书用的就是C语言),部分拓展知识点可能会使用到C++代码,这里我们先复习一下C语言中常用的结构体定义
1 |
|
1.数据和数据结构
数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别并输入给计算机处理的符号集合。数据不仅仅只包括整型等数值类型,还包括字符以及声音等非数值类型。
数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。数据元素也被称为记录
数据项:一个数据元素可以由若干个数据项组成,数据项是数据不可分割的最小单位
数据对象:是性质相同的数据元素的集合,是数据的子集。我们常称数据对象为数据(在不产生混淆的前提下)
结构:指各个组成部分相互搭配和排列的方式
数据结构:是一种相互之间存在一种或多种特定关系的数据元素的集合。我们把数据结构分为逻辑结构和物理结构
逻辑结构:指数据对象中数据元素之间的相互关系,可分为以下四种
- 集合结构:集合结构中的数据元素除了属于同一个集合外,没有任何关系
- 线性结构:线性结构中的数据元素之间是一对一的关系
- 树形结构:树型结构中的数据元素之间存在一种一对多的层次关系
- 图形结构:图形结构的数据元素是多对多的关系
物理结构:也称为存储结构,是指数据的逻辑结构在计算机中的存储形式,通俗来讲就是如何把数据元素存储到计算机的存储器中(存储器是针对内存而言,对于外存来说通常用文件结构来描述数据组织)。数据元素的存储结构形式有两种:顺序存储和链式存储
- 顺序存储结构:是把数据元素放在地址连续的存储单元中,其数据间的逻辑关系和物理关系是一致的
- 链式存储结构:是把数据元素存储在任意的存储单元中,这组存储单元可以是连续的也可以是不连续的,数据元素的存储关系并不能反映其逻辑关系
2.数据类型和抽象数据类型
数据类型:是指一组性质相同的值的集合以及定义在此集合上的一些操作的总称,在C语言中按照取值不同数据结构可以分为两类
- 原子结构:不可再分的基本类型,包括整型、字符型等
- 结构类型:由若干类型组合而成,可以分解,如整型数组、结构体等
抽象:是指“抽取出事物具有的普遍性的本质”这一行为,它隐藏了繁杂的细节,只保留实现目标必须的信息。对已有的数据类型进行抽象就可以得到抽象数据类型
抽象数据类型(ADT):是指一个数学模型以及定义在该模型上的一组操作。一个抽象数据类型定义了:一个数据对象、数据对象中各数据元素之间的关系以及对数据元素的操作。ADT体现了程序设计中对问题的分解、抽象和信息隐藏的特性
抽象数据类型的标准格式如下
1 |
|
3.算法和算法复杂度
算法:是“解决特定问题求解步骤的描述”,简单来说就是算法描述了解决问题的方法。在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。算法具有以下五个基本特征:
- 输入&输出:算法具有零个或多个输入,至少有一个或多个输出
- 有穷性:算法在执行有限的步骤之后会自动结束而不会出现无限死循环,且每一个步骤在可接受的时间内完成
- 确定性:算法的每一个步骤都具有确定的含义,不会出现二义性
- 可行性:算法的每一步都能够通过执行有限次数完成
函数的渐进增长:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是大于g(n),则称f(n)的渐进增长快于g(n)
算法的时间复杂度:进行算法分析时,语句总的执行次数f(n)是关于问题规模的n的函数,随着问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐进时间复杂度,简称为时间复杂度。记作T(n)=O(f(n)),我们称之为大O记法,由f(n)推到得到T(n)的方法如下
用常数1取代语句执行次数中所有的加法常数(这是为了避免如常数阶一样根本没有最高阶)
只保留最高阶的项
如果最高阶的项存在且不是1,则将常数项除去
常见的时间复杂度大小关系如下:
O(1)<O(logn)<O(n)<O(nlogn)<O(n^2^)<O(n^3^)<O(2^n^)<O(n!)<O(n^n^)
第二章 线性表
1.线性表的定义
线性表:零个或多个数据元素的有限序列
- 元素之间存在顺序,这里的顺序是指若元素存在多个则第一个元素无前驱,最后一个元素无后继,其他每个元素都有且仅有一个前驱和后继;
- 线性表强调有限,即元素的个数有限;
2.线性表的ADT
1 |
|
关于线性表更复杂的操作,可以由上述基本操作的组合来实现
3.线性表的物理结构
3.1 顺序存储结构
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素
- 可以使用C语言的一维数组来实现线性表的顺序存储结构,把第一个元素存储在下标为0的位置中,接着把线性表相邻的元素存储在数组中相邻的位置
1 |
|
(1)顺序表的时间复杂度:
- 对每个线性表位置的存入或取出数据对于计算机来说都是相等的时间,即存取时间性能为O(1),我们将具有这一特点的存储结构称为随机存取结构
- 线性表的顺序存储结构在存取数据不管对哪个位置时间复杂度都是O(1),插入或删除时时间复杂度都是O(n)
(2)顺序表的特点:
- 无需为表示表中元素之间的逻辑关系增加额外的存储空间
- 可以快速的存取表中任意位置的元素
- 插入和删除需要移动大量元素
- 当线性表长度变化较大时难以确定存储空间(数组)的容量
- 造成存储空间的“碎片化”(因为链表的存储空间是随机选取某一块连续空间)
3.2 链式存储结构
3.2.1 单链表
头指针和头结点:
- 头指针指向链表的第一个结点,若存在头结点则指向头结点,若不存在头结点则指向第一个结点
- 常用头指针冠以链表的名字,头指针是链表的必要元素
- 头结点不一定是链表必须要的元素(引入头结点为了方便单链表的特殊操作,保证在表头插入或者删除第一个结点与其他结点操作相同.保持了单链表操作的统一性)
1 |
|
单链表的时间复杂度:
- 查找、存取的时间复杂度为O(n)
- 插入删除的时间复杂度为O(1)
3.2.2 静态链表
当语言没有指针功能如Basic,就只能使用数组来代替指针描述单链表,称为静态链表
1 |
|
静态链表的特点:
在插入和删除式只需要修改游标不需要移动元素,改进了顺序表中插入和删除操作需要移动大量元素的缺点
失去了顺序表随机存取的特性,且与顺序表一样具有空间无法拓展的缺点
3.2.3 循环链表
将单链表的终端结点的指针端由空指针指向头结点,使得单链表形成一个环,也称为单循环链表
3.2.4 双向链表
1 |
|
第三章 栈
1.栈的定义
栈:限定仅在表尾进行插入和删除操作的线性表
- 把允许插入和删除的一端称为栈顶,另一端称为栈底,不含任何元素的栈称为空栈
- 栈对线性表的插入和删除的位置进行了限制,但是没有对元素进出的时间进行限制,只要保证式栈顶元素出栈即可
2.栈的ADT
1 |
|
3.栈的物理结构
3.1 顺序存储结构
对于栈这种线性表,使用数组来实现就必须考虑选择哪段作为栈顶和栈底——选取下标为0的一端作为栈底,这样整个数组的受元素的变化影响最小
3.1.1 顺序栈
1 |
|
3.1.2 共享栈
对于只有一个栈,我们只能选择一个合适的栈大小避免出现元素溢出的情况;对于两个相同类型的栈,我们可以使用共享栈的方式最大限度利用两个栈的空间——栈顶相连,令其中一个栈底下标为0,另外一个栈底下标为n-1。
当栈空时即top1=-1或者top2=n;当栈满时即top1+1=top2
1 |
|
3.2 链式存储结构
- 将单链表的头指针和栈顶指针合并
- 对于链栈来说通常不需要头结点(因为链表在插入删除时有头部和其他部位的操作差别,引入头结点来统一操作,使对链表第一个位置上的操作和其他位置上的操作相同,不用特殊处理。而链栈只在头部插入删除,所以不必要用头结点)
- 链栈为空表示为top=NULL
1 |
|
第四章 队列
1.队列的定义
队列:只允许在一端进行插入操作,而在另一端进行删除的线性表
- 队列是一种先进先出的线性表,允许插入的一端称为队尾,允许删除的一端称为队头
2.队列的ADT
1 |
|
3.队列的物理结构
3.1 顺序存储结构
实现队列的顺序存储结构和实现顺序表的方式完全相同(故此处不展示其代码),数组下标为0的一端是队头
- 入队列就是在队尾追加一个元素,不需要移动任何元素,时间复杂度为O(1)
- 出队列是使队头元素出队(下标为0的队列元素),这意味着需要将队列中所有的元素向前移动以保证数组下标为0的位置不为空,时间复杂度为O(n);假如去除限制条件——队头一定要在下标为0的位置
3.1.1 循环队列
引入两个指针(称为指针实际上不准确,指针指向的是内存地址,这个指向的是数组位置),front指向队头元素,rear指向队尾元素的下一个位置(而不是队尾元素)
把头尾相接的顺序存储结构称为循环队列
当
front==rear
如何区分队列是空还是满- 设置标志变量flag,当
flag==rear且flag=0
时队列为空,当flag==rear且flag=1
时队列满 - 当队列空时条件为
front==rear
,当队列满时我们认为实际上队列还剩一个元素空间;判断队列满的条件为(rear+1)%QueueSize==front
- 设置标志变量flag,当
通用计算队列长度公式
(rear-front+QueueSize)%QueueSize
1 |
|
3.2 链式存储结构
队列的链式存储结构实际上就是单链表,只不过它只能尾进头出;为了操作方便,将队头指针指向链队列的头结点,队尾指针指向终端结点;空队列时front和rear都指向头结点
1 |
|
第五章 串
1.串的定义
串:是由零个和多个字符组成的有限序列,又称为字符串
- 一般将串记为s=”a1a2a3…an”(n>=0),其中s是串的名称,双引号括起来的字符序列是串的值,值可以是字母、数字或其他字符,i就是该字符在串中的位置,n称为串的长度
- 串的逻辑结构和线性表很相似,但是串中的元素都是字符。线性表更关注的是单个元素的操作,而串中更多的是针对子串(而不是单个字符)的操作
2.串的ADT
1 |
|
3.串的物理结构
3.1 顺序存储结构
用一组地址连续的存储单元来存储串中的字符序列,一般使用定长数组来定义
3.2 链式存储结构
串的链式存储结构与线性表类似,但是因为串结构的特殊性(结构中的每个元素数据是一个字符),假如使用一个结点存储一个字符会造成很大的浪费,故考虑一个结点存放多个字符。
4.模式匹配算法
4.1 朴素模式匹配算法
通常将子串的定位操作称为串的模式匹配,朴素的模式匹配算法就是将主串和模式串的字符一个个对比,如果不同则模式串向后移动一位,递归进行上述操作。
4.2 KMP模式匹配算法
KMP的主要思想是:当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了,因此记录已经匹配的文本内容是KMP的重点也是next数组需要做的事;
i值表示当前主串的第i个字符正在进行匹配,j值表示当前模式串的第j个字符正在进行匹配
next数组表示假如此时匹配失败,则当前模式串需要从第next[j]个字符开始重新匹配;主串的i值在此过程中保持不变,匹配到哪个字符就保持,待会还是从主串的这个字符开始进行匹配
把模式串各个位置的指针跳转位置定义为一个数组next,next数组的长度就是模式串的长度
- j=1时对应的变化值为
next[0]=0
- j=i时,写出模式串的第1个到第i个字符串
- 若该字符串中前缀字符串x与后缀字符串相等y,则
next[i]=len(x)
- 若该字符串中没有和前缀字符串相等的后缀,则
next[i]=0
- 若该字符串中前缀字符串x与后缀字符串相等y,则
然而next数组并不是最好的解决办法,我们使用nextval数组对其进行改进(详见书本p144)
很多人可能看了上面的简介还是觉得云里雾里,所以这里我们分模块讲解KMP,参考链接代码随想录 (programmercarl.com);
4.2.1 前缀表的作用
next数组实际就是一个前缀表;
前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配;
下面我们举个例子,在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf;
第一次匹配的时候文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配;
如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配;
前缀表:记录下标为i之前(包括i)的模式串中有多大长度的相同前后缀
4.2.2 最长相等前后缀
字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;
字符串的后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串;
字符串a的最长相等前后缀为0,字符串aa的最长相等前后缀为1,字符串aaa的最长相等前后缀为2…(前缀表要求的就是相同长度的前后缀)
4.2.3 计算前缀表
前缀表的计算非常简单,只需要记住一句话“长度为i的子串,其最长相同前后缀的长度就是对应前缀表位置i-1的元素”
例如长度为前4个字符的子串aaba
,最长相同前后缀的长度为1,长度为前5个字符的子串aabaa
,最长相同前后缀的长度为2,长度为前6个字符的子串aabaaf
,最长相同前后缀的长度为0…
4.2.4 前缀表的使用
有了前缀表,我们就可以借助前缀表判断当字符不匹配的时候指针应该移动的位置
我们要看不匹配的模式串的位置的前一个字符的前缀表的数值是多少,本例中f的前一个字符的前缀表的数值是2,所以将模式串的指针移动到下标为2的位置继续匹配而不需要回溯文本串和模式串;
4.2.5 next数组和前缀表
实际上next数组就是前缀表,但是next数组是前缀表的具体实现,这意味着next数组可能需要在前缀表的元素统一减1(当然不变或者统一加1都是可以的)
前缀表减1实现next数组
1 |
|
前缀表实现next数组
1 |
|
当然具体选择哪种实现就是使用者自己的习惯问题,需要注意的是不同的next数组对应的匹配代码也是有差距的,个人习惯更偏向于直接使用前缀表实现匹配
1 |
|
第六章 树
1.树的定义
树是n(n>=0)个结点的有限集。n=0时称为空树。
n>0时,子树的个数没有限制但是子树不能相交
树的结点包含一个数据元素以及若干指向其子树的分支,结点拥有的子树的分支数称为结点的度,树的度是指树的结点的度的最大值
结点的分类
- 度为0的结点称为叶结点
- 度不为0的结点称为分支结点
结点的层次从根开始定义,根为第一层,根的孩子为第二层…树中结点的最大层次称为树的深度或高度
有序树是指子树从左至右有次序不能互换,否则为无序树
森林是互不相交的树的集合(注意不是子树相交形成树)
2.树的ADT
1 |
|
3.树的物理结构
简单的顺序存储结构不能满足树的实现要求(但顺序结构确实也能适用于图和树),结合顺序存储和链式存储的特点(借鉴静态链表、单链表等),可以得到下面三种表示方法
3.1 双亲表示法
静态链表
这样的存储结构,可以根据结点的parent指针很容易的找到它的双亲结点,所用时间复杂度为O(1);然而要找到孩子结点则需要遍历该结构,或者增加结点最左边孩子(firstchild)域;假如需要找到兄弟结点则可以在此基础上增加一个右兄弟域
1 |
|
3.2 孩子表示法
顺序表和单链表的组合(书本p160)
这种结构对于查找孩子和兄弟都很方便,但是对于查找双亲只能遍历;可以考虑把双亲表示法和孩子表示法综合(书本p161)
1 |
|
3.3 孩子兄弟表示法
单链表(书本p162)
这种表示法的最大好处就是将复杂的树变为了一棵二叉树,就可以充分的利用二叉树的特性和算法来进行处理了
1 |
|
4.二叉树
4.1 二叉树的特点
- 每个结点最多有两棵子树(可以没有子树或者有一棵子树),所以二叉树中不存在度大于2的结点
- 左子树和右子树是有顺序的,次数不能颠倒
- 即使树中只有一棵子树,也要区分是左子树还是右子树(相当于左手和右手不能一概而论)
4.2 二叉树的性质
- 在二叉树的第i层上至多有2^(i-1)个结点
…(详见书本p169)
4.3 二叉树的物理结构
4.3.1 顺序存储结构
前面提到的树无法使用简单的顺序存储结构实现,但是二叉树是一种特殊的树,所以可以使用顺序结构来实现
对于完全二叉树来说,因为其层序编号可以反映逻辑关系,所以可以直接使用一维数组存储二叉树中的结点;对于一般的二叉树,同样可以采用对其进行编号的方式,不存在的结点在数组中存入NULL即可。
考虑到对于一棵斜树如果采用顺序存储的话会导致浪费的空间过多,所以顺序存储结构一般只用于完全二叉树。
4.3.2 链式存储结构
使用顺序存储结构的适用性不强,我们考虑使用链式存储结构。二叉链表具有一个数据域和两个指针域,分别存放左孩子和右孩子的指针
1 |
|
4.4 二叉树的遍历
(1)前序遍历:先访问根节点,然后前序遍历左子树,接着前序遍历右子树(遍历树,访问结点)
(2)中序遍历:先中序遍历根节点的左子树,然后访问根节点,最后中序遍历右子树
(3)后序遍历:先后序遍历根节点的左子树,然后后序遍历根节点的右子树,最后访问根节点(其实用结点比较准确)
1 |
|
(4)层序遍历:从上到下,从左至右逐个对结点进行访问
1 |
|
4.5 树、森林和二叉树的转换
(1)树转化为二叉树
- 在所有的兄弟结点之间加一条线
- 对树中每一个结点,只保留它与第一个孩子结点的连线,删除它与其他孩子结点之间的连线
- 旋转整棵树使之层次分明
(2)森林转化为二叉树
- 先把每棵树转化为二叉树
- 将森林中的每一棵树都理解为兄弟,于是可以按照兄弟的处理办法来操作已经转化好的二叉树。保持第一棵二叉树不动,依次把后面的每一棵二叉树的根节点作为前一棵二叉树根节点的右孩子,连线即可
(3)二叉树转化为树
- 若某结点的左孩子节点存在,则将该结点与其“左孩子的所有右后代”都作为该节点的孩子进行连线
- 删除原二叉树中所有结点与其右孩子的连线
(4)二叉树转化为森林
- 只要二叉树的根结点有右孩子则就是森林
- 从根结点开始若右孩子存在则去除根节点与右孩子的连线,依次递归
- 再将分离得到的二叉树都转化为树即可
5.哈夫曼树
哈夫曼树和哈夫曼编码都是二叉树的应用,常用于数据压缩的编码解码
构建方法:书本p204页
第七章 图
1.图的定义
图是由顶点的有穷非空集合和顶点之间的边的集合组成,通常表示为G(V,E),V是图G中顶点的集合,E是图G中边的集合
- 线性表中的数据元素称为元素,树中的数据元素称为结点,图中的数据元素称为顶点Vertex
- 线性表中可以没有数据元素(空表),树中可以没有结点(空树),但是在图结构中不允许没有顶点
- 图中任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,顶点集不能为空,但是边集可以为空
(更多详细有关图的介绍可以参考离散数学中的笔记)
2.图的ADT
1 |
|
3.图的物理结构
因为图中任意两个顶点之间都可能存在联系,所以无法用数据元素在内存中的物理位置来表示元素之间的关系,即图不可能用简单的顺序存储结构表示;而使用多重链表也会导致因为度数不同而浪费空间
3.1 邻接矩阵
图是由顶点和边两部分组成,合在一起存储较为困难,能否分为两个结构来分别存储呢?
顶点部分主次大小,所以使用一个一维数组来存储;
边是顶点与顶点之间的关系,无法用一维搞定,所以考虑使用二维数组来存储;
1 |
|
3.2 邻接表
邻接矩阵是不错的图存储结构,但是对于边数相对顶点较少的图(稀疏图),这种结构会浪费大量存储空间;
借鉴树结构中的孩子表示法将数组和链表相结合的存储方法,我们在图中得到邻接表
- 图中顶点用一个一维数组存储(使用单链表来存储当然也可以),同时在该顶点数组中每个数据元素还需要存储指向第一个邻接点的指针以便于查找该顶点的边信息
- 图中每个顶点vi的所有邻接点构成一个线性表,因为邻接点的个数不定所以用单链表存储(有向图则使用弧尾作为顶点来存储边表)
1 |
|
3.3 十字链表
对于有向图来说邻接表是有缺陷的,如果关心出度问题则要了解入度只能遍历整个图,同理,使用逆邻接表就只能遍历才能了解到出度情况
- 重新定义顶点表的结点结构
- 重新定义边表的顶点结构(详见书本p233)
3.4 邻接多重表
十字链表是针对有向图的优化,对于无向图是否需要有优化的地方呢?
假如在无向图中关注的是顶点则邻接表是个不错的选择,但是如果我们关注边的操作即意味着需要找到这条边的两个边表结点进行操作,比如说需要删除边(v0,v2)则需要对邻接表结构中的边表的相应结点进行删除
4.图的遍历
(详见p237)
4.1 深度优先遍历
1 |
|
4.1.1 复杂度分析
- 邻接矩阵:O( |v|2) //一共需要访问v个结点,而访问v个结点都需要O(v)的时间
- 邻接表:O( |v| + |E| ) //访问v个顶点要O(v)的时间,访问各个邻接点要O(E)的时间
4.1.2 深度优先生成树
- 由深度优先遍历确定
- 生成树不唯一
- 深度优先遍历非联通图,优先得到生成森林
4.2 广度优先遍历
1 |
|
4.2.1 复杂度分析
- 邻接矩阵:O( |v|2)
- 邻接表:O( |v| + |E| )
4.2.2 广度优先生成树
- 由广度优先遍历确定
- 由于邻接表存储图不一致,由此确定的生成树不一致
- 遍历非联通图可以得到广度优先生成森林
其实这种就是无效笔记,做了和没做一样根本起不到提示的作用
5.最小生成树
将构造连通图的最小代价生成树称为最小生成树(详见p245)
最小生成树的几个特性:
- 最小生成可能由多个,但权值之和只有一个
- 边 = 顶点数 - 1
- 如果一个联通图本身是树,其最小生成树就是其本身
- 只有联通图才能生成树,非联通图只能生成森林
5.1 Prim算法
从某个点开始,每次将代价最小的顶点纳入生成树,生成树不唯一,时间复杂度:O(v^2^)
5.2 Kruskal算法
每次选择权值最小的边,使两头联通,原本已经联通的就不用选,时间复杂度: O(ElogE)
6.最短路径
对于网图来说,最短路径是指两顶点之间经过的边权值之和最少的路径,我们称起点为源点,最后一个顶点为终点(详见p257)
6.1 Dijistra算法
注意: 不适用于带负权图,时间复杂度为 O(v^2^)
6.2 Floyd算法
注意: 适用于于负权图,但不适用于带有”负权回路”的图,时间复杂度为 O(v^3^)
6.3 广度优先遍历(无权图)
1 |
|
7.有向无环图(DAG图)
8.拓扑排序
AOV网:顶点表示活动,边上没有权值
- 拓扑排序的方法:
- 从AOV网中选择一个没有前驱的顶点输出
- 从网中删除改顶点和所有以它为起点的有向边
- 重复1和2直到AOV网为空,或者当前网中不存在前驱(即非连通图)
- 拓扑排序的性质:
- 不唯一
- 若图中有环,则不存在拓扑排序
- 拓扑排序的算法:
1 |
|
9.关键路径
AOE网:用边表示活动的网络,用顶点表示事件
求关键路径步骤:
- 求所有事件的最早发生时间(ve) : 即求源点到目的顶点的最长路径
- 求所有时间的最迟发生时间(vl) : 不推迟整个工程的情况下,该事件最迟应该发生的时间
- 求所有活动的最早发生时间(e) : 活动弧的起点最早应该发生的时间
- 求所有活动的最迟发生时间(l) : 活动弧的终点所表示的事件的最迟时间与该活动所需的时间差.
- 求d = l - e , d = 0即关键活动
关键活动,关键路径的特性:
- 关键活动耗时增加,整个工程工期增加
- 缩短关键活动时间,缩短整个工程时间
- 当缩短到一定程度,关键活动可能变成非关键活动