图(大话数据结构略读)

目录

图的定义

各种图定义

图的顶点与边间关系

连通图相关术语 

图的存储结构 

邻接矩阵

无向图的邻接矩阵

有向图的邻接矩阵

有权图邻接矩阵

邻接矩阵的代码表现

邻接表

十字链表 

邻接多重表 

边集数组 

 图的遍历

深度优先遍历

邻接矩阵方式的代码

邻接表方式的代码 

​对比两种存储结构的优劣

广度优先遍历

邻接矩阵的广度优先遍历

邻接表的广度优先遍历

深度优先遍历与广度优先遍历的优劣

最小生成树

普利姆算法 

普利姆算法的运行逻辑

普里姆算法的代码实现

克鲁斯卡尔算法

克鲁斯卡尔算法的三个关键点 

克鲁斯卡尔算法的运行逻辑

克鲁斯卡尔代码实现

两套算法的优劣对比

最短路径

迪杰斯特拉算法

算法运行流程

算法代码实现

弗洛伊德算法 

 算法概念了解

 算法代码实现

两种算法的优劣

拓扑排序

拓扑排序算法

拓扑排序流程

拓扑排序代码实现

关键路径

四个关键参数

算法代码实现


图的定义

        图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。

图的定义,需要注意几点:

        1. 图中数据元素,我们称之为顶点。

        2. 与线性表中可以没有数据,树中可无结点不同,图结构中,不允许没有顶点。在定义中,若V是顶点的集合,则强调了顶点集合V有穷非空。

        3.图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示。

各种图定义

        无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边,用无序偶对(vi,vj)来表示。若图中任意两个顶点之间的边都是无向边,则称该图为无向图。如下所示:

        有向边:若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧,用有序偶<vi,vj>来表示,vi称为弧尾,vj称为弧头。若任意两个顶点之间的边都是有向边,则称该图为有向图。如下所示:

        请注意,无向边用“()”表示,而有向边用“<>”表示。

        在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。

        以上两者就不属于简单图。

        而在无向图中,若任意两个顶点之间都存在边,则称该图为无向完全图。如下:

        

        而在有向图中,若任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。如下:

        有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的的数叫做权。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网,如下

        

图的顶点与边间关系

        以如下无向图为例:

        顶点A的度为3,此图的边数为5,此图的各个顶点度的和为3+2+2+3=10。经推敲后发现,边数其实是各顶点度数和的一半。

       

        以如下有向图为例:

        顶点A的入度为2(从B到A的弧,从C到A的弧),出度是1(从A到D的弧),所以顶点A的度是2+1=3;此有向图的弧为4,而各顶点的入度和为2+1+0+1=4;各顶点的出度和为2+1+1+0=4;

        无向图中,顶点B到顶点D四种不同路径如下:

        有向图中,路径也是有向的,如下,顶点B到D有两种路径,而顶点A到B,就不存在路径。

        树中根结点到任意结点的路径是唯一的,但是图中顶点与顶点之间的路径却是不唯一的。

        路径的长度是路径上的边或弧的数目。

        就拿上图来举例,左侧的路径长度为2,右侧路径长度为3。

        第一个顶点到最后一个顶点相同的路径称为回路或环。序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。

        如上所示,因第一个顶点和最后一个顶点都是B,且C、D、A没重复出现。因此是一个简单环;而右侧的环,由于顶点C的重复,它就不是简单环了。

连通图相关术语 

 无向图篇

        在无向图G中,如果顶点A到顶点D有路径,则称顶点A和D是连通的。如果对于图中任意两个顶点都是连通的,则称G是连通图。如图下:

        左侧图中,其他顶点与E,F无路径,因此不能算作连通图;而且右侧图中,顶点A、B、C、D都是连通的,所以它本身是连通图。

        无向图中的极大连通子图称为连通分量。注意连通分量的概念,其强调:

        1. 要是子图

        2. 子图要是连通的

        3. 连通子图含有极大顶点数

        4. 具有极大顶点数的连通子图包含依附于这些顶点的所有边。

        上左侧图虽然是一个无向非连通图,但它有两个连通分量,即下图所示:

        而下图为什么不是其连通分量呢?

        因为他不满足连通子图的极大顶点数,因此它不是图1的无向图的连通分量。

