第7章 图(Graph)
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
无向边(Edge),用无序偶对(Vi,Vj)来表示。
有向边,也称为弧(Arc),用有序偶<Vi,Vj>来表示,Vi称为弧尾(Tail),Vj称为弧头(Head),图形显示为Vi---->Vj。
图的存储结构
1、邻接矩阵(Adjacency Matrix)
邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
typedef char VertexType; /* 顶点类型应由用户定义 */ typedef int EdgeType; /* 边上的权值类型应由用户定义 */ #define MAXVEX 100 /* 最大顶点数,应由用户定义 */ #define INFINITY 65535 /* 用65535来代表无穷大 */ typedef struct { VertextType vexs[MAXVEX]; /* 顶点表 */ EdgeType arc[MAXVEX][MAXVEX]; /* 邻接矩阵,可看做边表 */ int numVertexes, numEdges; /* 图中当前的顶点数和边数 */ }MGraph;
临界表顶点采用一个一维数组存储,另外对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
顶点的所有邻接点构成一个线性表,用单链表存储,无向图为顶点的边表,有向图为顶点弧尾的出边表。
typedef char VertexType; /* 顶点类型应由用户定义 */ typedef int EdgeType; /* 边上的权值类型应由用户定义 */ typedef struct EdgeNode /* 边表结点 */ { int adjvex; /* 邻接点域,存储该顶点对应的下标 */ EdgeType weight; /* 用于存储权值,对于非网图可以不需要 */ struct EdgeNode *next; /* 链域,指向下一个邻接点 */ }EdgeNode; typedef struct VertexNode /* 顶点表结点 */ { VertexType data; /* 顶点域,存储顶点信息 */ EdgeNode* firstedge; /* 边表头指针 */ }VertexNode, AdjList[MAXVEX]; typedef struct { AdjList adjList; int numVertexes,numEdges; /* 图中当前顶点数和边数 */ }GraphAdjList;
对于有向图来说,邻接表只关心出度问题,入度必须要遍历整个图才能知道。有向图中采用了十字链表的存储方法。
结构定义书中没有给出,整理如下
typedef char VertexType; /* 顶点类型应由用户定义 */ typedef int EdgeType; /* 边上的权值类型应由用户定义 */ typedef struct ArcNode /* 弧表结点 */ { int tailvex; /* 弧起点在顶点表的下标 */ int headvex; /* 弧终点在顶点表的下标 */ struct ArcNode *headlink; /* 入边表指针域,指向终点相同的下一条边 */ struct ArcNode *taillink; /* 边表指针域,指向起点相同的下一条边 */ EdgeType weight; /* 用于存储权值,对于非网图可以不需要 */ }ArcNode; typedef struct VertexNode /* 顶点表结点 */ { VertexType data; /* 顶点域,存储顶点信息 */ ArcNode* firstin; /* 表示入边头指针,指向该顶点的入边表中的第一个结点 */ ArcNode* firstout; /* 表示出边头指针,指向该顶点的出边表中的第一个结点 */ }VertexNode, OrtList[MAXVEX]; typedef struct { OrtList ortList; int numVertexes,numEdges; /* 图中当前顶点数和边数 */ }GraphOrtList;
4、邻接多重表(Adjacency MulList)
邻接多重表示对于无向图的邻接表的优化。
typedef char VertexType; /* 顶点类型应由用户定义 */ typedef int EdgeType; /* 边上的权值类型应由用户定义 */ typedef emnu{ unvisited,visited} VisitIf; typedef struct EBox { VisitIf mark: /*访问标记*/ int ivex,jvex; /*该边依附的两个顶点的位置*/ struct EBox ilink, jlink; /*分别指向依附这两个顶点的下一条边*/ InfoType info; /*该边信息指针*/ }EBox; typedef struct VexBox { VertexType data; EBox fistedge; /*指向第一条依附该顶点的边*/ }VexBox, Adjmulist[MAXVEX]; typedef struct { Adjmulist adjmulist; int numVertexes,numEdges; /* 无向图中当前顶点数和边数 */ }AMLGraph;
边集数组是有两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素 有一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
图的遍历
深度优先遍历DFS(Depth_First_Search)
邻接矩阵的深度优先遍历算法,对于n个顶点e条边的图,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此需要O(n^2)的时间。
typedef int Boolean; /* Boolean是布尔类型, 其值是true或false */ Boolen visited[MAX]; /* 访问标志的数组 */ /* 邻接矩阵的深度优先递归算法 */ void DFS ( MGraph G, int i ) { int j; visited[i] = true; printf("%c ",G.vexs[i]); for(j=0;j<G.numVertexes; j++) if (G.arc[i][j] == 1 && !visited[j]) DFS(G, j); } /* 邻接矩阵的深度遍历操作 */ void DFSTraverse(MGraph G ) { int i; for(i=0; i<G.numVertexes; i++) visited[i] = false; for(i=0; i<G.numVertexes; i++) if (! visited[i]) DFS(G, i); }
/* 邻接表的深度优先递归算法 */ void DFS(GraphAdjList GL, int i) { EdgeNode *p; visited[i] = true; printf("%c ",GL->adjList[i].data); p = GL->adjList[i].firstedge; while(p) { if (! visited[p->adjvex]) DFS(GL, p->adjvex); p = p->next; } } /* 邻接表的深度遍历操作 */ void DFSTraverse (GraphAdjList GL) { int i; for(i=0; i<GL->numVertexes; i++) visited[i] = false; for(i=0; i<GL->numVertexes; i++) if (! visited[i]) DFS(GL, i); }
邻接矩阵的广度遍历
/* 邻接矩阵的广度遍历算法 */ void BFSTraverse (MGraph G) { int i, j; Queue Q; for(i=0; i<G.numVertexes; i++) visited[i] = false; InitQueue(&Q); for(i=0; i<G.numVertexes; i++) { if (! visited[i]) { visited[i] = true; printf("%c ", G.vexs[i]); EnQueue[&Q,i); while(! QueueEmpty(Q)) { DeQueue(&Q, &i); for(j=0; j<G.numVertexes;j++) { if (G.arc[i][j] == 1 && !visited[j]) { visited[j] = true; printf("%c ",G.vexs[j]); EnQueue(&Q,j); } } } } } }
/* 邻接表的广度遍历算法 */ void BFSTraverse(GraphAdjList GL) { int i; EdgeNode *p; Queue Q; for(i=0; i<GL->numVertexes; i++) visited[i] = false; InitQueue(&Q); for(i=0; i<GL->numVertexes; i++) { if (! visited[i]) { visited[i] = true; printf("%c ",GL->adjList[i].data); EnQueue(&Q,i); while(! QueueEmpty(Q)) { DeQueue(&Q,i); p = GL->adjList[i].firstedge; while(p) { if (! visited[p->adjvex]) { visited[p->adjvex] = true; printf("%c ",GL->adjList[p->adjvex].data); EnQueue(&Q, p->adjvex); } p = p->next; } } } } }
最小生成树:构造连通图的最小代价生成树
普利姆(Prim)算法
普利姆算法是以顶点为起点,逐步找各顶点上最小权值的边来构建最小生成树的。
/* Prim算法生成最小生成树 */ void MiniSpanTree_Prim( MGraph G ) { int min, i, j, k; int adjvex[MAXVEX]; /* 保存相关顶点下标 */ int lowcost[MAXVEX]; /* 保存相关顶点间边的权值 */ lowcost[0] = 0; /* 初始化第一个权值为0, 即V0加入生成树 */ adjvex[0] = 0; /* 初始化第一个顶点下标为0 */ for(i=1; i< G.numVertexes; i++) /* 循环除下标为0外的全部顶点 */ { lowcost[i] = G.arc[0][i]; /* 将V0顶点与之有边的权值存入数组 */ adjvex[i] = 0; /* 初始化都为V0的下标 */ } for(i=1; i<G.numVertexes; i++) { min = INFINITY; /* 初始化最小权值为无穷大 */ j = 1; k = 0; while(j < G.numVertexes) /* 循环全部顶点 */ { if (lowcost[j] !=0 && lowcost[j] < min) /* 如果权值不为0 且权值小于min */ { min = lowcost[j]; /* 则让当前权值称为最小值 */ k = j; /* 将当前最小值的下标存入k */ } j++; } printf("(%d,%d)",adjvex[k],k); lowcost[k] = 0; /* 将当前顶点的权值设置为0, 表示此顶点已经完成任务 */ for(j=1;j<G.numVertexes; j++) /* 循环所有顶点 */ { /* 若下标为k顶点各边权值小雨此前这些顶点未被加入生成树权值 */ if (lowcost[j] != 0 && G.arc[k][j] < lowcost[j]) { lowcost[j] = G.arc[k][j]; /* 将较小权值存入lowcost */ adjvex[j] = k; /* 将下标为k的顶点存入adjvex */ } } } }
克鲁斯卡尔(Kruskal)算法
克鲁斯卡尔以边为目标去构建最小生成树,采用图的存储结构中的边集数组结构。
/* 对边集数组Edge结构的定义 */ typedef struct { int begin; int end; int weight; }Edge;
/* Kruskal算法生成最小生成树 */ void MiniSpanTree_Kruskal (MGraph G) /* 生成最小生成树 */ { int i, n, m; Edge edges[MAXEDGE]; /* 定义边集数组 */ int parent[MAXVEX]; /* 定义一数组用来判断边与边是否形成环路 */ /* 此处省略将邻接矩阵G转化为边集数组edges,并按权由小到大排列的代码 */ for (i=0; i<G.numVertexes; i++) parent[i] = 0; /* 初始化数组为0 */ for(i=0; i<G.numEdges; i++) /* 循环每一条边 */ { n = Find(parent, edges[i].begin); m = Find(parent, edges[i].end); if (n != m) /* 假如n与m不等,说明此边没有与现有生成树形成环路 */ { parent[n] = m; /* 将此边的结尾顶点放入下标为起点的parent中,表示此顶点已经在生成树集合中 */ printf("(%d,%d) %d ",edges[i].begin,edges[i].end,edges[i].weight); } } } int Find(int* parent, int f) /* 查找连线顶点的尾部下标 */ { while(parent[f] > 0) f = parent[f]; return f; }
对比两个算法,克鲁斯卡尔算法主要是针对边来展开,边数少时效率会非常高,所有对稀疏图有很大优势;而普利姆算法对于稠密图,即边数非常多的情况会更好一些。
最短路径
迪杰斯特拉(Dijkstra)算法
#define MAXVEX 9 #define INFINITY 65535 typedef int Pathmatirx[MAXVEX]; /* 用于存储最短路径下标的数组 */ typedef int ShortPathTable[MAXVEX]; /* 用于存储到各点最短路径的权值 */ /* Dijkstra算法,求有向图G的V0顶点到其余顶点v最短路径P[v]及带权长度D[v] */ void ShortestPath_Dijkstra(MGraph G, int v0, Pathmatirx* P, ShortPathTable* D) { int v,w,k,min; int final[MAXVEX]; /* final[w]=1表示求得顶点V0至Vw的最短路径 */ for(v=0,v<G.numVertexes;v++) /* 初始化数据 */ { final[v] = 0; /* 全部顶点初始化为未知最短路径状态 */ (*D)[v] = G.matirx[v0][v]; /* 将与V0点有连线的顶点加上权值 */ (*P)[v] = 0; /* 初始化路径数组P为0 */ } (*D)[v0] = 0; /* V0至V0路径为0 */ final[v0] = 1; /* V0至V0不需要求路径 */ /* 开始主循环,每次求得V0到某个V顶点的最短路径 */ for(v=1;v<G.numVertexes; v++) { min = INFINITY; for(w=0;w<G.numVertexes;w++) /* 寻找离V0最近的顶点 */ { if (! final[w] && (*D)[w]<min) { k = w; min = (*D)[w]; /* w顶点离V0顶点更近 */ } } final[k] = 1; /* 将目前找到的最近的顶点置为1 */ for(w=0;w<G.numVertexes;w++) /* 修正当前最短路径及距离 */ { /* 如果经过v顶点的路径比现在这条路径的长度短的话 */ if (! final[w] && (min+G.matirx[k][w]<(*D)[w])) { /* 说明找到了更短的路径,修改D[w]和P[w] */ (*D)[w] = min + G.matirx[k][w]; /* 修改当前路径的长度 */ (*P)[w] = k; } } } }
typedef int Pathmatirx[MAXVEX][MAXVEX]; typedef int ShortPathTable[MAXVEX][MAXVEX]; /* Floyd 算法,求网图G中各顶点v到其余顶点w最短路径P[v][w]及带权长度D[v][w] */ void ShortestPath_Floyd(MGraph G, Pathmatirx *P, ShortPathTable *D) { int v,w,k; /*初始化D与P */ for(v=0;v<G.numVertexes;v++) { for(w=0;w<G.numVertexes;w++) { (*D)[v][w] = G.matirx[v][w]; /* D[v][w]值即为对应点间的权值 */ (*P)[v][w] = w; /* 初始化 P */ } } for(k=0;k<G.numVertexes;k++) { for(v=0;v<G.numVertexes;v++) { for(w=0;w<G.numVertexes;w++) { /* 如果经过下标为k顶点路径比原两点间路径更短 */ /* 将当前两点间权值设为更小的一个 */ if ((*D)[v][w] > (*D)[v][k]+(*D)[k][w]) { (*D)[v][w] = (*D)[v][k]+(*D)[k][w]; (*P)[v][w] = (*P)[v][k]; /* 路径设置经过下标为k的顶点 */ } } } } }
AOV网:在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系。
拓扑排序:对一个有向图构造拓扑序列的过程。
typedef struct EdgeNode /* 边表结点 */ { int adjvex; /* 邻接点域,存储该顶点对应的下标 */ int weight; /* 用于存储权值,对于非网图可以不需要 */ struct EdgeNode *next; /* 链域,指向下一个邻接点 */ }EdgeNode; typedef struct VertexNode /* 顶点表结点 */ { int in; /* 顶点入度 */ int data; /* 顶点域,存储顶点信息 */ EdgeNode *firstedge; /* 边表头指针 */ }VertexNode, AdjList[MAXVEX]; typedef struct { AdjList adjList; int numVertexes, numEdges; /* 图中当前顶点数和边数 */ }graphAdjList,*GraphAdjList; /* 拓扑排序, 若GL无回路, 则输出拓扑排序序列并返回OK, 若有回路返回ERROR */ Status TopologicalSort(GraphAdjList GL) { EdgeNode *e; int i,k,gettop; int top = 0; /* 用于栈指针下标 */ int count = 0; /* 用于统计输出顶点的个数 */ int *stack; /* 建栈存储入度为0的顶点 */ stack = (int *)malloc(GL->numVertexes * sizeof(int)); for(i=0;i<GL.numVertexes;i++) if (GL->adjList[i].in = 0) stack[++top] = i; /* 将入度为0的顶点入栈 */ while(top != 0) { gettop = stack[top--]; /* 出栈 */ printf("%d -> ",GL->adjList[gettop].data); count++; /* 统计输出顶点数 */ /*对此顶点弧表遍历 */ for(e=GL->adjList[gettop].firstedge; e; e=e->next) { k = e->adjvex; if (!(--GL->adjList[k].in)) /* 将k号顶点邻接点的入度减1 */ stack[++top] = k; /* 若为0则入栈,以便于下次循环输出 */ } } if (count < GL->numVertexes) /* 如果count小于顶点数,说明存在环 */ return ERROR; else return OK; }
int *etv, *ltv; /* 事件最早发生时间和最迟发生时间数组 */ int *stack2; /* 用于存储拓扑序列的栈 */ int top2; /* 用于stack2的指针 */ /* 拓扑排序,用于关键路径计算 */ Status TopologicalSort(GraphAdjList GL) { EdgeNode *e; int i,k,gettop; int top = 0; /* 用于栈指针下标 */ int count = 0; /* 用于统计输出顶点的个数 */ int *stack; /* 建栈存储入度为0的顶点 */ stack = (int *)malloc(GL->numVertexes * sizeof(int)); for(i=0;i<GL.numVertexes;i++) if (GL->adjList[i].in = 0) stack[++top] = i; /* 将入度为0的顶点入栈 */ top2 = 0 ; /* 初始化为0 */ etv = (int *)malloc(GL->numVertexes*sizeof(int)); for(i=0;i<GL->numVertexes;i++) etv[i] = 0; /* 初始化为0 */ stack2 = (int *)malloc(GL->numVertexes * sizeof(int)); /* 初始化 */ while(top != 0) { gettop = stack[top--]; /* 出栈 */ printf("%d -> ",GL->adjList[gettop].data); count++; /* 统计输出顶点数 */ stack2[++top2] = gettop; /* 将弹出的顶点序号压入拓扑序列的栈 */ /*对此顶点弧表遍历 */ for(e=GL->adjList[gettop].firstedge; e; e=e->next) { k = e->adjvex; if (!(--GL->adjList[k].in)) /* 将k号顶点邻接点的入度减1 */ stack[++top] = k; /* 若为0则入栈,以便于下次循环输出 */ if ((etv[gettop]+e->weight)>etv[k]) /* 求各顶点事件最早发生时间值 */ etv[k] = etv[gettop]+e->weight; } } if (count < GL->numVertexes) /* 如果count小于顶点数,说明存在环 */ return ERROR; else return OK; } /* 求关键路径,GL为有向图,输出GL的各项关键活动 */ void CriticalPath(GraphAdjList GL) { EdgeNode *e; int i,gettop,k,j; int ete,lte; /* 声明活动最早发生时间和最迟发生时间变量 */ TopologicalSort(GL); /* 求拓扑序列,计算数组etv和stack2的值 */ ltv=(int *)malloc(GL->numVertexes*sizeof(int)); /* 事件最晚发生时间 */ for(i=0;i<GL->numVertexes;i++) ltv[i] = etv[GL->nmVertexes-1]; /* 初始化ltv */ while(top2 != 0) /* 计算ltv */ { gettop = stack2[top--]; /* 将拓扑序列出栈,后进先出 */ for(e = GL->adjList[gettop].firstedge; e; e->next) { /* 求各顶点事件的最迟发生时间ltv值 */ k = e->adjvex; /* 求各顶点事件最晚发生时间ltv */ if (ltv[k]-e->weight<ltv[gettop]) ltv[gettop] = ltv[k]-e->weight; } } /* 求ete,lte和关键活动 */ for(j=0;j<GL->numVertexes; j++) { for(e=GL->adjList[j].firstedge; e; e->next) { k = e->adjvex; ete = etv[j]; /* 活动最早发生时间 */ lte = ltv[k] - e->weight; /* 活动最迟发生时间 */ if(ete == lte) /* 两者相等即在关键路径上 */ { printf("<v%d,v%d> length: %d, ", GL->adjList[j].data,GL-.adjList[k].data,e->weight); } } } }