数据结构(九)图

图 1 所示为存储 V1、V2、V3、V4 的图结构,从图中可以清楚的看出数据之间具有的"多对多"关系。


图1:图存储结构示意图

与链表不同图中存储的各个数据元素被称为顶点(而不是节点)。

图存储结构中,习惯上用 Vi 表示图中的顶点,且所有顶点构成的集合通常用 V 表示,如图 1 中顶点的集合为 V={V1,V2,V3,V4}。

图存储结构可细分两种表现类型,分别为无向图(图 1)和有向图(图 2)


图2:有向图示意图

图的基本常识

弧头和弧尾:有向图中,无箭头的一端顶点被称为"初始点"或"弧尾",箭头指向的顶点被称为"终端点"或弧头。

入度和出度:对于有向图中的一个顶点V来说,箭头指向V的弧的数量为V的入度(InDegree,记为ID(V));箭头远离V的弧的数量为V的出度(OutDegree,记为OD(V))。拿图2中顶点V1来说,该顶点的入度为ID(1),出度为OD(2),该顶点的度为3.

(V1,V2)和无向图中描述两顶点(V1和V2)之间的关系可以用(V1,V2)来表示,而有向图描述从V1到V2的“单向”关系用来表示。由于图存储结构中顶点之间的关系式用线来表示的,因此(V1,V2)还可以用来表示无向图中连接V1和V2的线,又称为边;同样,也可以用来表示有向图中从V1到V2带方向的线,又称为弧。

集合VR的含义 :并且,图中习惯用 VR 表示图中所有顶点之间关系的集合。例如,图 1 中无向图的集合 VR={(v1,v2),(v1,v4),(v1,v3),(v3,v4)},图 2 中有向图的集合 VR={,,,}。

路径和回路:无论是无向图还是有向图,从一个顶点到另一顶点途经的所有顶点组成的序列(包含这两个顶点),称为一条路径。如果路径中第一个顶点和最后一个顶点相同,则此路径称为"回路"(或"环")。并且,若路径中各顶点都不重复,此路径又被称为"简单路径";同样,若回路中的顶点互不重复,此回路被称为"简单回路"(或简单环)。拿图 1 来说,从 V1 存在一条路径还可以回到 V1,此路径为 {V1,V3,V4,V1},这是一个回路(环),而且还是一个简单回路(简单环)。

权和网络:在某些实际场景中,途中的每条边(或弧)会赋予一个实数来表示一定的含义,这种与边(或弧)相匹配的实数被称为“权”,而带权的图通常称为。如图3所示就是一个网结构。

子图:指的是由图中一部分顶点和边构成的图,称为原图的子图。

图3,网结构

图存储结构的分类

根据不同的特征,图又分为完全图,连通图、稀疏图和稠密图。

    1. 完全图:若图中各个顶点都与除自身外的其他顶点有关系,这样的无向图称为完全图,同时满足此条件的有向图则称为有向完全图。

具有 n 个顶点的完全图,图中边的数量为 n(n-1)/2;而对于具有 n 个顶点的有向完全图,图中弧的数量为 n(n-1)。

图4:完全图示意图
    1. 连通图:无向图中,如果任意两个顶点之间都能够连通,则称此无向图为连通图,例如图5 就是一个连通图,因为任意两顶点之间都是连通的。
      图5:连通图示意图

      若无向图不是连通图,但图中存储某个子图符合连通图的性质,则称该子图为连通分量。
      如图 6 所示,虽然图 6a) 中的无向图不是连通图,但可以将其分解为 3 个"最大子图"(图 6b)),它们都满足连通图的性质,因此都是连通分量。
      图6:连通分量示意图

      需要注意的是,连通分量的提出是以"整个无向图不是连通图"为前提的,因为如果无向图是连通图,则其无法分解出多个最大连通子图,因为图中所有的顶点之间都是连通的。
    1. 强连通图:有向图中,若任意两个顶点 Vi 和 Vj,满足从 Vi 到 Vj 以及从 Vj 到 Vi 都连通,也就是都含有至少一条通路,则称此有向图为强连通图。如图 7 所示就是一个强连通图。
      图7:强联通图示意图

      与此同时,若有向图本身不是强连通图,但其包含的最大连通子图具有强连通图的性质,则称该子图为强连通分量。
      图8:强连通分量

      如图8所示,整个有向图虽然不是强连通图,但其含有两个强连通分量。
      连通图是在无向图的基础上对图中顶点之间的连通做了更高的要求,而强连通图是在有向图的基础上对图中顶点的连通做了更高的要求。
    1. 稀疏图和稠密图:两种图是相对存在的,即如果图中具有很少的边(或弧),此图就称为"稀疏图";反之,则称此图为"稠密图"。

