【考研·数据结构】 图 小结

本篇目录

前言

一、基本概念

二、图的存储

1.邻接矩阵法

2.邻接表法

3.十字链表法

4.邻接多重表

三、图的遍历

1.总论

2.广度优先搜索BFS

3.深度优先算法DFS

4.BFS & DFS

 四、图的应用

1.最小生成树MST

(1)总论

(2)Prim算法

(3)Kruskal算法

2.最短路径

(1)Dijkatra算法

(2)Floyd算法

3.有向无环图DAG

4.拓扑排序

5.关键路径

总结


前言

本篇梳理数据结构中“图”这部分的有关概念、算法,适用于考研复习。


一、基本概念

1.记为G(V,E),V是顶点集,E是边集。图至少有一个顶点,可以没有边。

2.简单图:不存在重复边&不存在顶点到自身的边。否则是多重图。

3.完全图:任意两个顶点之间都有边,无向完全图的边数为n(n-1)/2,有向完全图则x2即可。

4.子图:原图去掉一些边和顶点。生成子图:原图去掉一些边。

5.对于无向图:

连通图:任意两个顶点之间都有路径的图。

连通分量(极大连通子图):一个子图,它的顶点与子图外的顶点没有边相连。

生成树(极小连通子图):串起所有顶点但没有环的子图,边数是n-1。

6.对于有向图:

强连通图:有向图的任意两个顶点之间都有路径。

强连通分量(极大连通子图):一个子图,它的顶点与子图外的顶点没有边相连。

7.路径长度:经过的边的数目。简单路径:没有环的路径。

8.:边可以带权,带权图就是网。

9.无向图顶点的度:此顶点有几条边。有向图顶点的度分出度和入度。

有向树:顶点入度为0,其余顶点入度为1的有向图,就坍缩为有向树。


二、图的存储

1.邻接矩阵法

1.用二维数组表示图,数组角标表示顶点编号,数组元素的值表示边的有无或者权。

2.图的邻接矩阵唯一,无向图的邻接矩阵对称。

3.稠密图适合用邻接矩阵存储,无向图的矩阵可以压缩存储。

2.邻接表法

1.表示方法:用一维数组作为顶点表,每个顶点链式挂邻接的边。

2.邻接表法适合稀疏矩阵。

3.图的邻接表不唯一。

对比 邻接矩阵VS邻接表法

1.找到某顶点所有的边(或者说所有邻接顶点)需要的时间: 邻接矩阵 O(V) ;邻接表法 O(E)。

3.十字链表法

1.它是有向图的链式存储法。顺着链能找到从某结点出发的所有弧,也能找到到达某结点的所有弧。

2.顶点表用顺序存储

弧结点的域:①从哪出发 ②到哪儿去 ③同目的地的弧 ④ 同出发点的弧 ⑤其它信息

顶点结点的域:①数据 ②到这来的一条弧 ③从这出发的一条弧

4.邻接多重表

1.无向图的链式存储,一条边只用一个结点。

2.顶点表用顺序存储

边结点的域:①标志是否被搜索过 ②依附的一个顶点 ③这顶点的另一条边 ④依附的另一个顶点 ⑤ 这顶点的另一条边 ⑤其他信息

顶点结点的域:①数据 ②一条边


三、图的遍历

1.总论

 1.遍历

书上说图的遍历是访问全部顶点,且每个顶点只访问一次。但我个人认为,这样说可能产生误解。图是由顶点和边组成的,遍历应该说是访问所有的顶点和边,且只访问一次。不然的话,只需要访问顺序存储的顶点表,不就算遍历了吗(即不访问任何边),这样的话图的结构就坍缩为一维数组了。

树是一种特殊的图,就如同正方形是一种特殊的矩形。图的广度优先搜索(BFS)相当于树的层序遍历,图的深度优先搜索(DFS)相当于树的先序遍历。或者也可以反过来说,树的层序遍历相当于图的BFS,树的先序遍历相当于图的DFS。图要考虑不是连通图的情况,还要设置访问标记数组,因此比树的算法更复杂一些。