有向图篇

        在有向图中,每一对顶点都存在路径,则称其是强连通图。有向图中的极大强连通子图称作有向图的强连通分量。

        

        左图并非强连通图,因为顶点A到顶点D存在路径,而D到A就不存在。右图就是强连通图,而且显然图右是图左的极大强连通子图,即是它的强连通分量。

        连通图的生成树定义

        一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。

        误点:此处对环的理解有误,以为必须是封闭的形状才是环,环指的是至少有一个路径,使得第一个顶点和最后一个顶点相同。随便添加一条边后,都可以至少有一条路径满足环的要求。

        从上面可知,图1是一个普通图,但显然它不是生成树,当去掉两条构成环的边后,如图2或图3,此时它们都是一棵生成树;由此得出,若一个图有n个顶点和小于n-1条边,则是非连通图,若是它多余n-1边条,则必定构成一个环;比如图2和图3,随便加哪两顶点的边都将构成环;

有向图如何转变为有向树

        如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一颗有向树。一个有向图的生成森林由若干棵不相交的有向树的弧。

        

        如上,有向图去掉一些弧后,它可以分解为两棵有向树。

图的存储结构 

邻接矩阵

        考虑到图是由顶点和边两部分存储,合在一起比较困难,所以就很自然地考虑到分两个结构来分别存储。顶点不分大小、主次,所以用一个一维数组来存储很不错;而边或弧由于是顶点与顶点之间的关系,就考虑用二维数组来存储。

        图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组来存储图中顶点信息,一个二维数组来存储图中的边或弧的信息。

无向图的邻接矩阵

      

         无向图的邻接矩阵特点是对角线对称。判断vi到vj是否存在弧,只需要查找矩阵中是否为1即可,求vi的所有邻接点就是将矩阵第i行全部扫一遍。

有向图的邻接矩阵

        

         有向图讲究入度与出度,顶点v1的入度为2,出度为1。

有权图邻接矩阵

        

        为什么用无穷大来表示顶点之间不存在关系呢?权值有可能是0或负数,所以必须要用一个不可能的值来代表不存在。

邻接矩阵的代码表现

  构建邻接矩阵的存储结构

构建无向网图 

        以上代码的是对无向图的邻接矩阵的表现,只不过此时的顶点之间的边有权值。

邻接表

        将数组与链表相结合的存储方法称为邻接表;图中顶点用一个一维数组存储,顶点数组中,每个数据元素还需要存储第一个邻接点的指针,以便于查找该顶点的边信息。

        如下图就是一个无向图的邻接表:

        

        data是数据域,存储顶点信息,firstedge是指针域,指向边表第一个结点;边表结点由adjvex和next两个域组成,adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针。比如v1与v0、v2互为邻接点,则在v1的边表中,adjvex分别为v0的0和v2的2。

        若是有向图的话,因为有向图有方向,我们是以顶点为弧尾来存储边表,这样可以得到每个顶点的出度。但也有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表。

        有向图的邻接表与逆邻接表如下:

         而对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可。如下图所示:

 

邻接表的结点定义 

       

邻接表的构建

 

十字链表 

        十字链表是邻接表和逆邻接表结合而成的,这样可以容易找到顶点的出度和入度。而且它除了结构复杂一点外,时间复杂度与邻接表相同。

        以下方十字链表为例:

我们首先需要得知,顶点表结点结构如下:

        firstin是表示该顶点的出度边表的指针,firstout是该顶点的入度边表的指针。

边表结点结构如下:

        第一个是弧起点的下标,第二个是其出度的下标,第三个是入度边表的指针域,第四个是出度边表的指针域

顶点出度链表图解

顶点入度链表图解 

  

邻接多重表 

        如果我们在无向图的应用中,关注的是顶点,那么邻接表是不错的选择,但如果我们更关注边的操作——比如删除一条边等操作,那就需要找到这条边的两个边表结点进行操作。

        仿造十字链表,对结构进行些许改造,以下图为例

作出邻接多重表如下:

对单个结点链接进行分析 

 第二格是用来存储连接结点指针域,如果第一格相同,则进行结点连接。

而后部分则是第三格相同,则链接在一起。

边集数组 

        边集数组由两个一维数组组成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标、终点下标和权组成。

        在边集数组中,查找一个顶点的度需要扫描整个边数组,所以它更适合对边依次进行的操作,而不适合对顶点的操作。

以下图为例

        

 图的遍历

        从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程叫图的遍历。

