一、图的概念
图是一种复杂想非线性数据结构。设一个图用G表示,则图的二元组定义为G=(V ,E ),其中,V是定点集合,即V={ Vi | 0<=i<=n-1 , n>=0 },n为图G中的定点数,当n=0时则V为空集;E是V上的一个二元关系 ,即是V上定点的序偶或无序对(每个无序对(x,y)是两个对称序偶< x,y >和< y,x >的简写形式)的集合。对于V上的每个定点,在E中都允许有任意多个前驱和任意多个后继,即对每个定点的前驱和后继的个数不加限制。
对于一个图G,若E是序偶的集合,则每个序偶对应图形中的一条有向边,若E是无序对的集合,则每个无序对对应图形中的一条无向边,所以可把E看作是边的集合,但每条边的两个端点必须互不相同,即图中不允许存在自封闭的边,例如,出现( V1,V1)的边是非法的。这样,图的二元组定义可叙述为:图由顶点集和边集所组成。针对图G,顶点集和边集可分别记为V(G)和E(G)。若顶点集为空,则边集必然为空,若顶点集非空,则边集可以为空也可以非空,当为空时图G中的顶点均为孤立顶点。
对于一个图G,若边集E(G)中为有向边,则称此图为有向图,若边集E(G)中为无向边,则称此图为无向图。图8-1中的G1和G2分别为无向图和有向图,G1中每个顶点里的数字为该顶点的序号(从数字0开始),顶点的值没有在图形中给出,G2中每个顶点里的字母假定为该顶点的值或关键字,顶点外面的数字为该顶点的序号。当存储一个图形式,将按照序号把每个顶点的值依次存储到一个数组或文件中,待需要访问。G1和G2对应的顶点集和边集(假定每个顶点用序号i表示)分别如下:
V(G1)={0,1,2,3,4,5 }
E(G1)={ (0,1),(0,2),(0,3),(0,4),(1,4),(2,4),(2,5),(3,5),(4,5) }
E(G2)={ 0,1,2,3,4 }
E(G2)={ <0,1>,<0,2>,<1,2>,<1,4>,<2,1>,<2,3>,<4,3> }
若用G2顶点的值表示其顶点集和边集,则如下所示:
V(G2)={ A,B,C,D,E }
E(G2)={ <A,B>,<A,C>,<B,C>,<B,E>,<C,B>,<C,D>,<E,D> }
在日常生活中,图的应用到处可见,例如,各种交通图、线路图、结构图、流程图等。
二、图的基本术语
1、端点和邻接点
在一个无向图中,若存在一条边(Vi,Vj),则称Vi,Vj为此边的两个端点,并称它们互为邻接点,即Vi是Vj的一个邻接点,Vj也是Vi的一个邻接点。
在一个有向图中,若存在一条边< Vi,Vj >,则称此边是顶点Vi的一条出边、顶点Vj的一条入边;称Vi为此边的起始端点,简称起点或始点,Vj为此边的终止端点,简称终点;称Vi和Vj互为邻接点并称Vj是Vi的出边邻接点,Vi是Vj的入边邻接点。例如,在图G2中,顶点C有两条出< C,B >和< C,D >,两条入边< A,C >和< B,C>,顶点C的两个出边邻接点为B和D,两个入边邻接点为A和B。
2、顶点的度、入度、出度
无向图中顶点V的度定义为以该顶点为一个端点的边的数目,记为D(V)。如图8-1G1中V0顶点的度为4,V1顶点的度为2.有向图中顶点V的度有入度和出度之分,入度是该顶点的入边数目,记为ID(V);出度是该顶点的出边数目,记为OD(V);顶点V的度等于它的入度和出度之和,即D(V)=ID(V)+OD(V)。如图8-1G2中定点A的入度为0,出度为2,度为2;顶点C的入度为2,出度为2,度为4。若把有向图中顶点的入度和出度的概念也应用于无向图,则无向图中每个顶点的度既是它的入度也是他的出度,即度、入度和出度具有相同的值,这是因为每条无向边等于一条入边和一条出边。
每条边连接着两个顶点,使这两个顶点的度数分别增1,总和增2,所以全部顶点的度数为所有边数的两倍,或者说,边数为全部顶点的度数的一半。此结论对于无向图和有向图都是成立的。
3、完全图、稠密图、稀疏图
若无向图中的每两个顶点之间都存在着一条边,有向图中的每两个顶点之间都存在着方向相反的两条边,则称此图为完全图。显然,若完全图是无向的,则图中包含n(n-1)/2条边,即等于从n个数中每次取两个数的所有组合数;若完全图是有向的,则图中包含n(n-1)条边,即每个顶点向其余n-1个顶点都有一条出边。当一个图接近完全图时,则称它为稠密图,相反地,若一个图含有较少的边数,即边数远远小于顶点数的平方值时,则称他为稀疏图。
4、子图
设有两个图G=(V,E)和G'=(V',E'),若G包含G',则称G'是G的子图,其中,V'是V的子集,即V'包含于V,E'是E的子集,即E'包含于E。
5、路径和回路
在一个图G中,从顶点V到顶点V'的一条路径是一个顶点序列,其路劲长度是指该路径上经过的边的数目。若在一条路径上,除了开始和结束顶点可以相同外,其余所有顶点均不同,则称此路径为简单路径;若开始和结束顶点相同,则称此简单路径为简单回路或简单环。若一条路径不是简单路径,必然其内部出现重复结点,并形成内部的回路。
6、连通和连通分量
在无向图G中,若从顶点Vi到顶点Vj有路径,则称Vi和Vj是连通的。若图G中任意两个顶点都连通,则称G为连通图,否则称为非连通图。无向图G的极大连通子图称为G的连通分量。显然,一个连通图的任何连通分量都能够包含其全部顶点,但可能包含不同的边集,而非连通图具有多个不同的连通分量。
7、强连通图和强连通分量
在有向图G中,若从顶点Vi到顶点Vj有路径,则称从Vi到Vj是连通的。若图G中的任意两个顶点Vi和Vj都连通,即从Vi到Vj和从Vj到Vi都存在路径,则称G是强连通图。有向图G的极大连通子图称为G的强连通分量。显然,强连通图的任何一个强连通分量都能够连通所有顶点,而非强连通图需要有多个强连通分量才能够包含所有顶点。
8、权和网
在一个图中,每条边可以标上具有某种含义的数值,此数值称为该边的权,通常权为一个非负实数。例如,对于一个反映城市交通线路的图,边上的权可表示该线路的长度或等级。边上带有权的图称做带权图,带权图又称为网。图8-3中的G5和G6就分别是一个无向带权图和有向带权图。
在带权图中,两顶点之间的路径上所有边的权值之和被称为这两个顶点之间的带权路径长度。例如对于图8-3(a),顶点V0到V4的一条路径为(V0,V2)7、(V2,V3)6、(V3,V4)15,其路径长度为3,而带权路径长度为28。
二、图的存储结构
1、邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵。设G=(V,E)是具有n个顶点的图,顶点序号依次为0,1,2。。。。。。n-1,则G的邻接矩阵是具有如下定义的n阶方阵:
1 对于无向图,(Vi,Vj)或(Vj,Vi)
amx[ i,j ]= 对于有向图,< Vi, Vj >
0 E(G)不存在对应边
例如,对于图8-1中的G1和G2,它们的邻接矩阵分别如下面的amx1和amx2所示,由amx1可以看出,无向图的邻接矩阵是按主对角线对称的。
若图G是一个带权图,则用邻接矩阵便是也很方便,只要把1换为相应边上的权值,把非对角线上的0换为某一个很大的特定实数即可,用这个特定实数表示相应边不存在,通常标记为∞或MaxValue,它要大于图G中所有边上的权值之和。
例如,对于图8-3中的带权图G5和G6,它们的邻接矩阵分别如下面的amx5和amx6所示。
采用邻接矩阵表示图便于查找图中任一条边或边上的权。例如,要查找边(i,j)或<i,j>,则只要查找邻接矩阵找那个第i行第j列的元素amx[i][j]是否为一个有效值(即非零值和非MaxValue值)即可;若该元素为一个有效值,则表明此边存在,否则此边不存在。因邻接矩阵中的元素可以随机存取,所以查找一条边的时间复杂度为O(1)。
采用邻接矩阵也便于查找图中任一顶点的度,对于无向图,顶点的度就是对应第i行或第i列上有效元素的个数;对于有向图,顶点Vi的出度就是对应第i行上有效元素的个数,顶点Vi的入度就是对应第i列上有效元素的个数。由于求任一顶点的度需访问对应一行或一列中的所有元素,所以其时间复杂度为O(n),n表示图中的顶点数,亦即邻接矩阵的阶数。
从图的邻接矩阵中查找任一顶点的一个邻接点或所有邻接点也同样方便。例如,要查找Vi的一个邻接点(对于无向图)或出边邻接点(对于有向图),则只要在第i行上查找出一个有效元素,以该元素所在的列号j为序号的顶点Vj就是所求的一个邻接点或出边邻接点。一般算法要求依次查找出一个顶点Vi的所有邻接点(对于有向图则为所有出边邻接点或入边邻接点),此时需访问对应第i行或第i列上的所有元素,所以其时间复杂度为O(n)。
图的邻接矩阵的存储需要占用n*n个实数存储空间(因点点的权值为实数,对于无权图,元素0和1也是实数),所以其空间复杂度为O(n^2)。对于这种存储结构用于表示稠密图能够充分利用存储空间,但若用于稀疏图,则将使邻接矩阵变为稀疏矩阵,从而造成存储空间的很大浪费。
图的邻接矩阵表示只是利用了一个二维数组存储顶点之间相邻关系和相应边上的权值信息,若要存储图中n个顶点的信息,则还需要使用一个具有n个元素的一维数组,元素的类型可以是整数型或实数型,当然也可以是系统根基类Object及其继承类。
2、邻接表
邻接表是对图中的每个顶点建立一个邻接关系的单链表,并把他们的表头指针用一维数组(向量)存储的一种图的表示方法。为顶点Vi建立的邻接关系的单链表称做Vi邻接表。Vi邻接表中的每个结点用来存储相应的一条边的信息,因而被称为边结点。Vi邻接表中的结点数,对于无向图来说,等于Vi的边数,或邻接点数或度数;对于有向图来说,等于Vi的出边数或出边邻接点数或出度数。边结点通常包含3个域:一是邻接点域,用来存储顶点Vi的一个邻接顶点Vj的序号j;二是权值域,用来存储边(Vi,Vj)或< Vi,Vj >上的权;三是链接域,用来链接Vi邻接表中的下一个边结点。在这三个域中,邻接点域和邻接域是必不可少的,权值域可根据情况取舍若表示的是无权图,则应省去此域。
对于每个顶点Vi的邻接表,需要设置一个表头指针,若图G中有n个顶点,则有n个表头指针。为了便于随机访问任一顶点的邻接表,需要把这n个表头指针用一个向量(一维数组)存储起来,其中第i个分量存储Vi邻接表的表头指针。这样,图G就可以由这个表头向量来表示和存取。
图8-1中的G1和图8-3中的G6对应的邻接表分别如图8-4(a)和(b)所示。
图的邻接表表示不是唯一的,因为在每个顶点的邻接表中,各边结点的链接次序可以任意安排,其具体链接次序与边的输入次序和生成算法有关。
下面给出建立图的邻接表中所使用的边结点类的定义,假定类名用EdgeNode表示。
//定义邻接表中的边结点类型
public class EdgeNode {
int adjvex; //邻接点域
int weight; //边的权值域,假定为整型,对于无权图,边的权值为1
EdgeNode next; //指向下一个边结点的链接域
//对于无权图中的边结点进行初始化
public EdgeNode(int adj,EdgeNode nt)
{
adjvex=adj;
next=nt;
weight=1;
}
//对于有权图中的边结点进行初始化
public EdgeNode(int adjvex, int weight, EdgeNode next) {
this.adjvex = adjvex;
this.weight = weight;
this.next = next;
}
}
在图的邻接表中便于查找一个顶点的边(出边)或邻接点(出边邻接点),只要首先从表头向量中取出对应的表头指针,然后从表头指针出发进行查找即可。由于每个顶点但链表的平均长度为e/n(对于有向图)或2e/n(对于无向图),所以此查找运算的时间复杂度为O(e/n)。但要从有向图的邻接表中查找一个顶点的入边或入边邻接点,那就不方便了,需要扫描所有顶点邻接表中的边结点,因此其时间复杂度为O(n+e)。对于那些需要经常查找顶点入边或入边邻接点的运算,可以为此专门建立一个逆邻接表,该表中每个顶点的单链表不是存储该顶点的所有出边的信息,而是存储所有入边的信息,邻接点域存储的是入边邻接点的序号。
图8-5就是为图8-3中的G6建立的逆邻接表,从此表中很容易求出每个顶点的入边、入边上的权、入边邻接点和入度。
在图的邻接表和逆邻接表表示中,表头向量需要占用n个引用(指针)存储空间,所有边结点需要占用2e(对于无向图)或e(对于有向图)个边结点空间,所以其空间复杂度为O(n+e)。这种存储结构用于表示稀疏图比较节省存储空间,因为只需要很少的边结点,若用于表示稠密图,则将占用较多的存储空间,同时也将增加在每个顶点邻接表中查找结点的时间。
图的邻接表表示和图的邻接矩阵表示虽然方法不同,但也存在着对应的关系。邻接表中每个顶点Vi的单链表对应邻接矩阵中的第i行,整个邻接表可看做事邻接矩阵的带行指针向量的邻接存储。整个逆邻接表可看做事邻接矩阵的带列指针向量的链接存储,整个逆邻接表可看做是邻接矩阵的带列指针向量的链接存储。
3、边集数组
边集数组是利用一维数组存储图中所有边的一种图的表示方法。该数组中所含元素的个数要大于等于图中的边数,每个元素用来存储一条边的起点,终点(对于无向图,可选定边的任一端点为起点或终点)和权值(若有的话),各边在数组中的次序可任意安排,也可根据具体要求而定。边集数组只是存储图中所有边的信息,若需要存储顶点信息,同样需要一个具有n个元素的一维数组。图8-1中的G2和图8-3中的G5所对应的边集数组分别如图8-6(a)和(b)所示。
边集数组中的元素类型定义如下:
//定义边集数组中的元素类型
public class EdgeElement {
int fromvex; //边的起点域
int endvex; //边的终点域
int weight; //边的权值域,假定为整型,对于无权图,权值可为1
//对于无权图进行初始化
public EdgeElement(int v1,int v2)
{
fromvex=v1;
endvex=v2;
weight=1;
}
//对于有权图进行初始化
public EdgeElement(int fromvex, int endvex, int weight) {
this.fromvex = fromvex;
this.endvex = endvex;
this.weight = weight;
}
}
在边集数组中查找一条边或一个顶点的度都需要扫描整个数组,所以其时间复杂度为O(e)。边集数组适合那些对边依次进行处理的运算,不适合对顶点的运算和对任一条边的运算。边集数组表示的空间复杂度为O(e)。从空间复杂度上讲,边集数组也适合表示稀疏图。
三、图的抽象数据类型和接口类
图的抽象数据类型同样包括数据和操作两个部分。数据部分为一个图,它可以采用任何存储结构;操作部分包括初始化一个图,建立一个图,返回图的类型(有向图或有无权)返回图的顶点数,返回图的边数,从图中查找一条边是否存在,向图中插入一条边,从图中删除一条边,返回图中一个顶点的度、入度和出度,按照二元组的形式输出一个图,采用深度优先搜索方法遍历一个图,采用广度优先搜索树方法遍历一个图等一些常用操作。下面给出图的抽象数据类型的具体定义。
public interface Graph {
boolean createGraph(EdgeElement []d); //根据边集数组参数d建立一个图
int graphType(); //返回图的类型
int vertices(); //返回图中的顶点数
int edges(); //返回图中的边数
boolean find(int i,int j); //从图中查找一条边(i,j)是否存在
boolean putEdge(EdgeElement theEdge); //从图中插入一条边theEdge
boolean removeEdge(int i,int j); //从图中删除一条边
int degree(int i); //返回顶点i的度
int ingree(int i); //返回顶点i的入度
int outgree(int i); //返回顶点i的出度
void output(); //以图的顶点集和边集的形式输出一个图
void depthFirstSearch(int v); //从顶点v开始深度优先搜索遍历图
void breadthFirstSearch(int v); //从顶点v开始付昂度优先搜索遍历图
void clearGraph(); //清除图中的所有内容
}
四、图的邻接矩阵和邻接表存储类
1、图的邻接矩阵存储类
在图的邻接矩阵存储类中所包含的数据成员有表示图的顶点个数的整型变量n;表示图的边数的整型变量e;表示图的类型的整型变量type,用type的取值0,1,2,3分别表示无向无权图、无向有权图、有向无权图和有向有权图4中类型;表示存储图的邻接矩阵的二维整型数组a,这里假定边的权值为整型,实际需要时可以替换为其他任何类型;表示带权图中的边不存在时所使用的常量MaxValue,在这里把它的值定义为1000,实际使用时它要大于图中所有有效边上的权值之和。当然,邻接矩阵存储类要实现图的接口类Graph中定义的全部方法,并且还定义有自己的getArray()
方法,用来返回邻接矩阵的引用,即返回二维数组a的值,以便从类外能够访问它的元素值。假定图的邻接矩阵存储类用标志符AdjacencyGraph表示,下面给出了该类的具体定义,其中有些方法的实现将稍后介绍。
2、图的邻接表存储类
在图的邻接表存储类中,同样包含表示图的顶点数的数据成员n、表示图的边数的数据成员e和表示图的类型的数据成员type,它的邻接表数据成员也同样用标志符a表示,a为一个一维数组,元素类型为边结点类型EdgeNode,利用其中的每个元素存储相应顶点邻接表的表头指针(引用)。该邻接表存储类假定用标识符LinkedGraph表示,它也要实现图的接口类中定义的所有方法,同样也定义有自己的getArray()方法,用来返回邻接表的引用,下面给出了该类的数据成员的定义和构造函数的定义,其他未给出的成员方法的定义与上面的邻接矩阵存储类中已经给出的完全相同,当然有些方法在具体实现上会有差别,这将在稍后介绍。
五、图的遍历
图的遍历就是从图中指定的某个顶点(称此顶点为初始点)出发,按照一定的搜索方法对图中的所有顶点各做一个访问的过程。图的遍历比树的遍历要复杂,因为从树根到达树中的每个结点只有一条路径,而从图的初始点到达图中的每个顶点可能存在着多条路径。当顺着图中的一条路径访问过某一顶点后,可能还会顺着另一条路径回到该顶点。为了避免重复访问图中的同第顶点,必须记住每个顶点是否被访问过,为此可设置一个辅助数组visited[n],它的每个元素初始值均为逻辑假(false),表明开始时所有顶点都未被访问过,一旦访问了顶点Vi,就把对应元素visited[i]置为真(true),表明vi已被访问过。
根据搜索方法的不同,图的遍历有两种:一种叫做深度优先遍历;另一种叫做广度优先搜索遍历。
1、深度优先搜索遍历
深度优先搜索遍历类似于对树的先根遍历,它是一个递归过程。此过程首先访问一个顶点vi(一开始为初始点),并将其标记为已访问过,然后从Vi的任一个未被访问过的邻接点(对于有向图是出边邻接点)出发进行深度优先搜索遍历,当Vi的所有邻接点均被访问过时,则退回到上一个顶点Vk,从Vk的另一个未被访问过的邻接点出发进行深度优先搜索遍历,直到退回到初始点并且该初始点也没有被访问过的邻接点为止。
下面结合图8-7所示的无向图G7分析以V0作为初始点的深度优先搜索遍历的过程。
(1)访问顶点V0,并将visited[0]置为真,表明V0已被访问过,接着从V0的一个未被访问过的邻接点V1(V0的3个邻接点V1,V2和V3都未被访问过,假定取V1访问)出发进行深度优先搜索遍历。
(2)访问顶点V1,并将visited[1]置为真,表明V1已被访问过,接着从V1的一个未被访问过的邻接点V4(V1的4个邻接点中只有V0被访问过,其余3个邻接点V4,V5,V6均未被访问过,假定取V4访问)出发进行深度优先搜索遍历。
(3)访问顶点V4,并将visited[4]置为真,表明V4已被访问过,接着从V4的一个未被访问过的邻接点V5(V4的两个邻接点为V1和V5,V1被访问过,只剩下一个未被访问)出发进行深度优先搜索遍历。
(4)访问顶点V5,并将visited[5]置为真,表明V5已被访问过,接着V5的两个邻接点V1和V4都被访问过,所以按原路退回到上一个顶点V1,V1的4个邻接点中有3个已被访问过,此时只能从未被访问过的邻接点V6出发进行深度优先搜索遍历。
(5)访问顶点V6,并将visited[6]置为真,表明V6已被访问过,接着从V6的一个未被访问过的邻接点V2(只此一个)出发进行深度优先搜索遍历。
(6)访问顶点V2,并将visited[2]置为真,表明V2已被访问过,接着因V2的所邻接点(即V0和V6)都被访问过,所以按原路退回到上一个顶点V6,同理,由V6退回到V1,由V1再退回到V0,再从V0的一个未被访问过的邻接点V3(只此一个)出发进行深度优先搜索遍历。
(7)访问顶点V3,并将visited[3]置为真,表明V3已被访问过,接着因V3的所有邻接点(它仅有一个邻接点V0)都被访问过,所以退回到上一个顶点V0,又因V0的所有邻接点都已被访问过,并且 V0为初始点,所以再退回,实际上就结束了对图G7的深度优先搜索遍历的过程,返回到调用此算法的函数中去。
从以上对无向图G7进行深度优先搜索遍历的过程分析可知,从初始点V0出发,访问G7中各顶点的次序为V0,V1,V4,V5,V6,V2,V3。
由于在对图中的顶点进行深度优先搜索时的路径不同,得到的顶点序列也不同,但它们都是正确的。例如,顶点序列V0,V2,V6,V1,V5,V4,V3也是对图G7从初始点V0出发进行的深度优先搜索的正确序列。也就是说,对图进行深度优先搜索遍历时得到的顶点序列不是唯一的。
图的深度优先搜索遍历的过程分为两个函数定义来实现,一个称为驱动函数;另一个称为工作函数。驱动函数为一个非递归函数,它起到与外界接口和调用内部递归函数的作用,假定函数名用depthFirstSearch表示,它带有一个整型参数v,用来接收其初始顶点的序号,该函数的函数体已在上面定义的图类中给出,它的功能是定义和初始化visited数组和进行递归函数dfs的调用;工作函数是只允许在图类内部调用的私有递归函数,假定函数名用dfs表示,它带有两个参数,一个初始顶点参数;另一个为接收visited数组的参数,该函数具体完成对图中每个顶点的访问任务。下面分别给出在邻接矩阵图类和邻接表类中定义的对图进行深度优先搜索遍历的递归算法描述。
//进行深度优先搜索遍历的内部递归方法的定义
private void dfs(int i,boolean[]visited)
{
//从初始点Vi出发深度优先搜索由邻接矩阵所表示的图
System.out.print(i+" "); //假定访问顶点Vi以输出该顶点的序号代之
visited[i]=true; //标记Vi已被访问过
for(int j=0;j<n;j++) //依次搜索Vi的每个邻接点
{
if(a[i][j]!=0&&a[i][j]!=MaxValue&&!visited[j])
{
//若Vi的一个有效邻接点Vj未被访问过,则再从Vj出发进行递归调用
dfs(j,visited);
}
}
}
//进行深度优先搜索遍历的内部递归方法的定义
private void dfs(int i,boolean[]visited)
{
//从初始点Vi出发深度优先搜索由邻接表所表示的图
System.out.print(i+" "); //假定访问顶点Vi以输出该顶点的序号代之
visited[i]=true; //标记Vi已被访问过
EdgeNode p=a[i]; //把Vi邻接表的表头指针赋给p
while(p!=null)
{
int j=p.adjvex; //得到Vi的一个邻接点Vj
if(!visited[j]) //若Vi的一个有效邻接点Vj未被访问过
{
dfs(j,visited); //则从Vj出发进行递归调用
}
p=p.next; //使p指向下一个边结点
}
}
图8-7的G7所对应的邻接矩阵和邻接表分别如图8-8(a)和(b)所示,请自行结合他们分析上面的两个算法,看从顶点V1出发得到的深度优先搜索遍历的顶点序列是否分别为以下序列。
序列1:1,0,2,6,3,4,5
序列2:1,6,2,0,3,5,4
当图中每个顶点的序号确定后,图的邻接矩阵表示是否是唯一的,所以根据dfs算法从某一顶点出发进行深度优先搜索遍历时访问各顶点的次序也是唯一的;但图的邻接表表示不是唯一的,它与边的输入次序和链接次序有关,所以对于同一个图的不同邻接表,根据dfs算法从某一顶点出发进行深度优先搜索遍历时访问各顶点的次序也可能不同。另外,对于同一个邻接矩阵或邻接表所表示的图,如果指点的出发点不同,则将得到不同的遍历序列。
从以上两个算法可以看出,若图是连通的,对邻接矩阵表示的图进行深度优先搜索遍历是,可能需要扫描邻接矩阵中的每一个元素,所以其时间复杂度为O(n^2);对邻接表表示的图进行深度优先搜索遍历时,可能需要扫描邻接表中的表头数组的每个元素和所有边结点,所以其时间复杂度为O(n+e);二者的空间复杂度均为O(n)。
2、广度优先搜索遍历
广度优先搜索遍历类似于对树按层遍历,其遍历过程首先访问初始点Vi,并将其标记为已访问过,接着依次访问Vi的所有未被访问过的邻接点,各邻接点的访问次序可以任意,假定访问次序为Vi1,Vi2,Vi3、........Vit并均标记为已访问过,然后再按照这t个邻接点的次序,依次访问他们每个顶点的所有未被访问过的邻接点,并均标记为已访问过,按层向下类推,直到图中所有和初始点Vi有路径相通的顶点都被访问过为止。
下面结合图8-9所示的有向图G8分析从V0出发进行广度优先搜索遍历的过程。
(1)访问初始点V0,并将其标记为已访问过。
(2)访问V0的所有未被访问过的的邻接点V1和V2,并将它们标记为已访问过。
(3)访问V0的所有未被访问过的邻接点V3,V4和V5,并将它们标记为已访问过。
(4)访问顶点V2的所有未被访问过的邻接点V6(它的两个邻接点中的一个顶点V5已被访问过),并将其标记为已访问过。
(5)访问顶点V3的所有未被访问过的邻接点V7(只此一个邻接点且没有被访问过),并将其标记为已访问过。
(6)访问顶点V4的所有未被访问过的邻接点,因V4的邻接点V7(只此一个)以被访问过,所以此步不访问任何顶点。
(7)访问顶点V5的所有未被访问过的邻接点V8,并将其标记为已访问过。
(8)访问顶点V6的所有未被访问过的邻接点,因V6的仅一个邻接点V8已被访问过,所以此步不访问任何顶点。
(9)依次访问V7和V8的所有未被访问过的邻接点,因它们均没有邻接点,所以整个遍历过程到此结束。
从以上对有向图G8进行广度优先搜索遍历的过程分析可知,从初始点V0出发,得到的访问各顶点的次序为V0,V1,V2,V3,V4,V5,V6,V7,V8。
同深度优先搜索遍历的算法一样,广度优先搜索遍历的算法也需要使用两个函数定义来实现,一个是用于外部接口的驱动函数,其函数名假定用breadthFirstSearch表示,首先定义和初始化用于记录顶点访问标记的visited逻辑数组,然后调用bfs函数完成图的广度优先搜索遍历。
在bfs函数中需要使用一个队列,用来依次记住被访问过的顶点。函数开始时,对初始点Vi访问后插入队列中,以后每从队列中删除一个元素,就依次访问它的每一个未被访问过的邻接点,并令其进队,这样,当队列为空时,表明所有与初始点有路径相通的顶点都已访问完毕,算法到此结束。下面分别给出在邻接矩阵图类和邻接表图类中给出的对图进行广度优先搜索遍历的工作函数算法的描述。
//进行广度优先搜索遍历的内部非递归方法的定义
private void bfs(int i,boolean[] visited)
{
//从初始点Vi出发广度优先搜索由邻接矩阵所表示的图
Queue q=new SequenceQueue(); //定义和创建一个空队列q
System.out.print(i+" "); //访问初始点Vi以输出序号i代之
visited[i]=true; //标记初始点Vi已访问过
q.enter(i); //将已访问过的初始点序号i入队
while(!q.isEmpty()) //当队列非空时进行循环处理
{
int k=(Integer)q.leave(); //删除队首元素,k第1次值为i
for(int j=0;j<n;j++)
{
if(a[k][j]!=0&&a[k][j]!=MaxValue&&!visited[j])
{
//对Vk的一个未被访问过的邻接点Vj进行处理
System.out.print(j+" "); //访问顶点Vj以输出序号j代之
visited[j]=true; //标记Vj已访问过
q.enter(j); //顶点序号j入队
}
}
}
}
//进行广度优先搜索遍历的内部非递归方法的定义
private void bfs(int i,boolean[] visited)
{
//从初始点Vi出发广度优先搜索由邻接表所表示的图
Queue q=new SequenceQueue(); //定义和创建一个空队列q
System.out.print(i+" "); //访问初始点Vi
visited[i]=true; //标记初始点Vi已访问过
q.enter(i); //将已访问过的初始点序号i入队
while(!q.isEmpty()) //当队列非空时进行循环处理
{
int k=(Integer)q.leave(); //删除队首元素,k的第1次值为i
EdgeNode p=a[k]; //将顶点Vk的表头指针赋给p
while(p!=null) //依次搜索Vk的每一个邻接点
{
int j=p.adjvex; //Vj为Vk的一个邻接点
if(!visited[j]) //若Vj以输出序号代之
{
System.out.print(j+" "); //访问Vj以输出序号代之
visited[j]=true; //标记Vj已访问过
q.enter(j); //顶点序号j入队
}
p=p.next; //p指向下一个边结点,得到Vk的下一个邻接点
}
}
}
请自行结合图8-8的(a)和(b)分析上面的两个算法,看从顶点V1出发得到的广度优先搜索遍历的顶点序列是否分别为以下序列。
序列1:1,0,4,5,6,2,3
序列2:1,6,5,4,0,2,3
与图的深度优先搜索遍历一样,对于图的广度优先搜索遍历,若采用邻接矩阵表示,其时间复杂度为O(n^2),若采用邻接表表示,其时间复杂度为O(n+e),二者的空间复杂度均为O(n)。
由图的某个顶点出发进行广度优先搜索遍历时,访问各顶点的次序对于邻接矩阵来说是唯一的,对于邻接表来说,可能因邻接表的不同而不同,这一点也与图的深度优先搜索遍历是的情形一样。
3、非连通图的遍历
上面所讨论的图的深度优先搜索遍历算法和图的广度优先搜索算法,对于无向图来说,若无向图是连通图,则能够访问到图中的所有顶点,若无向图是非连通图,则只能访问到初始点所在连通分量中的所有顶点,其他连通分量中的顶点是不可能访问到的,为此需要从其他每个连通分量中选定初始点,分别进行搜索遍历,才能够访问到图中的所有顶点;对于有向图来说,若从初始点到图中的每个顶点都有路径,则能够访问到图中的所有顶点,进行搜索遍历,直到图中的所有顶点都被访问过为止。
为了能够访问到任意图中的所有顶点,方法很简单,只要以图中未被访问到的每个顶点作为初始点,去调用dfs或bfs算法即可。具体地说,当进行深度优先搜索遍历时,在depthFirstSearch函数定义中的"dfs(v,visited);"语句之后添加如下一条for循环语句:
for(int i=0;i<n;i++)
{
if(visited[i]==false)
{
dfs(i,visited);
}
}
或者当进行广度优先搜索遍历时,在breadthFirstSearch函数定义中的"bfs(v,visited);"之后添加如下一条for循环语句:
for(int i=0;i<n;i++)
{
if(visited[i]==false)
{
bfs(i,visited);
}
}
若一个无向图是连通的,或者从一个有向图的顶点V0到其余每个顶点都是有路径的,则执行此循环语句时,因条件表达式始终为假,所以不会再调用一次相应的遍历算法;否则,要再执行一次或多次相应的遍历算法才能结束整个遍历过程。对于无向图来说,每次调用相应的遍历算法将得到一个连通分量,有多少次调用过程,就说明该图有多少个连通分量。
六、对图的其他运算的算法
1、根据图的边集数数组建立相应图类中的邻接矩阵和邻接表
建立按邻接矩阵所存储的图
//根据边集数组参数d建立一个图
public boolean createGraph(EdgeElement[] d) {
//根据一个图的边集数组参数d建立按邻接矩阵所存储的图
for(int i=0;i<d.length;i++) //每次处理d数组中的一条边
{
if(d[i]==null)
{
System.out.println("边集中元素为空,停止建图,返回假!");
return false;
}
int v1,v2;
v1=d[i].fromvex; //用V1,V2保存边的两个顶点
v2=d[i].endvex;
if(v1<0||v1>n-1||v2<0||v2>n-1||v1==v2)
{
System.out.println("边的顶点序号无效,停止建图,返回假!");
return false;
}
if(a[v1][v2]!=0&&a[v1][v2]!=MaxValue)
{
System.out.println("边已经存在,停止建图,返回假!");
return false;
}
if(type==0) //根据d[i]建立无向无权图所对应的a中的元素
{
a[v1][v2]=a[v2][v1]=1;
}
else if(type==1) //根据d[i]建立无向有权图所对应的a中的元素
{
a[v1][v2]=a[v2][v1]=d[i].weight;
}
else if(type==2) //根据d[i]建立有向无权图所对应的a中的元素
{
a[v1][v2]=1;
}
else //根据d[i]建立有向有权图所对应的a中的元素
{
a[v1][v2]=d[i].weight;
}
}
e=d.length; //把图中的边数设置为边集数组d的长度值
return true;
}
建立按邻接表所存储的图
//根据边集数组参数d建立一个图
public boolean createGraph(EdgeElement[] d) {
//根据一个图的边集数组参数d建立按邻接表所存储的图
for(int i=0;i<d.length;i++) //每次处理d数组中的一条边
{
if(d[i]==null)
{
System.out.println("边集中元素为空,停止建图,返回假!");
return false;
}
int v1,v2;
v1=d[i].fromvex; //用v1和v2保存边的两个顶点
v2=d[i].endvex;
if(v1<0||v1>n-1||v2<0||v2>n-1||v1==v2)
{
System.out.println("边的顶点序号无效,停止建图,返回假!");
return false;
}
EdgeNode p=a[v1];
while(p!=null) //检查边是否已经存在
{
if(p.adjvex!=v2)
{
p=p.next; //注意:此时原文是p=p.adjvex;
}
else
{
System.out.println("边已经存在,停止建图,返回假");
return false;
}
}
if(type==0) //根据d[i]建立无向无权图所对应的邻接表中的结点
{
a[v1]=new EdgeNode(v2,a[v1]);//向v1邻接表的表头插入结点
a[v2]=new EdgeNode(v1,a[v2]);//向v2邻接表的表头插入结点
}
else if(type==1) //根据d[i]建立无向有权图所对应的邻接表中的结点
{
a[v1]=new EdgeNode(v2,d[i].weight,a[v1]);
a[v2]=new EdgeNode(v1,d[i].weight,a[v2]);
}
else if(type==2) //根据d[i]建立有向无权图所对应的邻接表中的结点
{
a[v1]=new EdgeNode(v2,a[v1]);
}
else //根据d[i]建立有向有权图所对应的邻接表中的结点
{
a[v1]=new EdgeNode(v2,d[i].weight,a[v1]);
}
}
e=d.length; //把图中的边数设置为边集数组d的长度值
return true;
}
2、从图中查找一条边是否存在
在邻接矩阵存储的图中,只要检查一条边的两个端点所对应的二维数组元素是否为一个有效值即可,若为有效值,则表明存在这条边,应返回true,否则不存在这条边,应返回false。具体算法描述为:
//从图中查找一条边(i,j)是否存在
public boolean find(int i, int j) {
//从邻接矩阵图中查找一条边(i,j)是否存在,若存在则返回真否则返回假
if(i<0||i>n-1||j<0||j>n-1||i==j)
{
System.out.println("边的顶点序号无效,退出运行!");
System.exit(1);
}
if(a[i][j]!=0 && a[i][j]!=MaxValue)
{
return true;
}
else
{
return false;
}
}
在邻接表存储的图中,需要扫描一条边的开始顶点所对应的邻接表,若发现一个边结点中邻接点域的值为要查找的一条边的终止顶点,则表明存在这条边,应返回true,否则当扫描完所有边结点后,应返回false,表明不存在这条边。具体算法描述为:
//从图中查找一条边(i,j)是否存在
public boolean find(int i, int j) {
//从邻接表表示的图中查找一条边(i,j)是否存在,若存在则返回真否则返回假
if(i<0||i>n-1||j<0||j>n-1||i==j)
{
System.out.println("边的顶点序号无效,退出运行!");
System.exit(1);
}
EdgeNode p=a[i]; //把顶点vi邻接表的表头指针赋给p
while(p!=null) //扫描顶点vi的整个邻接表
{
if(p.adjvex==j) //查找到邻接点j则返回真
{
return true;
}
p=p.next; //p指向下一个边结点
}
return false; //对不存在对应的边则返回假
}
3、向图中插入一条边
向图中插入一条边时,若当前图中不存在此边,则应修改表示图中边数的数据成员e的值,使其增加1,然后再完成插入,若已经存在此条边,则返回假。此运算在邻接矩阵图类和邻接表图类中所对应的算法描述分别如下:
向邻接矩阵所表示的图中插入一条边
//从图中插入一条边theEdge
public boolean putEdge(EdgeElement theEdge) {
//向邻接矩阵所表示的图中插入一条边,若存在此边则返回假
int v1,v2;
v1=theEdge.fromvex; //用V1,V2保存边的两个顶点
v2=theEdge.endvex;
if(v1<0||v1>n-1||v2<0||v2>n-1||v1==v2)
{
System.out.println("边的顶点序号无效,停止建图,返回假!");
return false;
}
if(a[v1][v2]==0 && a[v1][v2]==MaxValue)
{
e++; //边数e的值增1
}
else
{
System.out.println("边已经存在,返回假!");
return false;
}
if(type==0||type==1) //对于无向图的处理
{
if(type==0)
{
a[v1][v2]=a[v2][v1]=1; //插入无向无权边
}
else
{
a[v1][v2]=a[v2][v1]=theEdge.weight; //插入无向有权边
}
}
else //对于有向图的处理
{
if(type==2)
{
a[v1][v2]=1; //插入有向无权边
}
else
{
a[v1][v2]=theEdge.weight; //插入有向有权边
}
}
return true;
}
向邻接表所表示的图中插入一条边
//从图中插入一条边theEdge
public boolean putEdge(EdgeElement theEdge) {
//向邻接表所表示的图中插入一条边,若存在此边则返回假
int v1,v2;
v1=theEdge.fromvex; //用v1和v2保存边的两个顶点
v2=theEdge.endvex;
if(v1<0||v1>n-1||v2<0||v2>n-1||v1==v2)
{
System.out.println("边的顶点序号无效,停止建图,返回假!");
System.exit(1);
}
EdgeNode p=a[v1];
while(p!=null) //扫描顶点v1所对应的邻接表
{
if(p.adjvex==v2)
{
break;
}
p=p.next;
}
if(p==null)
{
e++; //图中不存在边(v1,v2)则边数e增1
}
else
{
System.out.println("边已经存在,返回假!");
return false;
}
if(type==0) //无向无权图插入边
{
a[v1]=new EdgeNode(v2,a[v1]);
a[v2]=new EdgeNode(v1,a[v2]);
}
else if(type==1) //无向有权图插入边
{
a[v1]=new EdgeNode(v2,theEdge.weight,a[v1]);
a[v2]=new EdgeNode(v1,theEdge.weight,a[v2]);
}
else if(type==2) //有向无权图插入边
{
a[v1]=new EdgeNode(v2,a[v1]);
}
else
{
a[v1]=new EdgeNode(v2,theEdge.weight,a[v1]);
}
return true;
}
4、从图中删除一条边
从图中删除一条边( i,j ),若此边不存在则返回假,若此边存在,则对于无向图,还要同时删除掉另一条对应边(i,j)。从图中删除一条边后,要修改表示边数的数据成员e的值,使其减少1。此运算在邻接矩阵图类和邻接表图类中所对应的算法描述如下:
从邻接矩阵所表示的图中删除一条边
//从图中删除一条边
public boolean removeEdge(int i, int j) {
//从邻接矩阵所表示的图中删除一条边(i,j)
if(i<0||i>n-1||j<0||j>n-1||i==j)
{
System.out.println("边的顶点序号无效,退出运行!");
System.exit(1);
}
if(a[i][j]==0||a[i][j]==MaxValue)
{
System.out.println("要删除的边不存在,退出运行!");
System.exit(1);
}
if(type==0)
{
a[i][j]=a[j][i]=0; //删除无向无权图中的边
}
else if(type==1) //删除无向有权图中的边
{
a[i][j]=a[j][i]=MaxValue;
}
else if(type==2)
{
a[i][j]=0; //删除有向无权图中的边
}
else
{
a[i][j]=MaxValue; //删除有向有权图中的边
}
e--; //删除一条边后,数据成员e的值减1
return true;
}
从邻接表所表示的图中删除一条边
//从图中删除一条边
public boolean removeEdge(int i, int j) {
//从邻接表表示的图中删除一条边(i,j)
if(i<0||i>n-1||j<0||j>n-1||i==j)
{
System.out.println("边的顶点序号无效,退出运行!");
System.exit(1);
}
EdgeNode p=a[i],q=null; //把顶点vi邻接表的表头指针赋给p
while(p!=null) //扫描顶点vi的整个邻接表
{
if(p.adjvex==j)
{
break;
}
q=p;
p=p.next;
}
if(p==null)
{
System.out.println("要删除的边不存在,返回假!");
return false;
}
if(q==null)
{
a[i]=a[i].next; //从表头删除边结点
}
else
{
q.next=p.next; //删除非表头位置上的边结点
}
if(type==0||type==1)
{
EdgeNode p1=a[j],q1=null;
while(p1!=null)
{
if(p1.adjvex==i)
{
break;
}
q1=p1;
p1=p1.next;
}
if(q1==null)
{
a[j]=a[j].next; //从表头删除边结点
}
else
q1.next=p1.next; //删除非表头位置上的边结点
}
e--; //删除一条边后e的值减少1
return true;
}
5、求出并返回图中一个顶点的度
返回邻接矩阵表示的图中顶点的度
//返回顶点i的度
public int degree(int i) {
//返回以邻接矩阵表示的图中顶点i的度
if(i<0||i>n-1)
{
System.out.println("参数的顶点序号值无效,退出运行!");
System.exit(1);
}
int k=0; //用k统计顶点i的度数
if(type==0||type==1)
{
for(int j=0;j<n;j++)
{
if(a[i][j]!=0 && a[j][i]!=MaxValue)
{
k++;
}
}
return k; //返回无向图中顶点i的度
}
else
{
return inDegree(i)+outDegree(i); //返回有向图中顶点i的度
}
}
返回邻接表存储的图中顶点的度
//返回顶点i的度
public int degree(int i) {
//返回以邻接表存储的图中顶点i的度
if(i<0||i>n-1)
{
System.out.println("参数的顶点序号值无效,退出运行!");
System.exit(1);
}
int k=0; //用k统计顶点i的度数
if(type==0||type==1) //对无向图的情况进行统计
{
EdgeNode p=a[i];
while(p!=null)
{
p=p.next;
k++;
}
return k;
}
else
{
return inDegree(i)+outDegree(i); //返回有向图中顶点i的度
}
}
6、求出并返回图中一个顶点的入度
对于无向图,顶点i的入度和出度实际上就是顶点i的度;对于有向图,顶点i的入度等于邻接矩阵中第i列的所有有效元素之和,或者等于邻接表中所有边结点的邻接点域的值为i的边结点个数。此运算在邻接矩阵图类和邻接表图类中所对应的算法描述分别如下:
邻接矩阵图类中所对应的算法如下:
//返回顶点i的入度
public int inDegree(int i) {
//返回以邻接矩阵表示的图中顶点i的入度
if(i<0||i>n-1)
{
System.out.println("参数的顶点序号值无效,退出运行!");
System.exit(1);
}
int k=0; //用k统计顶点i的入度数
if(type==0||type==1) //对无向图求入度返回其度数
{
return degree(i);
}
for(int j=0;j<n;j++)
{
if(a[j][i]!=0 && a[j][i]!=MaxValue)
{
k++;
}
}
return k;
}
邻接表图类中所对应的算法如下:
//返回顶点i的入度
public int inDegree(int i) {
//返回以邻表表示的图中顶点i的入度
if(i<0||i>n-1)
{
System.out.println("参数的顶点序号值无效,退出运行!");
System.exit(1);
}
int k=0; //用k统计顶点i的入度数
if(type==0||type==1) //对无向图求入度返回其度数
{
return degree(i);
}
for(int j=0;j<n;j++) //双重循环遍历邻接表中的每个边结点
{
EdgeNode p=a[j];
while(p!=null)
{
if(p.adjvex==i)
{
k++;
}
p=p.next;
}
}
return k;
}
7、求出并返回图中一个顶点的出度
无论对于无向图还是有向图,顶点i的出度都等于邻接矩阵中第i行的所有有效元素之个数,或者等于顶点vi邻接表找那个所有边结点的个数。此运算在邻接矩阵图类和邻接表图类中所对应的算法描述分别如下:
返回邻接矩阵存储的图中顶点i的出度
//返回顶点i的出度
public int outDegree(int i) {
//返回以邻接矩阵存储的图中顶点i的出度
if(i<0||i>n-1)
{
System.out.println("参数的顶点序号值无效,退出运行!");
System.exit(1);
}
int k=0;
for(int j=0;j<n;j++) //统计出第i行中有效元素的个数
{
if(a[i][j]!=0 && a[i][j]!=MaxValue)
{
k++;
}
}
return k;
}
返回以邻接表存储的图中顶点i的出度
//返回顶点i的出度
public int outDegree(int i) {
//返回以邻接表存储的图中顶点i的出度
if(i<0||i>n-1)
{
System.out.println("参数的顶点序号值无效,退出运行!");
System.exit(1);
}
int k=0;
EdgeNode p=a[i];
while(p!=null) //统计出Vi邻接表找那个的边结点个数
{
p=p.next;
k++;
}
return k;
}
8、输出一个图所对应的顶点集和边集
对于邻接矩阵表示的图,此运算需要扫描到二维数组中的每个元素;对于邻接表表示的图,此运算需要扫描到邻接表中的每个边结点。此运算在邻接矩阵图类和邻接表图类中所对应的算法描述如下:
输出用邻接矩阵表示一个图所对应的顶点集和边集
//以图的顶点集和边集的形式输出一个图
public void output() {
//输出用邻接矩阵表示一个图所对应的顶点集和边集
int i,j;
System.out.print("V={"); //输出顶点集开始
for(i=0;i<n-1;i++)
{
System.out.print(i+" ");
}
System.out.println(n-1+"}"); //输出顶点集结束
System.out.print("E={"); //输出边集开始
if(type==0 || type==2)
{
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
{
if(a[i][j]!=0 && a[i][j]!=MaxValue)
{
if(type==0) //对无向无权图的处理
{
if(i<j) //使用i<j,是为了避免输出重复边
{
System.out.print("("+i+","+j+"), ");
}
}
else
{
System.out.print("<"+i+","+j+">, ");
}
}
}
}
}
else //对有权图的处理情况
{
for(i=0;i<n;i++)
{
for(j=0;j<n;j++)
{
if(a[i][j]!=0 && a[i][j]!=MaxValue)
{
if(type==1) //对无向有权图的处理
{
if(i<j)
{
System.out.print("("+i+","+j+")"+a[i][j]+",");
}
else
{
System.out.print("<"+i+","+j+">"+a[i][j]+", ");
}
}
}
}
}
}
System.out.println("}"); //输出边集的最后一条花括号
}
输出以邻接表表示一个图所对应的顶点集和边集
//以图的顶点集和边集的形式输出一个图
public void output() {
int i,j;
System.out.print("V={"); //输出顶点集开始
for(i=0;i<n-1;i++)
{
System.out.print(i+" ");
}
System.out.println(n-1+"}"); //输出顶点集结束
System.out.print("E={"); //输出边集开始
for(i=0;i<n;i++) //循环扫描每个顶点的邻接表
{
EdgeNode p=a[i];
while(p!=null) //扫描一个顶点Vi的邻接表
{
j=p.adjvex;
if(type==0) //对无向无权图的处理
{
if(i<j) //使用条件i<j,是为了避免输出重复边
{
System.out.print("("+i+","+j+"), ");
}
}
else if(type==2) //对有向无权图的处理
{
System.out.print("<"+i+","+j+">, ");
}
else if(type==1)
{
if(i<j)
{
System.out.print("("+i+","+j+")"+p.weight+",");
}
}
else
{
System.out.print("<"+i+","+j+">"+p.weight+",");
}
p=p.next;
}
}
System.out.println("}"); //注意:边集的最后一条边的后面多出一个逗号
}