稀疏和稠密的判断条件是:e


生成树,生成森林

生成树

对连通图进行遍历,过程中所经过的边和顶点的组合可看作是一棵普通树,通常称为生成树。


图9:连通图及其对应的生成树

如图 9 所示,图 9a) 是一张连通图,图 9b) 是其对应的 2 种生成树。

连通图中,由于任意两顶点之间可能含有多条通路,遍历连通图的方式有多种,往往一张连通图可能有多种不同的生成树与之对应。

连通图中的生成树必须满足一下两个条件:

1.包含连通图的中所有的顶点;
2.任意两顶点之间有且仅有一条通路;

因此,连通图的生成树具有这样的特征,即生成树中边的数量=顶点数-1

生成森林

生成树是对连通图来说的,而生成森林是对应非连通图来说的。

非连通图可分解为多个连通分量,而每个连通分量又各自对应多个生成树,因此与整个非连通图相对应的,是由多棵生成树组成的生成森林。


图10:非连通图和连通分量

如图10所示,这是一张非连通图,可分解为3个连通分量,其中各个连通分量对应的生成树如图11所示:

图11 生成森林

注意:图3中列出的仅是各个连通分量的其中一种生成树。

因此,多个连通分量对应的多棵生成树就构成了整个非连通图的生成森林。


图的顺序存储结构(邻接矩阵)

使用图结构表示数据元素之间虽然有多对多的关系,但是同样可以采用顺序存储。也就是使用数组存储图。

使用数组存储图中各顶点本身数据,一维数组就够了;但是存储各个顶点之间的关系时,要记录每个顶点和其他所有顶点之间的关系,所以就需要二维数组了。

不同类型的图,存储的方式略有不同,根据图有无权,可以将图划分为两大类:图和网。

图,包括无向图和有向图;网是指带权的图,包括了无向网和有向网。存储方式的不同,指的是:在二维数组存储图中顶点之间的关系时,如果顶点之间存在边或弧,在相应的位置用1表示,反之用0表示。如果使用二维数组存储网中顶点之间的关系,顶点之间如果有边或者弧的存在,在数组的相应位置存储其权值;反之用0表示。

图12:有向图和无向图

例如,存储图 12 中的无向图(B)时,除了存储图中各顶点本身具有的数据外,还需要使用二维数组存储任意两个顶点之间的关系。

由于 (B) 为无向图,各顶点没有权值,所以如果两顶点之间有关联,相应位置记为 1 ;反之记为 0 。构建的二维数组如图 13 所示。


图13:无向图对应的二维数组arcs

在此二维数组中,每一行代表一个顶点,依次从 V1 到 V5 ,每一列也是如此。比如 arcs[0][1] = 1 ,表示 V1 和 V2 之间有边存在;而 arcs[0][2] = 0,说明 V1 和 V3 之间没有边。

对于无向图来说,二维数组构建的二阶矩阵,实际上是对称矩阵,在存储时就可以采用压缩存储的方式存储下三角或者上三角。

通过二阶矩阵,可以直观地判断出各个顶点的度,为该行(或该列)非 0 值的和。例如,第一行有两个 1,说明 V1 有两个边,所以度为 2。

储图 12 中的有向图(A)时,对应的二维数组如图 14 所示:


图14:有向图对应二维数组arcs

