740 likes | 974 Views
第 7 章 图. 基本概念 图的存储结构 图的遍历 生成树 最短路径 拓扑排序. 7.1 图的基本概念. 图的定义: 一个图 G 是由两个集合 V 和 E 组成, V 是有限的非空顶点集, E 是 V 上的顶点对所构成的边集,分别用 V ( G )和 E ( G )来表示图中的顶点集和边集。用二元组 G= ( V , E )来表示图 G 。. 有向图与无向图 若图 G 中的每条边都是有方向的,则称 G 为有向图。有向边也称为 弧 。若图 G 中的每条边都是没有方向的,则称 G 为无向图。
E N D
第7章 图 • 基本概念 • 图的存储结构 • 图的遍历 • 生成树 • 最短路径 • 拓扑排序
7.1 图的基本概念 图的定义:一个图G是由两个集合V和E组成,V是有限的非空顶点集,E是V上的顶点对所构成的边集,分别用V(G)和E(G)来表示图中的顶点集和边集。用二元组 G=(V,E)来表示图G。
有向图与无向图 若图G中的每条边都是有方向的,则称G为有向图。有向边也称为弧。若图G中的每条边都是没有方向的,则称G为无向图。 • 完全图 对有n个顶点的图,若为无向图且边数为n(n-1)/2,则称其为无向完全图;若为有向图且边数为n(n-1) ,则称其为有向完全图。 • 邻接顶点 若(vi,vj)是一条无向边,则称顶点vi和vj互为邻接点,或称vi和vj相邻接,并称边(vi,vj)关联于顶点vi和vj,或称(vi,vj)与顶点vi和vj相关联。
顶点的度 一个顶点v的度是与它相关联的边的条数。记作TD(v)。 • 顶点 v 的入度 是以 v 为终点的有向边的条数, 记作 ID(v)。 • 顶点 v 的出度是以 v 为始点的有向边的条数, 记作 OD(v)。 • 子图 设有两个图 G=(V, E) 和 G’=(V’, E’)。若 V’ V 且 E’E, 则称 图G’ 是 图G 的子图。
路径 在图 G=(V, E)中, 若存在一个顶点序列 • vp1, vp2, …, vpm,使得(vi, vp1)、(vp1, vp2)、 • ...、(vpm, vj)均属于E,则称顶点vi到vj存在一 • 条路径。若一条路径上除了vi和vj可以相同外, • 其余顶点均不相同,则称此路径为一条简单路径 • 。起点和终点相同的路径称为简单回路或简单环。
图的连通 在无向图G中,若两个顶点vi和vj之间有 • 路径存在,则称vi和vj是连通的。若G中任意两 • 个顶点都是连通的,则称G为连通图。非连通图的 • 极大连通子图叫做连通分量。 • 强连通图与强连通分量在有向图中, 若对于每一对顶点vi和vj, 都存在一条从vi到vj和从vj到vi的路径,则称此图是强连通图。非强连通图的极大强连通子图叫做强连通分量。 • 权某些图的边具有与它相关的数, 称之为权。这种带权图叫做网络。
7 2 5 B 10 9 60 40 1 12 80 8 A C 6 3 7 75 30 6 35 15 6 3 D E 4 7 45 16 权图
生成树 一个连通图的生成树是它的极小 连通子图,在n个顶点的情形下,有n-1条 边。 • 生成林 若G是一个不连通的无向图,G的每个连通分量都有一棵生成树,这些生成树构成G的生成森林,简称生成林。
7.2 图的存储结构 • 邻接矩阵 (Adjacency Matrix)表示法 • 在图的邻接矩阵表示中,有一个记录各个顶点信息的顶点表,还有一个表示各个顶点之间关系的邻接矩阵。 • 设图 A = (V, E)是一个有 n 个顶点的图,则图的邻接矩阵是一个二维数组 A .Edge[n][n],定义: • 无向图的邻接矩阵是对称的,有向图的邻接矩阵一般情况下是不对称的。
在有向图中, 统计第 i行 1 的个数可得顶点 i的出度,统计第 j 列 1 的个数可得顶点 j的入度。 • 在无向图中, 统计第 i行 (列) 1 的个数可得顶点i的度。
邻接矩阵表示法中图的类型定义: #define MAXSIZE 100 /*图的顶点个数*/ typedef int datatype; typedef struct { datatype vexs[MAXSIZE]; /*顶点信息表*/ int edges[MAXSIZE][ MAXSIZE] ;/ *邻接矩阵*/ int n,e ; /*顶点数和边数*/ }graph;
2 1 4 3 5 无向图
B A D C E 有向图
2 5 20 40 70 1 4 50 80 30 6 3 有向权图
邻接矩阵表示法中无向网络的建立算法 void Create_Graph(graph *ga) { int i,j ,k,w; printf ("请输入图的顶点数和边数:\n"); scanf ("%d",&(ga->n),& (ga->e)); printf ("请输入顶点信息(顶点编号),建立顶点信息表:\n") ; for(i = 0;i<ga->n;i++) scanf("%c",&(ga->vexs[i]));/*输入顶点信息*/ for (i = 0;i<ga->n;i++) /*邻接矩阵初始化*/ for (j = 0;j<ga->n;j++) ga->edges[i][j] = 0; for (k = 0;k<ga->e;k++) /*读入边的顶点编号和权值,建立邻接矩阵*/ { printf ("请输入第%d条边的顶点序号i,j和权值w:",k+1); scanf ("%d,%d,%d",&i,&j,&w); ga->edges[i][j] = w; ga->edges[j][i] = w; } }
算法分析 该算法的执行时间是O(n+n2+e),由于e< n2,所以算法的时间复杂度为O(n2)。
v1 v2 v3 v4 v1 2 3 ^ v2 v3 1 4 ^ v4 1 4 ^ 2 3 ^ 邻接表 (Adjacency List)表示法 • 无向图的邻接表 表头结点 表结点 data first vertex next G5
每个链表的上边附设一个表头结点,在表头结点中,除了设有链域first用于指向链表中第一个结点之外,还设有存储顶点vi名或其它有关信息的数据域data 。 • 把同一个顶点发出的边链接在同一个边链表中,链表的每一个结点代表一条边,叫做表结点,结点中保存有与该边相关联的另一顶点的顶点下标 vertex和指向同一链表中下一个表结点的指针 next。
2 ^ 1 ^ 2 ^ 2 ^ v1 1 3 ^ v2 v3 邻接表 逆邻接表 • 有向图的邻接表和逆邻接表 • 在有向图的邻接表中,第 i个边链表链接的边都是顶点 i发出的边。也叫做出边表。 • 在有向图的逆邻接表中,第 i个边链表链接的边都是进入顶点 i的边。也叫做入边表。 G6
邻接表的类型定义 #define nmax 100 /*假设顶点的最大数为100*/ typedef struct node *pointer; struct node { /*表结点类型*/ int vertex ; struct node *next ; } nnode; typedef struct {/*表头结点类型,即顶点表结点类型*/ datatype data ; pointer first ;/*边表头指针*/ }headtype ; typedef struct { /*表头结点向量,即顶点表*/ headtype adlist[nmax]; int n,e ; }lkgraph ;
建立无向图邻接表的算法 void creatqraph(Ikgraph *ga) { /*建立无向图的邻接表*/ int i,j,e,k; pointer p; printf(“请输入顶点数:\n”); scanf (“%d”, &(ga->n)); for (i =1; i<= ga->n; i++) { /*读入顶点信息,建立顶点表*/ scanf (“ \n %c”, &( ga->adlist[i].data) ); ga->adlist[i].first = NULL; } e = 0; scanf (“\n%d,%d\n”, &i,&j ); /*读入一个顶点对号i和j*/ while (i>0) { /*读入顶点对号,建立边表*/ e++; /*累计边数 */ p = (pointer)malloc(size(struct node));/*生成新的邻接点序号为j的表结点*/ p-> vertex = j; p->next = ga->adlist[i].first; ga->adlist[i].first = p;/*将新表结点插入到顶点vi的边表的头部*/ p = (pointer)malloc(size(struct node));/*生成邻接点序号为i的表结点*/ p-> vertex = i; p->next = ga->adlist[j].first; ga->adlist[j].first=p; /*将新表结点插入到顶点vj的边表头部*/ scanf (“\n%d,%d\n”, &i,&j );/*读入一个顶点对号i和j*/ } ga->e = e ; }
该算法的时间复杂度是O(n+e) • 在邻接表的边链表中,各个表结点的链入顺序任意,视表结点输入次序而定。 • 设图中有 n 个顶点,e 条边,则用邻接表表示无向图时,需要 n 个表头结点,2e 个表结点;用邻接表表示有向图时,若不考虑逆邻接表,只需 n 个表头结点,e 个表结点。 • 带权图的边结点中还应保存该边上的权值 cost。
网络 (带权图) 的邻接表 表结点
7.3 图的遍历 • 从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历( Graph Traversal )。 • 图中可能存在回路,且图的任一顶点都可能与其它顶点相通,在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。 • 为了避免重复访问,可设置一个标志顶点是否被访问过的辅助数组 visited [ ],它的初始状态为 0,在图的遍历过程中,一旦某一个顶点 i 被访问,就立即让 visited [i] 为 1,防止它被多次访问。
深度优先搜索DFS ( Depth First Search ) 深度优先搜索的示例
DFS 在访问图中某一起始顶点 v 后,由 v 出发, 访问它的任一邻接顶点 v 1;再从 v 1 出发,访问与 v 1邻接但还没有访问过的顶点 v 2;然后再从 v 2 出 发,进行类似的访问,… 如此进行下去,直至到达 所有的邻接顶点都被访问过的顶点vk为止。接着, 退回一步,退到前一次刚访问过的顶点,看是否还 有其它没有被访问的邻接顶点。如果有,则访问此 顶点,之后再从此顶点出发,进行与前述类似的访 问;如果没有,就再退回一步进行搜索。重复上述 过程,直到连通图中所有顶点都被访问过为止。
深度优先搜索算法 void DFS(graph *g) /*按深度优先搜索法遍历图g*/ { int i; for(i=0;i<g->n;i++ ) visid[i] = 0; /*初始化数组visid,使每个元素为0*/ /*标示图中的每个结点都未曾访问过*/ for(i= 0;i<g->n;i++) if(!visid[i]) DFSM(g,i);/*调用函数DFSM,对图进行遍历*/ } void DFSM(graph *g,int i )/*邻接矩阵上进行DFS遍历*/ { int j; printf("深度优先遍历结点:%c\n" ,g->vexs[i]); visid[i]=1; /*假定g->vexs[i]为顶点的编号,然后变访问标志为1*/ for(j =0;j<g->n;j++) if((g->edges[i][j] = =1)&&!visid[j]) DFSM(g,j); }
void DFSL(lkgraph *g,int n )/*邻接表上进行DFS遍历*/ { pointer p; int j; printf(" %d\n" ,g-> adlist[n].data); /*访问出发点,输出顶点数据*/ visid[i]=1; /*然后变访问标志为1*/ for(p = g->adlist[n].first;p!=NULL; p = p->next) if(!visid[p-> vertex ]) DFSL(g,p-> vertex ); }
算法分析 • 图中有 n 个顶点,e 条边。 • 如果用邻接表表示图,沿链可以找到某个顶点 v 的所有邻接顶点 vi。由于总共有 2e 个边结点,所以扫描边的时间为O(e)。而且对所有顶点递归访问1次,所以遍历图的时间复杂性为O(n+e)。 • 如果用邻接矩阵表示图,则查找每一个顶点的所有邻接点,所需时间为O(n),则遍历图中所有的顶点所需的时间为O(n2)。
广度优先搜索BFS ( Breadth First Search) 广度优先搜索的示例 广度优先搜索过程 广度优先生成树
使用广度优先搜索在访问了起始顶点 v 之后,由 v 出发,依次访问 v 的各个未曾被访问过的邻接顶点 v 1, v 2, …, v t,然后再顺序访问 v 1, v 2, …, v t的所有还未被访问过的邻接顶点。再从这些访问过的顶点出发,再访问它们的所有还未被访问过的邻接顶点,… 如此做下去,直到图中所有顶点都被访问到为止。 • 广度优先搜索是一种分层的搜索过程,每向前走一步可能访问一批顶点,不像深度优先搜索那样有往回退的情况。因此,广度优先搜索不是一个递归的过程,其算法也不是递归的。
为了实现逐层访问,算法中使用了一个队列,以记忆正在访问的这一层和上一层的顶点,以便于向下一层访问。为了实现逐层访问,算法中使用了一个队列,以记忆正在访问的这一层和上一层的顶点,以便于向下一层访问。 • 与深度优先搜索过程一样,为避免重复访问,需要一个辅助数组 visited [ ],以便给被访问过的顶点加标记。
广度优先搜索算法 void BFS(graph *g,int v)/*v是出发顶点的序号,按广度优先搜索法遍历图g ,采用邻接矩阵存储结构,BFS遍历*/ { int j,n; seqqueue q;/*假设采用顺序队列,定义顺序队列类型变量q*/ n = g->n; init_seqqueue(&q); /*队列初始化*/ printf(“访问出发点 %d”,v) ;/*访问出发点,假设为输出顶点序号*/ visid[v] = 1; /*置访问标志为1,表示此点已访问过*/ en_sqqueue(&q,v); /*顶点v入队*/ while (!empty_Seqqueue(&q)) { /*队列空否?*/ de_ sqqueue(&q,&v);/*队列非空时,出队*/ for(j= l;j<= n;j++) if(q->adges [v][j] = = l &&!visid[j]) {printf(“访问顶点%d”,j);visid[j] = 1;/*置顶点j被访问标志*/ en_seqqueue(&q,j); /*顶点j入队*/ } } }
void BFSL(graph *g,int v) { /*采用邻接表存储结构,BFS遍历*/ seqqueue q; /*假设采用顺序队列,定义顺序队列类型变量q*/ pointer p; init_seqqueue(&q); /*队列初始化*/ printf(“访问出发点%d”,v); /*访问出发点,假设为输出顶点序号*/ visid[v] = 1; /*置访问标志为1,表示此点已访问过*/ en_seqqueue(&q,v); /*顶点v入队*/ while(!empty _seqqueue(&q)) { de_seqqueue(&q,&v); p = g->adlist[v].first; While(p != NULL) { if(! visid[p->vertex]) { printf("%d”,p-> vertex); visid[p-> vertex ]= 1; en_ seqqueue (&q,p-> vertex); } p = p->next ; } } }
算法分析 • 当使用邻接矩阵表示图时,因为每个顶点均入队、出队一次,所以算法BFS的外循环次数为n,且其内循环也是n次,因此算法BFS的时间复杂度为O(n2)。 • 当使用邻接表表示图时,同样知道函数BFSL的外循环次数也是n,且其内循环次数取决于各顶点的边表结点个数,而内循环执行的总次数是边表结点的总个数2e,因此执行函数BFSL的时间复杂度是O(n+e)。
非连通图的遍历 非连通图的遍历必须多次调用深度优先搜索或广度优先搜索算法,以DFS为例: TRAVER() /* 遍历用邻接矩阵表示的非连通图*/ { int i; for ( i = 0; i < n; i++) visited[i] = FALSE; /* 标志数组初始化 */ for ( i = 0; i < n; i++) { if ( !visited[i]) DFS(i); /* 从顶点出发遍历一个连 通分量 */ printf( “comp end\n”); } }
7.4 生成树 连通图G的一个子图如果是一棵包含G的所有顶点的树,则该子图称为G的生成树。 生成树是连通图的极小连通子图。所谓极小是指:若在树中任意增加一条边,则将出现一个回路;若去掉一条边,将会使之变成非连通图。 生成树各边的权值总和称为生成树的权。权最小的生成树称为最小生成树。
用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。用不同的遍历图的方法,可以得到不同的生成树;从不同的顶点出发,也可能得到不同的生成树。 按照生成树的定义,n 个顶点的连通网络的生成树有 n 个顶点、n-1 条边。 构造最小生成树,要解决以下两个问题: 1.尽可能选取权值小的边,但不能构成回路。 2.选取n-1条恰当的边以连接网的 n个顶点。
构造最小生成树的普里姆(Prim)算法 普里姆算法的基本思想: 从连通网络 N = { V, E }中的某一顶点 u0 出 发,选择与它关联的具有最小权值的边(u0, v), 将其顶点加入到生成树的顶点集合U中。以后每 一步从一个顶点在U中,而另一个顶点不在U中 的各条边中选择权值最小的边(u, v),把它的顶点 加入到集合U中。如此继续下去,直到网络中的 所有顶点都加入到生成树顶点集合U中为止。
1 1 5 6 1 2 4 1 2 4 5 7 5 3 3 3 2 3 2 5 4 4 5 6 5 6 6 用普里姆算法构造最小生成树的过程
Prim算法的形式描述如下: 置T为任意一个顶点; 求初始候选边集; while(T中结点数<n) {从候选边集中选取最短边(u,v); 将(u,v)及顶点v,扩充到T中; 调整候选边集; }
{min = minedge[j].len; v = j; } if(min=INTMAX) {printf ( “ 图不连通,无生成树!” ) ; return(0); } printf(“%d %d”,v, minedge[v].end); minedge[v].len = -minedge[v].len; for(j=1;j<=g->n;j++) if(g-> edges [j][v]<minedge[j].len) {minedge[j].len = g-> edges [j][v]; minedge[j].end = v; } } } PRIM算法 void prim(graph *g,int u) { int v,k,j,min; for (v =1;v<=g->n;v++) if(v ! = u) {minedge[v].end=u; minedge[v].len = g->edges[v][u]; } minedge[u].len = 0; for( k = 1;k< g->n;k++) {min = minedge[k].len; v = k; for(j =1;j<g->n;j++) if(minedge[j].len>0&&minedge[j].len<min)
算法分析 上述算法的初始化时间是O(1),k循环中有两个循环语句,其时间大致为: 令O(1)为某一正常数C,展开上述求和公式可知其数量级仍是 n 的平方。所以,整个算法的时间复杂性是O(n2)。
构造最小生成树的克鲁斯卡尔 (Kruskal) 算法 克鲁斯卡尔算法的基本思想: 设有一个有 n 个顶点的连通网络 N = { V, E }, 最初先构造一个只有 n 个顶点,没有边的非连通 图 T = { V, }, 图中每个顶点自成一个连通分量。 当在 E 中选到一条具有最小权值的边时,若该边的 两个顶点落在不同的连通分量上,则将此边加入 到 T 中;否则将此边舍去,重新选择一条权值最 小的边。 如此重复下去,直到所有顶点在同一个连通 分量上为止。
1 1 5 6 1 1 2 4 2 4 5 7 5 3 3 3 2 3 2 5 4 4 5 6 5 6 6 用克鲁斯卡尔算法构造最小生成树的过程
克鲁斯卡尔算法的形式描述如下: T = (V, φ); While ( T中所含边数 < n-1 ) { 从E中选取当前最短边 (u,v); 从E中删除边(u,v); if ((u,v) 并入T之后不产生回路 ) 将边 (u,v) 并入T中; }
Kruskal算法 typedef Struct{ int v1,v2; int len; }edgetype; /* 边的类型:两个端点号和边长*/ int parent[nmax+1];/*结点双亲的指针数组,设为全局量,nmax为结点数最大值*/ int getroot(int v) /*找结点v所在的树根*/ { int i; i = v; while(parent[i]>0)i = parent[i]; return i; /*若无双亲(初始点),双亲运算结果为其自己*/ } int getedge(edgetype em [ ],int e) /*找最短边,e为边数*/ {int i, j, min = 0; for (i =1 ; i<= e ; i++) if (em [i-1].len<min) min = em [i-1].len; return min; }
void kruskal(edqetype em [ ],int n,int e) /*n为结点数,e为边数*/ {int i,p1,p2,m,i0; for(i=1;i<= n;i++)/*初始结点为根,无双亲*/ parent[i ] = -1; /*以后用于累计结点个数,此初值不能置为0*/ m = 1; while(m< n) {i0 = getedge(em,e); /*获得最短边号*/ p1= getroot(em [i0].v1); p2= getroot(em [i0].v2); if(p1= = p2)continue; /*连通分量相同,不合并*/ if(p1>p2){ parent[p2]= parent[p1]+parent[p2];/*p2的双亲中累计结点总数(为负值)*/ parent[pl]= p2; /*p1成为p2的孩子 } else{ parent[ p1]= parent[p1]+parent[p2]; parent[p2]= p1; } m++; printf (“%d%d%d\n”,m,em[i0].v1,em [i0].v2 ); } }
算法分析 用Kruskal算法构造最小生成树的时间复杂度为O(eloge),与网中边的数目e有关,因此,它适用于求稀疏图的最小生成树。