2.时间复杂度

无论广度优先还是深度优先,既然目的是遍历,那么所有顶点都一定会被访问一次,每条边也要被访问一次。无论存储方式是邻接矩阵还是邻接表法,访问所有结点的时间都是O(V)。然后,如果用邻接矩阵,对于每个结点,需要访问它的所有边,需要的时间是O(V),因此BFS总的时间复杂度是O(V^2)。如果用邻接表法,查找图中所有的边需要的总时间是时间就是O(E),因此DFS总的时间复杂度是O(V+E)。可见如果要广度优先搜索,用邻接表法更好一些。

结论就是,无论广度优先还是深度优先,用邻接矩阵的时间复杂度是O(V^2),用邻接表法的时间复杂度是O(V+E)。

3.空间复杂度

BFS与DFS都是递归算法,需要借助工作栈,对于每一个顶点都要递归执行一次操作。在BFS中,对每一个顶点都要执行一次“访问所有邻接顶点”的操作;在DFS中,对每一个顶点都要执行“找到一个没被访问过的邻接顶点”。因此无论是BFS还是DFS,空间复杂度都是 O(V) 。

4.生成树

生成树是否是唯一的,决定于图所用的存储方法。无论是BFS还是DFS,图的邻接矩阵表示是唯一的,那么生成树也是唯一的;图的邻接表法表示不唯一,那么生成树也不唯一。

2.广度优先搜索BFS

1.原理

初始化顶点的访问标记数组,初始化辅助队列,之后对每个顶点递归访问。对于每个顶点,都优先将所有的邻接顶点入队,之后再从队列出队一个顶点进行访问。这就像在吃单人火锅时,一个人总是优先将已经熟了的所有食材全都盛到自己的盘子(队列)里,然后再按顺序(队列FIFO)逐个吃掉盘子里的食物(出队并访问)。要是盘子里的食物还没吃完(队列不空),又发现锅里有新的已熟食材,那么他优先将食材盛到自己的盘子里(入队),而不是等盘子里的吃完了再去盛。最终锅里所有的食物都被盛出来并吃完(完成图的遍历)。他的这种策略,就需要一个盘子(队列)。

2.伪代码

//图的广度优先搜索算法
bool visited[MAX_V_NUM];        //建立访问标记数组
void (BFSTraverse(Graph G)      //广度优先搜索
    for(int i=0;i<G.vexnum;++i){    //访问标记数组初始化
        visited[i] = false;
    InitQueue(Q);                //辅助队列初始化

    for(int i=0;i<G.vexnum;++i)     //从0号开始,遍历每个顶点
        if(!visited[i])
            BFS(G,i);            //访问每个顶点
}
//递归访问每个顶点
void BFS(Graph G,int v){
    visit(v);                    //访问当前顶点
    visited[v] = true;           //标记 顶点v已访问
    Enqueue(Q,v);                //访问过顶点就入队

    while(!isEmpty(Q)){          //队列中顶点的处理
        DeQueue(Q,v);        //出队一个顶点
        for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))    
        //对该顶点的所有邻接顶点访问并入队
            if(!visited[w]){
                visit(w);        //访问该邻接顶点
                visited[w] = true;    //访问标记
                EnQueue(Q,w);    //入队
            }
    }
}

3.深度优先算法DFS

1.原理

初始化顶点的访问标记数组,之后对每个顶点递归访问。对于每个顶点,只要抓到一个邻接顶点就立马优先去访问,回头再去找有没有其它的邻接顶点。这就像在吃单人火锅时,一个人不管锅里有多少已经熟了的食材(待访问的顶点),他总是用一个小勺子,盛一勺就吃一勺,不断重复,到最后也吃完了锅里所有的食物。因此他不需要一个盘子(队列)来盛放一时来不及吃掉的食物,也不关心锅里还有没有已经熟了的食材。

2.伪代码