例如,arcs[0][1] = 1 ,证明从 V1 到 V2 有弧存在。且通过二阶矩阵,可以很轻松得知各顶点的出度和入度,出度为该行非 0 值的和,入度为该列非 0 值的和。例如,V1 的出度为第一行两个 1 的和,为 2 ; V1 的入度为第一列中 1 的和,为 1 。所以 V1 的出度为 2 ,入度为 1 ,度为两者的和 3 。


图的邻接表存储法

通常,图更多是采用链表存储,具体的存储方法有3中,邻接表、邻接多重表、十字链表。
邻接表即适合存储无向图,也适合存储有向图。

邻接指的是图中顶点之间有边或者弧的存在。

邻接表存储图的实现方式是,给图中的各个顶点独自建立一个链表,用节点存储该顶点,用链表中其他节点存储各自的临界点。

与此同时,为了便于管理这些链表,通常会将所有链表的头节点存储到数组中(也可以用链表存储)。也正因为各个链表的头节点存储的是各个顶点,因此各链表在存储临界点数据时,仅需存储该邻接顶点位于数组中的位置下标即可。

例如,存储图 15a) 所示的有向图,其对应的邻接表如图 15b) 所示:


图15:邻接表存储有向图

拿顶点 V1 来说,与其相关的邻接点分别为 V2 和 V3,因此存储 V1 的链表中存储的是 V2 和 V3 在数组中的位置下标 1 和 2。

从图中可以看出,存储各顶点的节点结构分为两部分,数据域和指针域。数据域用于存储顶点数据信息,指针域用于链接下一个节点,如图 16 所示:


图16:邻接表节点结构

在实际应用中,除了图 16这种节点结构外,对于用链接表存储网(边或弧存在权)结构,还需要节点存储权的值,因此需使用图 17 中的节点结构:


图17:邻接表存储网结构使用的节点

邻接表计算顶点的从出度和入度

用邻接表计算无向图中顶点的入度和出度会非常简单,只需从数组中找到该顶点然后统计此链表中节点的数量即可。

而使用邻接表存储有向图时,通常各个顶点的链表中存储的都是以该顶点为弧尾的邻接点,因此通过统计各顶点链表中的节点数量,只能计算出该顶点的出度,而无法计算该顶点的入度。
对于利用邻接表求某顶点的入度,有两种方式:

  1. 遍历整个邻接表中的节点,统计数据域与该顶点所在数组位置下标相同的节点数量,即为入度。
  2. 建立一张逆邻接表,该表中的各顶点链表专门用于存储以此顶点为弧头的所有顶点在数组中的位置下标,比如建立一张图15a)对应的逆邻接表。如图18所示:


    图18:逆邻接表示意图

对于具有 n 个顶点和 e 条边的无向图,邻接表中需要存储 n 个头结点和 2e 个表结点。在图中边或者弧稀疏的时候,使用邻接表要比邻接矩阵更加节省空间。


图的十字链表法存储

与邻接表不同,十字链表法仅适用于存储有向图和有向网,不仅如此,十字链表法还改善了邻接表计算图中顶点入度的问题。

该结构可以看成是将有向图的邻接表和逆邻接表结合起来得到的。用十字链表来存储有向图,可以达到高效的存取效果。同时,代码的可读性也会得到提升。

字链表存储有向图(网)的方式与邻接表有一些相同,都以图(网)中各顶点为首元节点建立多条链表,同时为了便于管理,还将所有链表的首元节点存储到同一数组(或链表)中。


图19:十字链表存储有向图示意图

其中,建立个各个链表中用于存储顶点的首元节点结构如图 20 所示:


图20:十字链表首元节点结构示意图

从图 20 可以看出,首元节点中有一个数据域和两个指针域(分别用 firstin 和 firstout 表示):

data :用于存储该顶点中的数据
firstin :指向第一条入弧的节点
firstout :指向第一条出弧的节点

由此可以看出,十字链表实质上就是为每个顶点建立两个链表,分别存储以该顶点为弧头的所有顶点和以该顶点为弧尾的所有顶点。