深度优先遍历

        

        首先从顶点A开始,做上表示走过的记号后,走向B、F,我们给自己定一个原则,在未碰到重复顶点时,始终走向右边,注意不要被图中内容误导,不是根据图的右边来定义,每一个分支顶点都可以放右边,只是注意后续分支影响,该分支顶点也可以自己定义后续分支顶点谁在右边。

        假设B为右边顶点,一直遍历到F,到F时再往右走到了A顶点,此时发现A顶点已被标记,回到F顶点,此时A已经被标记过了,于是检查另一分支顶点G,G中B、D已走过,然后又到新的分支顶点H,至此H的分支顶点时D、E,都被标记过,因此一直从H返回到D,在返回途中也会查找每个顶点的分支顶点是否有新的——至此在D中查找到了I。

        直到回到顶点A,你这才完成遍历。

邻接矩阵方式的代码

        

        以上图为例,我们逐一推敲邻接矩阵的深度优先遍历。我们需要一个辅助数组visited,它的作用是记录访问过的顶点。

         图中流程大致如下:遍历数组表,当数组表Graph[0][1]为1时,visit[1]变为true,这代表着它已被访问;随之数组表由Graph[1][0]开始,直到再次找到表中为1的数值,重复上方操作。

邻接矩阵的深度优先递归算法   

    

如果邻接矩阵为非连通图

邻接表方式的代码 

      用下图来推敲邻接表的深度优先遍历       

         当从顶点表开始进行遍历,访问其边表顶点。若是未被访问过,则进入该顶点的顶点表再次进行访问。确定是否该顶点被访问过,也是使用visit辅助数组

邻接表深度遍历代码如下:       

非连通图的遍历代码 

       
   
对比两种存储结构的优劣

        对于同样n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,时间复杂度为O(n^{2});而邻接表则是O(n+e);邻接表在点多变少的稀疏图来说,邻接表结构大大占优。

        对有向图来说,它只是对通道存在可行不可行,算法上完全没有变化,是可以通用的。

广度优先遍历

        如果说图的深度优先遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历了,如下图:

       我们将图左稍稍变形,变形原则是顶点A放置在最上第一层,让与它有边的顶点B、F为第二层,再让与B、F有边的顶点C、I、G、E为第三层,再让这四个顶点有边的D、H放在第四层。

邻接矩阵的广度优先遍历

        以下图开始为例进行推敲,假设以2为顶点开始遍历。

        

         

        因为顶点是根据顶点的分支顶点来遍历,不仅需要一个数组记录哪些顶点被访问过,还需要一个阵列记录当前顶点哪些分支顶点等待访问。

邻接矩阵的广度优先遍历代码如下:

        执行第一个顶点的遍历

执行第一个顶点的分支顶点的遍历 

       

邻接表的广度优先遍历

广度优先遍历代码如下 

深度优先遍历与广度优先遍历的优劣

        两种遍历方式在时间复杂度上一样的,不同之处仅仅在于对顶点访问的顺序不同;遍历的目的是为了寻找合适的顶点,深度优先更适合目标比较明确的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。

最小生成树

        

        以上是带权值的图,即网结构,我们把构造连通网的最小代价生成树称为最小生成树 

普利姆算法 

        从顶点出发选择权值最小的两条边,然后在两个顶点集合中选择权值最小的边。两个顶点结合分别是已选顶点的集合,另一个是未选顶点结合。

        这两个集合中的顶点连接选择一条权值最小的边进行连接,同时将这条边的顶点移动到已选顶点集合中。所以我们需要一个数组域adjvex用于存储连接顶点的下标,一个lowcost数组用来存储它的权值。 

普利姆算法的运行逻辑

以下图为例开始推敲:

        

        从v0顶点开始,而顶点集与adjvex数组和lowcost数组关系如下:

        而如果顶点是从v3开始的话,顶点集 与adjvex数组和lowcost数组关系如下:

        可以发现规律,起始顶点的adjvex都是它自己的下标,而lowcost则是它邻接矩阵的当行数据。而当lowcost=0时,则说明该顶点已进入已选顶点集合中。

        继续以起始顶点v0开始分析,它的最小权值边为顶点v5的连接边,选中顶点v5,表格更新如下:

        此时已选顶点集合中为{v0,v5},现在是从这两个顶点中选择与未选顶点的最小权值边。

        比较v0与v5与未选顶点的每条边的权值

        得出表格更新如下

        此时顶点v2与顶点v3同样身为最小权值,因为顺序,优先访问顶点v2,v2的adjvex变为5,v2的lowcost变为0,表格更新如下: 

 

        再次比较v2顶点与未选顶点集合的最小权值边,以及其他已选顶点与未选顶点集合的最小权值边。 

        又得出了v3,重复上方v2的修改方式,将v3的adjvex变为2,lowcost变为0;

普里姆算法的代码实现