//深度优先搜索 递归算法
bool visited{MAX_V_NUM];    //访问标记数组
void DFSTraverse(Graph G){
    for(int v=0;v<G.vexnum;++v)    //初始化访问标记
        visiten[v] = false;
    for(int v=0;v<G.vexnum;++v)    //从0号顶点开始访问
        if(!visited[v])
            DFS(G,v);
}
//对每一个顶点v递归访问
void DFS(Graph G,int v){
    visit(v);                //访问当前顶点
    visited[v] = true;
    for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w))    //访问每一个邻接顶点
        if(!visited[w])
            DFS(G,w);
}

4.BFS & DFS

进一步对比一下这两种图的遍历算法,就会发现它们有很多步骤是相同的。我在图中标记它们的代码的不同之处。可以看到,它们的不同之处主要就在于广度优先搜素算法中多了对于队列的处理,因而代码显得比深度优先算法复杂一些。还是用之前吃火锅的例子,BFS就相当于,这个人吃火锅的时候,不仅要操心自己正在吃的食物(访问的顶点),还要操心锅里是不是有已经熟的食材,如果有就要捞出来放到盘子里(入队)。因为多了这样一些心思,所以代码就长一点。而DFS就相当于,另一个人,一门心思只管吃,盛一勺吃一勺,吃完再来一勺。这人的心思简单,代码也就简单。但是无论如何,这两个人最后都吃掉了锅里所有的食材(完成图的遍历),并且速度差不多(时间复杂度一致),所以喜欢哪个策略就用哪个吧。

 


 四、图的应用

1.最小生成树MST

(1)总论

1.最小生成树MST:边的权值总和最小的生成树(见概念部分)。

2.最小生成树可能不唯一(有权值相同的边时),但MST的权值总和是唯一的。

3.构造MST的思路

从定义看,最小生成树包含所有的顶点,而边就是挑着用的。既然目标是权值总和最小,那么自然就要挑权值小的边。Prim和Kruskal两种算法的区别在于,选边的策略不同,一个以顶点作为考虑的出发点,另一个以边作为考虑的出发点。

(2)Prim算法

1.原理

P算法适用于顶点稀疏边稠密的图。既然顶点少,就从顶点出发来选边。初始时先随机选一个顶点作为树根,之后就让树中所有顶点伸手向外抓,遍历一遍看谁能抓到权值最小的边(且不能形成回路),然后把这条边以及依附于的顶点加进树里。这样,顶点数就+1。之后不断重复这个步骤,直到所有顶点都进入树中。

2.时间复杂度

在每次的选边步骤中,都要遍历树中全部顶点(来找到权值最小的边),因此,一次选边步骤的时间是O(V)。因为每次选边步骤就能向树中增加一个顶点,总共V个顶点,所以需要重复选边步骤的次数是O(V)。因此,Prim算法总的时间复杂度是O(V^2),与E无关。

(3)Kruskal算法

1.原理

K算法适用于边稀疏顶点稠密的图。既然边少,那么就不管顶点了,只管选择权值最小的边就行。每次都遍历待选边集,从中选出权值最小的一条边(且不产生回路),加入树。

2.时间复杂度

通常K算法的边集用堆来存放,因此,每次的选边步骤的时间为O(log E)。树中的边可以用并查集来描述,每次加入新的边,就相当于向并查集(树)内并入一棵新树。一共有E条边要并入树中,因此,并查集的合并操作一共要执行O(E)次。由此可知,Kruskal算法总的时间复杂度为O(E log E),与V无关。

2.最短路径

对于带权有向图,找出最短路径的算法,一类是单源最短路径Dijkatra算法,另一类是每对顶点最短路径Floyd算法。

(1)Dijkatra算法

【1】原理

1.初始化,设置源点到其它各顶点的距离数组。

2.遍历从当前顶点v出发的弧,更新邻接顶点的权值(若新值比数组中的值更小,则更新)。

3.选出与顶点v距离最近的顶点v_next ,重复步骤2,直到所有顶点都被选过。

注意:不适用负权值。

【2】时间复杂度