存储图的十字链表中,各链表中首元节点与其他节点的结构并不相同,图 20 所示仅是十字链表中首元节点的结构,链表中其他普通节点(普通节点其实表示的是弧)的结构如图 2 1所示:


图21:十字链表中普通节点的结构示意图

tailvex:此弧的弧尾数组下标
headvex:此弧的弧头数组下标
hlink:指向下一条弧头相同的普通节点
tlink:指向下一条弧尾相同的普通节点
info: 用于存储弧的信息,例如权


图的邻接多重表存储

邻接多重表仅适用于存储无向图或无向网,如果想对图中某顶点进行实操(修改或删除),由于邻接表中存储该顶点的节点有两个,因此需要操作两个节点。

邻接多重表存储无向图的方式,可看作是邻接表和十字链表的结合。同邻接表和十字链表存储图的方法相同,都是独立卫图中各自顶点建立一张表,存储各顶点的节点作为各链表的首元节点,同时为了便于管理将各个首元节点存储到一个数组中。首元节点结构如图所示:


图22:邻接多重表首元节点结构示意图

data:存储此顶点的数据;
firstedge:指针域,用于指向同该顶点有直接关联的存储其他顶点的节点。

从图 22 可以看到,邻接多重表采用与邻接表相同的首元节点结构。但各链表中其他节点的结构与十字链表中相同,如图 23 所示:

图23:邻接多重表中其他节点结构

图 23 节点中各区域及功能如下:

  • mark:标志域,用于标记此节点是否被操作过,例如在对图中顶点做遍历操作时,为了防止多次操作同一节点,mark 域为 0 表示还未被遍历;mark 为 1 表示该节点已被遍历;
  • ivex 和 jvex:数据域,分别存储图中各边两端的顶点所在数组中的位置下标;
  • ilink:指针域,指向下一个存储与 ivex 有直接关联顶点的节点;
  • jlink:指针域,指向下一个存储与 jvex 有直接关联顶点的节点;
  • info:指针域,用于存储与该顶点有关的其他信息,比如无向网中各边的权;

综合以上信息,如果我们想使用邻接多重表存储图 24a) 中的无向图,则与之对应的邻接多重表如图 24b) 所示


图24:无向图及其对应的邻接多重表

从图 24 中,可直接找到与各顶点有直接关联的其他顶点。比如说,与顶点 V1 有关联的顶点为存储在数组下标 1 处的 V2 和数组下标 3 处的 V4,而与顶点 V2 有关联的顶点有 3 个,分别是 V1、V3 和 V5。


深度优先搜索(DFS[Depth First Search]、深搜)和广度优先搜索(BFS[Breadth First Search]、广搜)

图常用的遍历方式有两种:深度优先搜索和广度优先搜索

深度优先

图的深度优先搜索和树的先序遍历比较类似。具体可以用栈来实现,具体访问过程:

1.访问指定的起始顶点
2.若当前访问的顶点的邻接顶点有未被访问的,则任选其一访问之;反之,退回到最近访问过的顶点;直到与起始顶点相同的全部顶点都访问完毕。
3.若此时图中尚有未被访问的,则再选其中一个顶点作为起始顶点并访问之,转2;反之,遍历结束。

连通图的深度优先遍历类似于树的先序遍历。

无向图的深度优先搜索
图25:无向图示意图

对上面的图25进行深度优先遍历,从顶点A开始。


图26:深度优先遍历无向图
  • 第1步:访问A。

  • 第2步:访问(A的邻接点)C。
      在第1步访问A之后,接下来应该访问的是A的邻接点,即"C,D,F"中的一个。但在本文的实现中,顶点ABCDEFG是按照顺序存储,C在"D和F"的前面,因此,先访问C。

  • 第3步:访问(C的邻接点)B。
      在第2步访问C之后,接下来应该访问C的邻接点,即"B和D"中一个(A已经被访问过,就不算在内)。而由于B在D之前,先访问B。

  • 第4步:访问(C的邻接点)D。
       在第3步访问了C的邻接点B之后,B没有未被访问的邻接点;因此,返回到访问C的另一个邻接点D。

  • 第5步:访问(A的邻接点)F。
      前面已经访问了A,并且访问完了"A的邻接点B的所有邻接点(包括递归的邻接点在内)";因此,此时返回到访问A的另一个邻接点F。

  • 第6步:访问(F的邻接点)G。

  • 第7步:访问(G的邻接点)E。
    因此访问顺序是:A -> C -> B -> D -> F -> G -> E

