1.41k likes | 1.55k Views
第八章 图. 图的基本概念 图的存储表示 图的遍历与连通性 最小生成树 最短路径 活动网络 小结. 图的基本概念. 图定义 图是由顶点集合 (vertex) 及顶点间的关系集合组成的一种数据结构 : Graph = ( V , E ) 其中 V = { x | x 某个数据对象 } 是顶点的有穷非空集合; E = {( x , y ) | x , y V } 或 E = {< x , y> | x , y V && Path ( x , y )}
E N D
第八章 图 • 图的基本概念 • 图的存储表示 • 图的遍历与连通性 • 最小生成树 • 最短路径 • 活动网络 • 小结
图的基本概念 • 图定义图是由顶点集合(vertex)及顶点间的关系集合组成的一种数据结构: Graph=( V, E ) 其中 V = { x | x 某个数据对象} 是顶点的有穷非空集合; E = {(x, y) | x, y V } 或E = {<x, y> | x, y V && Path (x, y)} 是顶点之间关系的有穷集合,也叫做边(edge)集合。Path (x, y)表示从 x 到 y 的一条单向通路, 它是有方向的。
有向图与无向图 在有向图中,顶点对<x, y>是有序的。在无向图中,顶点对(x, y)是无序的。 • 完全图 若有 n 个顶点的无向图有 n(n-1)/2 条边, 则此图为完全无向图。有 n 个顶点的有向图有n(n-1) 条边, 则此图为完全有向图。 • 邻接顶点 如果 (u, v) 是 E(G) 中的一条边,则称 u 与 v 互为邻接顶点。
权 某些图的边具有与它相关的数, 称之为权。这种带权图叫做网络。 • 子图 设有两个图 G=(V, E) 和 G‘=(V’, E‘)。若 V’ V 且 E‘E, 则称 图G’ 是 图G 的子图。 • 顶点的度 一个顶点v的度是与它相关联的边的条数。记作TD(v)。在有向图中, 顶点的度等于该顶点的入度与出度之和。
顶点 v 的入度是以 v 为终点的有向边的条数, 记作 ID(v); 顶点 v的出度是以 v 为始点的有向边的条数, 记作 OD(v)。 • 路径 在图 G=(V, E) 中, 若从顶点vi 出发, 沿一些边经过一些顶点vp1, vp2, …, vpm,到达顶点vj。则称顶点序列 ( vi vp1 vp2 ... vpm vj )为从顶点vi 到顶点 vj 的路径。它经过的边(vi, vp1)、(vp1, vp2)、...、(vpm, vj)应是属于E的边。 • 路径长度 • 非带权图的路径长度是指此路径上边的条数。 • 带权图的路径长度是指路径上各边的权之和。
简单路径 若路径上各顶点 v1,v2,...,vm 均不互相重复, 则称这样的路径为简单路径。 • 回路 若路径上第一个顶点 v1 与最后一个顶点vm 重合, 则称这样的路径为回路或环。 • 连通图与连通分量 在无向图中, 若从顶点v1到顶点v2有路径, 则称顶点v1与v2是连通的。如果图中任意一对顶点都是连通的, 则称此图是连通图。非连通图的极大连通子图叫做连通分量。
强连通图与强连通分量 在有向图中, 若对于每一对顶点vi和vj, 都存在一条从vi到vj和从vj到vi的路径, 则称此图是强连通图。非强连通图的极大强连通子图叫做强连通分量。 • 生成树 一个连通图的生成树是它的极小连通子图,在n个顶点的情形下,有n-1条边。但有向图则可能得到它的由若干有向树组成的生成森林。 • 本章不予 讨论的图
图的抽象数据类型 • class Graph { • public: • Graph ( ); • voidInsertVertex ( Type & vertex ); • voidInsertEdge • ( intv1, intv2, intweight ); • voidRemoveVertex ( intv ); • voidRemoveEdge ( intv1, intv2 ); • intIsEmpty ( ); • TypeGetWeight ( int v1, int v2 ); • intGetFirstNeighbor ( int v ); • intGetNextNeighbor ( intv1, intv2 ); • }
图的存储表示 • 邻接矩阵 (Adjacency Matrix) • 在图的邻接矩阵表示中,有一个记录各个顶点信息的顶点表,还有一个表示各个顶点之间关系的邻接矩阵。 • 设图 A = (V, E)是一个有 n 个顶点的图,则图的邻接矩阵是一个二维数组 A.edge[n][n],定义: • 无向图的邻接矩阵是对称的,有向图的邻接矩阵可能是不对称的。
在有向图中, 统计第 i行 1 的个数可得顶点 i的出度,统计第 j 行 1 的个数可得顶点 j的入度。 • 在无向图中, 统计第 i行 (列) 1 的个数可得顶点i的度。
用邻接矩阵表示的图的类的定义 const int MaxEdges = 50; const int MaxVertices = 10; template <class NameType, class DistType> classGraph { private: SeqList<NameType>VerticesList (MaxVertices); DistType Edge[MaxVertices][MaxVertices]; int CurrentEdges; intFindVertex ( SeqList<NameType> & L; const NameType &vertex ) { return L.Find (vertex); }
intGetVertexPos ( Const NameType &vertex ) { returnFindVertex (VerticesList, vertex );} public: Graph ( intsz = MaxNumEdges ); intGraphEmpty ( ) const { return VerticesList.IsEmpty ( );} int GraphFull( ) const{ returnVerticesList.IsFull( ) || CurrentEdges ==MaxEdges;} intNumberOfVertices ( ) { returnVerticesList.last +1;} intNumberOfEdges ( ) { returnCurrentEdges;}
NameType GetValue ( inti ) { returni >= 0 &&i <= VerticesList.last ?VerticesList.data[i] :NULL;} DistType GetWeight ( intv1, intv2 ); intGetFirstNeighbor ( intv ); intGetNextNeighbor ( intv1, int v2 ); void InsertVertex ( NameType &vertex ); voidInsertEdge ( int v1, int v2, DistTypeweight ); voidRemoveVertex ( int v ); voidRemoveEdge ( intv1, intv2 ); }
邻接矩阵实现的部分图操作 template <class NameType, class DistType> Graph<NameType, DistType> :: Graph( int sz) { //构造函数 for ( int i = 0;i < sz;i++ ) for ( intj = 0;j < sz;j++ ) Edge[i][j] = 0; CurrentEdges = 0; } template <class NameType, class DistType> DistType Graph<NameType, DistType> :: GetWeight( int v1, int v2 ) { //给出以顶点 v1 和 v2 为两端点的边上的权值
if ( v1 != -1 &&v2 != -1 ) returnEdge[v1][v2]; else return 0; } template <class NameType, class DistType> intGraph<NameType, DistType>:: GetFirstNeighbor ( const intv ) { //给出顶点位置为 v 的第一个邻接顶点的位置 if ( v != -1 ) { for ( intcol = 0;col < VerticesList.last;col++ ) if ( Edge[row][col] > 0 && Edge[row][col] < max ) returncol; }
return-1; } template <class NameType, class DistType> int Graph<NameType, DistType> :: GetNextNeighbor ( intv1, intv2 ) { //给出顶点v1的某邻接顶点v2的下一个邻接顶点 intcol; if ( v1 != -1 &&v2 != -1 ) { for ( col = v2+1;col < VerticesList.last;col++ ) if ( Edge[v1][col] > 0 && Edge[v1][col] < max ) return col; } return-1; }
邻接表 (Adjacency List) • 无向图的邻接表 把同一个顶点发出的边链接在同一个边链表中,链表的每一个结点代表一条边,叫做边结点,结点中保存有与该边相关联的另一顶点的顶点下标 dest和指向同一链表中下一个边结点的指针 link。
有向图的邻接表和逆邻接表 • 在有向图的邻接表中,第 i个边链表链接的边都是顶点 i发出的边。也叫做出边表。 • 在有向图的逆邻接表中,第 i个边链表链接的边都是进入顶点i的边。也叫做入边表。
带权图的边结点中保存该边上的权值 cost。 • 顶点 i的边链表的表头指针adj在顶点表的下标为 i 的顶点记录中,该记录还保存了该顶点的其它信息。 • 在邻接表的边链表中,各个边结点的链入顺序任意,视边结点输入次序而定。 • 设图中有 n 个顶点,e 条边,则用邻接表表示无向图时,需要 n 个顶点结点,2e 个边结点;用邻接表表示有向图时,若不考虑逆邻接表,只需 n 个顶点结点,e 个边结点。
邻接表表示的图的类定义 const intDefaultSize = 10; template <class DistType> classGraph; template <class DistType> struct Edge { friend classGraph<NameType, DistType>; intdest; DistTypecost; Edge<DistType> *link; Edge ( ) { } Edge ( int D, DistTypeC ) : dest (D), cost (C), link (NULL) { } int operator != ( Edge<DistType>& E ) const { returndest != E.dest;} }
template <class NameType, class DistType> structVertex{ friend classGraph<NameType, DistType>; NemeType data; Edge<DistType> *adj; } template <class NameType, class DistType> class Graph { private: Vertex<NameType, DistType> *NodeTable; intNumVertices; intMaxVertices;
int NumEdges; int GetVertexPos ( NameType & vertex ); public: Graph ( intsz ); ~Graph ( ); intGraphEmpty ( ) const { return NumVertices == 0;} int GraphFull ( ) const { returnNumVertices == MaxVertices;} NameType GetValue ( int i ) { returni >= 0 &&i < NumVertices? NodeTable[i].data: NULL;} intNumberOfVertices ( ) { return NumVertices;}
intNumberOfEdges ( ) { returnNumEdges;} void InsertVertex ( NameType &vertex ); voidRemoveVertex ( intv ); voidInsertEdge ( int v1, int v2, DistTypeweight ); voidRemoveEdge ( int v1, int v2 ); DistType GetWeight ( int v1, int v2 ); intGetFirstNeighbor ( int v ); intGetNextNeighbor ( int v1, intv2 ); }
网络 (带权图) 的邻接表 邻接表的构造函数和析构函数 template <class NameType, class DistType> Graph<NameType, DistType> :: Graph ( intsz = DefaultSize ) : NumVertices (0), MaxVertices (sz), NumEdges (0){
intn, e, k, j;NameTypename, tail, head; DistTypeweight; NodeTable = //创建顶点表 newVertex<Nametype>[MaxVertices]; cin >> n; //输入顶点个数 for ( inti = 0; i < n;i++) //输入各顶点信息 {cin >> name;InsertVertex ( name );} cin >> e; //输入边条数 for ( i = 0;i < e; i++) { //逐条边输入 cin >> tail >> head >> weight; k = GetVertexPos ( tail ); j = GetVertexPos ( head ); InsertEdge ( k, j, weight ); }
} template <class NameType, class DistType> Graph<NameType, DistType> :: ~Graph ( ) { for ( inti = 0;i < NumVertices;i++ ) { Edge<DistType> *p = NodeTable[i].adj; while ( p != NULL ) { //逐条边释放 NodeTable[i].adj = p→link;deletep; p = NodeTable[i].adj; } } delete [ ] NodeTable; //释放顶点表 }
邻接表部分成员函数的实现 template <class NameType, class DistType> intGraph<NameType, DistType> :: GetVertexPos ( const NameType &vertex ) { //根据顶点名vertex查找该顶点在邻接表中的位置 for ( inti =0; i < NumVertices;i++ ) if ( NodeTable[i].data == vertex ) returni; return-1; } template <Class NameType, class DistType> intGraph<NameType, DistType> :: GetFirstNeighbor ( int v ) {
//查找顶点 v 第一个邻接顶点在邻接表中的位置 if ( v != -1 ) { //若顶点存在 Edge<DistType> *p = NodeTable[v].adj; if ( p != NULL ) returnp→dest; } return-1; //若顶点不存在 } template <Class NameType, class DistTypeType> intGraph<NameType, DistType> :: GetNextNeighbor ( intv1, int v2 ) { //查找顶点 v1在邻接顶点 v2 后下一个邻接顶点 if ( v1 != -1 ) { Edge<DistType> *p = NodeTable[v1].adj;
while ( p != NULL ) { if ( p→dest == v2 &&p→link != NULL ) returnp→link→dest; //返回下一个邻接顶点在邻接表中的位置 elsep = p→link; } } return-1; //没有查到下一个邻接顶点返回-1 }
template <Class NameType, class DistType> DistType Graph<NameType, DistType> :: GetWeight ( int v1, int v2) { //取两端点为 v1 和 v2 的边上的权值 if ( v1 != -1 &&v2 != -1 ) { Edge<DistType> *p = NodeTable[v1].adj; while ( p != NULL ) { if ( p→dest == v2) returnp→cost; elsep = p→link; } } return 0; }
邻接多重表 (Adjacency Multilist) • 在邻接多重表中,每一条边只有一个边结点。为有关边的处理提供了方便。 • 无向图的情形 • 边结点的结构 mark vertex1 vertex2 path1 path2 其中,mark 是记录是否处理过的标记;vertex1和vertex2是依附于该边的两顶点位置。path1域是链接指针,指向下一条依附于顶点vertex1的边;path2也是链接指针,指向下一条依附于顶点vertex2的边。需要时还可设置一个存放与该边相关的权值的域 cost。
data Firstout • 顶点结点的结构 存储顶点信息的结点表以顺序表方式组织,每一个顶点结点有两个数据成员:其中,data 存放与该顶点相关的信息,Firstout 是指示第一条依附于该顶点的边的指针。 在邻接多重表中, 所有依附于同一个顶点的边都链接在同一个单链表中。 从顶点 i出发, 可以循链找到所有依附于该顶点的边,也可以找到它的所有邻接顶点。 • 邻接多重表的结构
有向图的情形 在用邻接表表示有向图时,有时需要同时使用邻接表和逆邻接表。用有向图的邻接多重表(十字链表)可把这两个表结合起来表示。 • 边结点的结构 mark vertex1 vertex2 path1 path2
其中,mark是处理标记;vertex1和vertex2指明该有向边始顶点和终顶点的位置。path1是指向始顶点与该边相同的下一条边的指针;path2是指向终顶点与该边相同的下一条边的指针。需要时还可有权值域cost。其中,mark是处理标记;vertex1和vertex2指明该有向边始顶点和终顶点的位置。path1是指向始顶点与该边相同的下一条边的指针;path2是指向终顶点与该边相同的下一条边的指针。需要时还可有权值域cost。 • 顶点结点的结构 每个顶点有一个结点,它相当于出边表和入边表的表头结点:其中,数据成员data存放与该顶点相关的信息,指针Firstin指示以该顶点为始顶点的出边表的第一条边,Firstout指示以该顶点为终顶点的入边表的第一条边。 data Firstin Firstout
图的遍历与连通性 • 从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历 ( Graph Traversal )。 • 图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。 • 为了避免重复访问,可设置一个标志顶点是否被访问过的辅助数组 visited [ ],它的初始状态为 0,在图的遍历过程中,一旦某一个顶点 i被访问,就立即让 visited [i]为 1,防止它被多次访问。
深度优先搜索DFS ( Depth First Search ) • 深度优先搜索的示例
DFS 在访问图中某一起始顶点 v 后,由 v 出发,访问它的任一邻接顶点 w1;再从 w1 出发,访问与 w1邻 接但还没有访问过的顶点 w2;然后再从 w2 出发,进行类似的访问,… 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点 u 为止。接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问;如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
图的深度优先搜索算法 template<class NameType, class DistType> voidGraph <NameType, DistType> :: DFS ( ) { int * visited = newint [NumVertices]; for ( inti = 0;i < NumVertices;i++ ) visited [i] = 0; //访问标记数组 visited初始化 DFS (0, visited ); delete [ ] visited; //释放 visited } template<class NameType, class DistType> voidGraph<NameType, DistType> :: DFS ( const intv, int visited [ ] ) {
cout << GetValue (v) << ‘ ’; //访问顶点 v visited[v] = 1; //顶点 v作访问标记 intw = GetFirstNeighbor (v); //取 v的第一个邻接顶点 w while ( w != -1 ) {//若邻接顶点 w存在 if ( !visited[w] ) DFS ( w, visited ); //若顶点 w未访问过, 递归访问顶点 w w = GetNextNeighbor ( v, w ); //取顶点 v的排在 w后面的下一个邻接顶点 } }
算法分析 • 图中有 n 个顶点,e 条边。 • 如果用邻接表表示图,沿Firstout link 链可以找到某个顶点 v 的所有邻接顶点 w。由于总共有 2e 个边结点,所以扫描边的时间为O(e)。而且对所有顶点递归访问1次,所以遍历图的时间复杂性为O(n+e)。 • 如果用邻接矩阵表示图,则查找每一个顶点的所有的边,所需时间为O(n),则遍历图中所有的顶点所需的时间为O(n2)。
广度优先搜索BFS ( Breadth First Search) • 广度优先搜索的示例 广度优先搜索过程 广度优先生成树
使用广度优先搜索在访问了起始顶点 v 之后,由 v 出发,依次访问 v 的各个未曾被访问过的邻接顶点 w1, w2, …, wt,然后再顺序访问 w1, w2, …, wt 的所有还未被访问过的邻接顶点。再从这些访问过的顶点出发,再访问它们的所有还未被访问过的邻接顶点,… 如此做下去,直到图中所有顶点都被访问到为止。 • 广度优先搜索是一种分层的搜索过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况。因此,广度优先搜索不是一个递归的过程,其算法也不是递归的。
为了实现逐层访问,算法中使用了一个队列,以记忆正在访问的这一层和上一层的顶点,以便于向下一层访问。为了实现逐层访问,算法中使用了一个队列,以记忆正在访问的这一层和上一层的顶点,以便于向下一层访问。 • 为避免重复访问,需要一个辅助数组 visited [ ],给被访问过的顶点加标记。 图的广度优先搜索算法 template<class NameType, class DistType> voidGraph <NameType, DistType> :: BFS ( intv ) { int * visited = new int[NumVertices]; for ( inti = 0;i < NumVertices;i++ ) visited[i] = 0; //visited初始化
cout << GetValue (v) << ' '; visited[v] = 1; Queue<int> q;q.EnQueue (v); //访问 v, 进队列 while ( !q.IsEmpty ( ) ) { //队空搜索结束 v = q.DeQueue ( ); //不空, 出队列 intw = GetFirstNeighbor (v); //取顶点 v的第一个邻接顶点 w while ( w != -1 ) { //若邻接顶点 w存在 if ( !visited[w] ) { //若该邻接顶点未访问过 cout << GetValue (w) << ‘ ’; //访问 visited[w] = 1;q.EnQueue (w); //进队 } w = GetNextNeighbor (v, w); //取顶点 v的排在 w后面的下一邻接顶点
} //重复检测 v的所有邻接顶点 } //外层循环,判队列空否 delete [ ] visited; } 算法分析 • 如果使用邻接表表示图,则循环的总时间代价为 d0 + d1 + … + dn-1 = O(e),其中的 di 是顶点 i 的度。 • 如果使用邻接矩阵,则对于每一个被访问过的顶点,循环要检测矩阵中的 n 个元素,总的时间代价为O(n2)。
连通分量 (Connected component) • 当无向图为非连通图时,从图中某一顶点出发,利用深度优先搜索算法或广度优先搜索算法不可能遍历到图中的所有顶点,只能访问到该顶点所在的最大连通子图(连通分量)的所有顶点。 • 若从无向图的每一个连通分量中的一个顶点出发进行遍历,可求得无向图的所有连通分量。 • 在算法中,需要对图的每一个顶点进行检测:若已被访问过,则该顶点一定是落在图中已求得的连通分量上;若还未被访问,则从该顶点出发遍历图,可求得图的另一个连通分量。
对于非连通的无向图,所有连通分量的生成树组成了非连通图的生成森林。对于非连通的无向图,所有连通分量的生成树组成了非连通图的生成森林。
确定连通分量的算法 template<class NameType, class DistType> voidGraph<NameType, DistType> :: Components ( ) { int *visited = new int[NumVertices]; for ( int i = 0; i < NumVertices;i++ ) visited[i] = 0; //visited 初始化 for ( i = 0;i < NumVertices;i++ ) if ( !visited[i] ) { //检测所有顶点是否访问过 DFS ( i, visited ); //从未访问的顶点出发访问 OutputNewComponent ( ); //输出一个连通分量 }