对于每一个顶点都要执行一次选最小边的步骤,因而选最小边的步骤一共执行O(V)次。而一次选最小边的步骤中,需要遍历当前顶点的邻接顶点来选出距离最近的顶点v_next,需要的时间也是O(V)。因此总的时间复杂度为O(V^2)。

如果要用D法计算每对顶点之间的最短路径,那就可以依次将每个顶点都设为源点。这样,总的时间复杂度就是O(V^3)。

(2)Floyd算法

【1】原理

1.以图的邻接矩阵作为算法的n阶矩阵。

2.对当前顶点,遍历所有弧,更新矩阵中的距离值。

3.选下一个顶点(按0 1 2 3...选就行),重复步骤2 。

注意:F法允许负权值,但不允许负权值回路。

【2】时间复杂度

首先要遍历所有顶点,这就需要O(V)的时间。对每一个顶点,要遍历所有弧并更新矩阵,矩阵的数据量是O(V^2),所以每个顶点的处理需要O(V^2)的时间。因此,F法总的时间复杂度为O(V^3)。但它代码紧凑,常数系数很小,因此对于中等规模的输入来说仍然相当有效。

【3】对比 Dijkatra法与Floyd法

D法和F法都要遍历所有顶点,对每一个顶点要遍历所有弧并更新数组/矩阵。区别在于,D法依据就近原则(距离最近)选下一个顶点,F法则无差别对待,按编号依次选择。

3.有向无环图DAG

DAG图适用于描述含有公共子式的表达式

4.拓扑排序

1.AOV网:DAG图可以表示一个工程,顶点表示活动,弧表示了活动之间的先后顺序,这样的图就是AOV网。

2.拓扑排序:将AOV网中的顶点,每次选择一个入度为0的顶点并去掉它的所有弧,直到选完所有顶点,得到的顶点序列就是拓扑排序,可能不唯一。利用DFS也可以实现拓扑排序。

时间复杂度

拓扑排序要遍历每个结点,这需要O(V)的时间,除此之外,还需要去除每个结点的所有弧,整个图一共有E条弧(边),因此去除弧的这一步总共需要O(E)的时间。总的来说,拓扑排序的时间复杂度为 O(V+E) 。

5.关键路径

【1】概念

1.AOE网:在DAG图中,以顶点表示事件,以弧表示活动,以弧的权值表示该活动的开销(比如时间),这样就是用边表示活动的网络,即AOE网。AOE网只有一个入度为0的顶点,是开始顶点(源点),也只有一个出度为0的顶点,是结束顶点(汇点)。当某顶点(事件)发生后,所有它发出的弧(活动)才能开始;只有当某顶点(事件)的所有到达弧(活动)全部结束时,该顶点(事件)才能发生。

2.关键路径:从源点到汇点的所有路径中,具有最大路径长度的路径。关键路径上的活动就是关键活动。关键路径可能不唯一,加快所有关键路径上共同的关键活动可以缩短工期。

【2】求解方法

1.求事件v(k)的最早发生时间 ve(k)

即求从源点到各顶点的最短路径!源点的ve=0 ,剩余的顶点可以按拓扑排序的顺序来求。

2.求事件v(k)的最迟发生时间 vl(k)

设第1步中求出的汇点的ve为m,现在将所有弧的方向取反,计算汇点到所有顶点的最短路径 a ,该顶点的vl = m - a 。

3.求活动a(i)的最早开始时间 e(i)

弧(活动)的最早开始时间 = 出发点的ve(最早发生时间)

4.求活动a(i)的最迟开始时间 l(i)

弧(活动)的最迟开始时间 = 弧终点的vl(最迟发生时间) - 弧持续的时间 。

5.求活动a(i)的最迟开始时间l(i)与最早开始时间e(i)的差额 d(i)

d(i) = l(i) - e(i) ,即该活动完成的时间余量,即该活动可以拖延的时间。d(i)=0则必须如期完成,否则会拖延工期,这样的活动就是关键活动。


总结

本篇总结了数据结构中“图”这部分的概念、存储方法、应用(各种算法)。

猜你喜欢

转载自blog.csdn.net/Dr_Cheeze/article/details/128017018