有向图的深度优先搜索
图27:有向图示意图

对上面的图27进行深度优先遍历,从顶点A开始。


图28:有向图深度优先遍历示意图
  • 第1步:访问A。
  • 第2步:访问B。
      在访问了A之后,接下来应该访问的是A的出边的另一个顶点,即顶点B。
  • 第3步:访问C。
      在访问了B之后,接下来应该访问的是B的出边的另一个顶点,即顶点C,E,F。在本文实现的图中,顶点ABCDEFG按照顺序存储,因此先访问C。
  • 第4步:访问E。
      接下来访问C的出边的另一个顶点,即顶点E。
  • 第5步:访问D。
      接下来访问E的出边的另一个顶点,即顶点B,D。顶点B已经被访问过,因此访问顶点D。
  • 第6步:访问F。
      接下应该回溯"访问A的出边的另一个顶点F"。
  • 第7步:访问G。

因此访问顺序是:A -> B -> C -> E -> D -> F -> G


广度优先

广度优先搜索算法,又称为宽度优先算法或横向优先搜索

方法:从图的某一结点出发,首先依次访问该结点的所有邻接顶点 Vi1, Vi2, …, Vin 再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点,重复此过程,直至所有顶点均被访问为止。

连通图的广度优先遍历类似于树的层次遍历。具体可以用队列来实现。

无向图的广度优先搜索
图29:广度优先示意图
  • 第1步:访问A。
  • 第2步:依次访问C,D,F。
      在访问了A之后,接下来访问A的邻接点。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,C在"D和F"的前面,因此,先访问C。再访问完C之后,再依次访问D,F。
  • 第3步:依次访问B,G。
      在第2步访问完C,D,F之后,再依次访问它们的邻接点。首先访问C的邻接点B,再访问F的邻接点G。
  • 第4步:访问E。
      在第3步访问完B,G之后,再依次访问它们的邻接点。只有G有邻接点E,因此访问G的邻接点E。

因此访问顺序是:A -> C -> D -> F -> B -> G -> E

有向图的广度优先搜索
图30:深度优先示意图
  • 第1步:访问A。
  • 第2步:访问B。
  • 第3步:依次访问C,E,F。
      在访问了B之后,接下来访问B的出边的另一个顶点,即C,E,F。前面已经说过,在本文实现中,顶点ABCDEFG按照顺序存储的,因此会先访问C,再依次访问E,F。
  • 第4步:依次访问D,G。
      在访问完C,E,F之后,再依次访问它们的出边的另一个顶点。还是按照C,E,F的顺序访问,C的已经全部访问过了,那么就只剩下E,F;先访问E的邻接点D,再访问F的邻接点G。

因此访问顺序是:A -> B -> C -> E -> F -> D -> G


推荐阅读

链接 描述
图的深度优先搜索和广度优先搜索 图文并茂的介绍了两种搜索算法的具体实现过程
深度优先搜索(DNS)和广度优先搜索(BFS) 详细介绍了两种搜索算法的实现过程
图的遍历算法(深度优先算法DFS和广度优先算法BFS) 详细介绍了两种算法的实现过程,并配备的 C++ 的实现代码
广度优先搜索(BFS)和深度优先搜索(DFS)的应用实例 从实例出发介绍两种搜索算法

深度优先生成树和广度优先生成树

对无向图进行遍历的时候,遍历过程中所经历过的图中的顶点和边的组合,就是图的生成树或者生成森林。


图31:无向图示意图