克鲁斯卡尔算法

        克鲁斯卡尔算法以边为目标进行构建,只不过构建时需要考虑是否会形成环路而已。

        当选取了n-1条边时,此时最小生成树已经形成。

克鲁斯卡尔算法的三个关键点 

        在该算法中,主要运行步骤如下:

        1.如何判别边的两个顶点是位于两个连通分量

        2.此外,图采用哪种存储形式也是很重要的——因为该算法的主体是边,而邻接表和邻接矩阵存储顶点来存储边的信息,需要搜索边时需要历经每个顶点才能找到最短边。

        3.采用哪种排序方式?采用插入排序,时间复杂度O(n2);采用堆排序或快速排序,时间复杂度O(nlog2n)

三个关键点解决如下:

        1.图采用哪种存储形式? 

数据存储结构设计如下:

        用边集数组的形式进行存储,可以有效存储边的信息

        2.如何确定两个顶点为连通分量? 

         此处使用了一则并查集的方式,大致原理如下:

   一个集合构造一棵树,任选一个元素为根结点,建立数组parent记录每个结点的父结点,式子parent【当前结点】=根结点。

                                       

        而合并就是将另一子树的根结点更改,如下:

        而确认两个顶点为连通分量,也是将两个顶点合并,即合成为同一棵树。 

克鲁斯卡尔算法的运行逻辑

 开始初步运行:    

        根据如上步骤得出图如下:

        当非连通顶点得以连接后,parent数组如下:

当连通顶点与其他子树连通时如下: 

       

当连通顶点即将构成环时

当边数为n-1时,最小生成树生成 

克鲁斯卡尔代码实现

边集数组结构定义

算法实现 

两套算法的优劣对比

        克鲁斯卡尔算法的Find函数由边数e决定,时间复杂度为O(loge),而外面有一个for循环e次。所以克鲁斯卡尔算法的时间复杂度为O(eloge)。

        而循环嵌套可得知普里姆算法的时间复杂度为O(n^{2})。

        对比两个算法,克鲁斯卡尔算法主要是针对边来展开,边数少时效率会非常高,所以对于稀疏图有很大的优势;而普里姆算法对于稠密图,即边数非常多的情况会更好一些。 

最短路径

        在非网图中,他没有边上的权值,所谓最短路径,其实就是指两顶点之间边数最少的路径;对于网图而言,最短路径是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上第一个顶点是源点,最后一个顶点是终点。

迪杰斯特拉算法

        需要一个已选顶点集合和未选顶点集合来控制顶点是否使用,另外还需要一个数组来控制达到目前顶点的最短路径,还需要一个数组控制达到当前顶点时,上一个顶点是什么。

算法运行流程

        求出下图0-6的最短路径

        各项数组参数从顶点0开始如下: 

        开始顶点0到其他顶点的最短路径: 

        而此时在顶点1时,完成如下流程: 

         

        此时顶点1结束后,各参数如下:

       

        由dist数组可知,目前最短路径为顶点2,于是我们访问顶点2

        

        顶点2各参数如下:

        

        后续以同理往下推,当结束顶点3的访问时,注意此刻最短路径为顶点5了。 

        

        最终到达了顶点6时

                

        最后,求得最短路径及长度如下: 

算法代码实现

 数组初始化

         这里的startV是可以从任意顶点起始,将dist[i]中数组全都填入该顶点的邻接矩阵数据,path[i]数组能与该顶点相连通的录入该顶点下标,不能连通的输入-1,S数组用来确定该顶点是否选中。

主算法运行

 while大循环控制

        再一次在新的dist表中找到最小顶点,记录其下标,接着看加入新顶点后是否需要修改dist表和path表。

弗洛伊德算法 

 算法概念了解

        以上图为例,dist矩阵代表顶点之间的权值大小,path矩阵代表顶点路径。

        迭代第一次,加入顶点A,此时路径能以顶点A为中转,得出表格如下:

        迭代第二次,加入顶点B,此时路径能以A,B为中转,得出表格如下:

        迭代第三次,加入顶点C,此时路径能以A,B,C为中转,得出表格如下:

        综上,得出公式结论如下:

 算法代码实现

初始化dist数组和path数组

主程序运行 

         这一段path矩阵需要着重修改,因为有acb存在,可能需要三维数组进行存储。

两种算法的优劣

        两者算法复杂度都是O(n^{3}),对于单个顶点到单个顶点的最短路径可以用迪杰斯特拉算法,(单个顶点到单个顶点的该算法复杂度为O(n^{2})),多个顶点到多个顶点用弗洛依德算法更合适。