图 1 中的无向图是由 V1~V7 的顶点和编号分别为 a~i 的边组成。当使用深度优先搜索算法时,假设 V1 作为遍历的起始点,涉及到的顶点和边的遍历顺序为(不唯一):

图32:深度优先遍历顺序

此种遍历顺序构建的生成树为:

图33:深度优先生成树

由深度优先搜索得到的树为深度优先生成树。同理,广度优先搜索生成的树为广度优先生成树,图32 无向图以顶点 V1 为起始点进行广度优先搜索遍历


图34:广度优先生成树

非连通图生成森林

非连通图生成森林与连通图生成树相同,只是森林有中多棵树。

非连通图在遍历生成森林时,可以采用孩子兄弟表示法将森林转化为一整棵二叉树进行存储。


最小生成树算法

  • 普利姆算法
  • 克鲁斯卡尔算法

对于含有n个顶点的连通图来说可能包含多种生成树。例如


图35:连通图的生成树

图35中连通图和它相对应的生成树,可以用于解决实际生活中的问题:假如A、B、C、D为四座城市,为了方便生产生活,要为这四座城市建立通信,对于4个城市来讲,本着节约经费的原则,只需要建立3个通信线路即可,就如图35中生成的任意一种方式。

在具体选择采用哪一种方式时,需要综合考虑城市之间间隔,建设通信线路的难度等各种因素,将这些因素综合起来用一个数值表示,当做这条线路的权值。

图36:无向网

假设通过综合分析,城市之间的权值如图36所示,对于(b)中的方案中,选择权值综合为7的两种方案最节约经费。
这就是最小生成树的问题。简单的理解就是给定一个带有权值的连通网,如何从众多的生成树中筛选出权值综合最小的生成树,即为该图的最小生成树

最小生成树: 一个具有n个顶点的加权的无相连通图,用n-1条边连接这n个顶点,并且使得连接之后的所有边的权值之和最小的树。


普利姆算法

Prim算法:先以一个结点作为最小生成树的第一个结点,然后以迭代的方式找出与最小生成树中各结点权值最小边,加到最小生成树中。加入之后如果产生回路则跳过这条边,选择下一个结点。当所有结点都加入到最小生成树中之后,就找出最小生成树了。


图37:无向连通图

根据下图3个步骤找出最小生成树:

图38:最小生成树迭代过程

使用两个数组 T:所有顶点, U:最小生成树的结点,那么在最初阶段
数组T为{A,B,C,D} ,而最小生成树数组U此时为空。(两点不直接连通权值为无穷大)

    1. 随机选取一个顶点(这里选择了A),T:{B、C、D} 。 U:{A}
    1. 遍历B、C、D与A的权,选出权值最小加入U 选出的点为D,此时T:{B,C} 。U:{A,D}
    1. 遍历B,C 与A的权,遍历B,C与D的权,选出四种关系中最小的,加入U,此时T:{C} ,U:{A,D,B}
    1. 最后,遍历C 与A、D、B的权值,选择最小的权值,并将C加入到U中 此时T:{} ,U:{A,D,B,C},此时四个节点的三个最小权值的边已经确定好。可以输出。

普里姆算法的时间复杂度为O(n2),适合稠密度更高的连通网,普里姆算法是从顶点的角度为出发点。
普利姆算法:具体代码java实现

克鲁斯卡尔算法

克鲁斯卡尔算法,是从边的角度求网的最小生成树,时间复杂度为O(eloge),和普里姆算法相反,克鲁斯卡尔算法更适合于求稀疏网的最小生成树。

对于任意一个连通网的最小生成树来说,在要求的权值最小的情况下,最直接的想法就是将连通网中所有的边按照权值大小进行生序排序,从小到大依次选择。

由于最小生成树本身是一棵树,所以满足以下两点:

    1. 生成树中任意顶点之间有且仅有一条通路,也就是说,生成树中不能存在回路。
    1. 对于具有n个顶点的连通网,其生成树只能有n-1条边,这n-1条边连通n个顶点。

所以克鲁斯卡尔算法的具体实现思路是:将所有边按照权值大小进行排序,然后从小到大判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之则舍去该边。知道具有n个顶点的连通网筛选出来n-1条边为止。筛选出来的边和所有顶点构成此连通图的最小生成树

判断是否有产生回路的方法为:在初始状态下给每个顶点赋予不同的标记,对于遍历过程的每条边,其都有两个顶点,判断这两个顶点的标记是否一致,如果一致,说明它们本身就出在同一棵树种,如果继续连接就会产生回路,如果不一致,说明它们之间还没有任何关系,可以连接。

克鲁斯卡尔算法需要解决两个问题,:

问题1:对图的边按权值大小进行排序
问题2:将边添加到最小生成树中时,怎么样判断是否形成了回路。

克鲁斯卡尔算法:具体代码java实现


重连通图

在无向图中,如果任意两个顶点之间包含有不只一条通路,这个图就被称为重连通图。在删除某个顶点及该顶点相关的边后,图中各顶点之间的连通性也不会被破坏。

关节点(割点):在一个无向图中,如果删除了某个顶点机器相关联的边后,原来的图被分割为两个及以上的连通分量,则称该点为关节点。

重连通图:就是没有关节点的连通图。

连通度:如果一味的做删除顶点的操作,知道删除K个顶点及相关联的边后,图的连通性遭到了破坏,那么称此重连通图的连通度为K.

图39:连通图

图39是连通图,但不是重连通图,图中有4个关节点分别是:A、B、D、G。比如删除顶点B及相关联的边后,原图就变为:


图40:连通分量

可以看到,图被分割为各自独立的 3 部分,顶点集合分别为:{A、C、F、L、M、J}、{G、H、I、K} 和 {D、E}。

判断重连通图的方法

对于任意一个连通图来说,都可以通过深度优先算法获得一棵深度优先生成树。例如,图39通过深度优先搜索获得的深度右先生成树为:


图41:深度优先生成树

虚线表示遍历生成树时未用到的边,简称"回边"。 也就是说图中有,但是遍历时没有用到。

在深度优先生成树中,图中的关节点有两种特性:

  1. 首先判断整棵树的根结点,如果树根有两条或者两条以上的子树,那么该顶点一定是关节点。因为一旦树根丢失,生成树就会变成森林
  2. 然后判断生成树中每个非叶子结点,以该节点(假设结点M)为根结点的每棵子树中(每棵子树中都必须有),如果有结点的回边与M结点的祖宗结点相关联,那么M结点就不是关节点(M不是关节点),反之,就是关节点。

注意:必须是和该非叶子结点的祖宗结点(不包含该结点本身)相关联,才说明子结点不是关节点。

按图41的生成树来说,利用两个特性判断每个顶点是否为关节点

  • 首先根结点A有两棵子树,所以A是关节点。然后判断树中所有非叶子结点,也就是L、M、B、D、H、K、G
  • L结点为根结点的子树中,B结点有回边关联A,所以L不是关节点
  • 在以 M 结点为树根的子树中,J 结点和 B 结点都有回边关联 M 结点的祖宗结点,所以,M 不是关节点;
  • 以 B 结点为根结点的 3 棵子树中,只有一棵子树(只包含结点 C )与 B 结点的祖宗结点 A 有关联,其他两棵子树没有,所以结点 B 是关节点;
  • 以 D 结点为根结点的子树中只有结点 E,且没有回边与祖宗结点关联,所以,D 是关节点;
  • 以 H 结点为根结点的子树中, G 结点与 B 结点关联,所以, H 结点不是关节点;
  • K 结点和 H 结点相同,由于 G 结点与祖宗结点 B 关联,所以 K 结点不是关节点;
  • 以 G 结点为根结点的子树中只有一个结点 I,没有回边,所以结点 G 是关节点;

拓扑排序

关键路径

迪杰斯特拉算法

佛洛伊德算法

未完待续。这几个算法有点头疼

你可能感兴趣的:(数据结构(九)图)