拓扑排序

        在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样的有向图为顶点表示活动的网,我们称为AOV网。AOV网中的弧存在某种制约关系,比如演职人员确定好了才可以进场拍摄;另外,AOV网中不能存在回路。

         

        设G是一个有n个顶点的有向图,V中顶点序列为v1,……,vn,若满足一个顶点vi到vj有一条路径,则在顶点序列中顶点vi必在vj前。则我们称这样的顶点为一个拓扑序列。

        如上图的拓扑序列不止一条,v0v1v2v5……v16是一条拓扑序列,v0v1v4……v16是一条拓扑序列。 

        而所谓拓扑排序,其实就是对一个有向图拓扑构造拓扑序列的过程。构造时有两个结果:若是此网的全部顶点都被输出,说明它不存在环的AOV网,若是输出顶点少了,即使是一个,也说明这个网存在环,不是AOV网。

拓扑排序算法

 对AOV网基本思路是:

        (1)从AOV网中选择一个没有前驱的顶点并且输出;
        (2)从AOV网中删去该顶点,并且删去所有以该顶点为尾的弧;
        (3)重复上述两步,直到全部顶点都被输出,或AOV网中不存在没有前驱的顶点。

        该拓扑排序为1、2、3、4、5、6、7

该图的数据结构:

        因为需要删除顶点,所以我们选择用邻接表来建立AOV网更加方便,而考虑到算法过程中始终要查找入度为0的顶点,我们在原来顶点表结点结构中,增加一个入度域in,表示入度的数量。

                

        另外,我们还需要一个栈S来存储所有无前驱的顶点,也可以用队列。

拓扑排序流程

        以下图为例子开始实现算法流程: 

         

        扫描顶点表,将堆栈初始化。将入度为0的顶点B,E压入堆栈。

        将栈顶元素E出栈,根据顶点E的firstedge遍历所有的边,其指向各个顶点的入度值-1,在处理时,如果发现某个顶点入度值为0,则压入栈堆。

         

        此时发现C入度为0,入栈。后面输出也是同理类推。 

拓扑排序代码实现

数据初始化 

         

算法运行 

关键路径

        在一个表示工程的带权有向图中,用顶点表示事件,有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,我们称之为AOE网。

        尽管AOE网和AOV网都是用来对工程建模,但它们还是有很大不同,主要体现在AOV网是顶点表示活动的网,它只描述活动之间的制约关系;而AOE网是用边表示活动的网,边上的权值表示活动持续的时间。

        路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大的长度路径叫关键路径,在关键路径上的活动叫关键活动。

四个关键参数

        为此,我们需要定义如下几个参数:

        1. 事件的最早发生时间ve[k]:ve[k]是指从始点开始到顶点vk的最大路径长度。这个长度决定了所有从顶点vk发出的活动能够开工的。

        以上公式的含义是,一个顶点有几种不同的路径时,取权值和最大的那个值,就比如此时ve[4]有5和7两条路径取了7。

        2. 事件的最晚发生时间ltv:再不推迟整个工期的情况下,事件允许的最晚发生时间

         

        从终点倒推,回溯到某顶点时有多个路径时,选权值最小的那条路径。

        3. 活动的最早开工时间ee[i]:若活动a是由弧<vk,vj>表示,则活动a的最早开始时间应等于事件的最早发生时间。

  

         

        当活动顶点为起始点时,起始点的所有到其他顶点的弧,ee[i]=0,而a3这个活动开始时间,就是v1这个事件开始时间,a3的ee【i】=6;同理,a9这个活动最早开始时间,就是v6这个事件最早开始时间,a9的ee【i】=ve【6】=16

        4. 活动的最晚开工时间lte:即弧ak的最晚发生时间

       

 

算法代码实现

求拓扑序列

        

误点1:

误点2: 

        以为就这下面一种输出方式,实际上这只是其中一种.

        输出内容与边表的链接有关,以下两个边表位置可以置换,但置换后输出先后改变.

        还是用取点摘边法来判断有哪些输出方式吧. 

关键路径的算法代码 

初始化 

计算ltv 

计算ete和lte 

        所以最终,就是判断lte与ete是否相等,相等意味着没有任何空闲,是关键活动。

        分析整个求关键路径的算法,第6行是拓扑排序,时间复杂度为O(n+e),第8~9行时间复杂度为O(n),第10~19行时间复杂度为O(n+e),第20~~31行时间复杂也为O(n+e),根据我们对时间复杂度的定义,所有的常数系数可以忽略,所以最终求关键路径算法的时间复杂度依然是O(n+e)。 

         

         

       

        

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值