顶点(Vertex)、弧(Arc)、弧头(初始点)、弧尾(终结点)、边(Edge)、有向图(Directed graph)、无向图(Undigraph)、完全图(Completed grapg)、有向完全图、稀疏图(Sparse graph)、稠密图(Dense graph)、权(weigh)、网(network)、无向网、有向网、子图(Subgraph)、邻接点(Adjacent)、度(Degree)、入度(Indegree)、出度(Outdegree)、路径(path)、简单路径、回路(环)、连通、连通图(Connected graph)、连通分量(Connected Component)、强连通图、强连通分量(有向图中的极大强连通子图)、生成树、极小连通子图、有向树。
无向图:G=(V, {E})、0≤边≤n(n-1)/2
有向图:G=(V, {A})、0≤弧≤n(n-1)
生成树:一个连通图的生成树是一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。一个有n个顶点的生成树有且仅有n-1条边。如果一个图有n和顶点和小于n-1条的边,则是非连通图。如果多余n-1条边,则一定有环。但有n-1条边的图不一定是生成树。
(无向图、有向图、无向网、有向网)
用两个数组来表示图。一个一维数组存储图中数据元素(顶点)的信息,一个二维数组(邻接矩阵)存储图中数据元素之间的关系(边或弧)的信息。
若图G是无向图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
下图就是一个无向图:
从上面可以看出,无向图的边数组是一个对称矩阵。所谓对称矩阵就是n阶矩阵的元满足aij = aji。即从矩阵的左上角到右下角的主对角线为轴,右上角的元和左下角相对应的元全都是相等的。
从这个矩阵中,很容易知道图中的信息。
(1)要判断任意两顶点是否有边无边就很容易了;
(2)要计算某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行或(第i列)的元素之和;
(3)求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,arc[i][j]=1的vj就是邻接点;
而有向图有入度和出度之分:顶点vi的入度为是第i列各数之和,顶点vi的出度是第i行的各数之和。
若图G是网图,有n个顶点,则邻接矩阵是一个n*n的方阵,定义为:
wij表示(vi,vj)或<vi,vj>上的权值。无穷大表示一个计算机允许的、大于所有边上权值的值,也就是一个不可能的极限值。下图是一个有向网图和它的邻接矩阵:
可以看出:
(1)第i行权重大于0、小于无穷的个数之和为顶点vi的出度(OD(vi));
(2)第j列权重大于0、小于无穷的个数之和为顶点vj的入度(ID(vj));
(3)第i行和第i列权重大于0、小于无穷的个数之和即为顶点vi的度(TD(vi) = OD(vi) + ID(vj))。
使用邻接矩阵存储图并创建图的代码示例如下:
typedef char VertexType; //顶点类型,由用户自己定义 typedef int EdgeType; //边上的权值类型,由用户自己定义 #define MAX_VEX 20 //最大顶点数,由用户定义 #define INFINITY 65535 //代表无穷大 typedef struct { VertexType vexs[MAX_VEX]; //顶点数组 EdgeType arc[MAX_VEX][MAX_VEX]; //邻接矩阵 int vexNum, arcNum; //图中当前定点数和弧数 }Graph;
//定位顶点v在顶点数组的下标位置,不存在返回-1 int LocateVex(Graph *g, VertexType v) { int i = 0; for (i = 0; i < g->vexNum; i++) { if (g->vexs[i] == v) break; } if (i > g->vexNum) { fprintf(stderr,"no such vertex.\n"); return -1; } return i; } //用邻接矩阵表示法,构造有/无向网g void CreateUDN(Graph *g) { int i, j; EdgeType w; printf("输入顶点数和边数:\n"); scanf("%d,%d", &(g->vexNum), &(g->arcNum)); //输入顶点 for(i = 0; i < g->vexNum; i++) { printf("顶点%d:", i + 1); g->vexs[i] = getchar(); while(g->vexs[i] == '\n') { g->vexs[i] = getchar(); } } getchar(); //初始化邻接矩阵 for (i = 0; i < g->arcNum; i++) { for (j = 0; j < g->arcNum; j++) { g->arc[i][j] = INFINITY; } } printf("输入边(vi, vj)上的顶点vi vj和权值w\n"); for (i = 0; i < g->arcNum; i++) { char v1, v2; //v1 = getchar(); //while (v1 == '\n') //{ // v1 = getchar(); //} //v2 = getchar(); //while (v2 == '\n') //{ // v2 = getchar(); //} //scanf("%d", &w); scanf("%c %c %d", &v1, &v2, &w); getchar(); int m = LocateVex(g, v1); int n = LocateVex(g, v2); if (m == -1 || n == -1) { fprintf(stderr, "no this vertex!\n"); return; } g->arc[m][n] = w; #if 0 g->arc[n][m] = g->arc[m][n]; //无向图的邻接矩阵对称 #endif } } void PrintGrapth(Graph *g) { for (int i = 0; i < g->vexNum; i++) { for (int j = 0; j < g->vexNum; j++) { printf("%6d", g->arc[i][j]); } putchar('\n'); } }
int main() { Graph g; CreateUDN(&g); PrintGrapth(&g); return 0; }
创建上文中的有向网图,程序运行示例截图:
创建无向网图,运行截图为:
n个顶点和e条边的无向网图的创建,时间复杂度为O(n + n^2 + e),其中对邻接矩阵的初始化耗费了O(n^2)的时间。
使用邻接表储图并创建网图的代码示例如下:
typedef char VertexType; //顶点类型,由用户自己定义 typedef int EdgeType; //边上的权值类型,由用户自己定义 #define MAX_VEX 20 //最大顶点数,由用户定义 typedef struct EdgeNode //边表结点 { int adjvex; //邻接点域,存储该顶点在顶点表中的下标 EdgeType weight; //网图权值 struct EdgeNode *next; //链域,指向下一个邻接点 }EdgeNode; typedef struct VertexNode //顶点表结点 { VertexType data; //顶点域,存储顶点信息 EdgeNode *firstedge; //边表头指针 }VertexNode, AdjList[MAX_VEX]; typedef struct { AdjList adjList; int vexNum, arcNum; //图中当前顶点数和弧数 }GraphList;
//定位顶点v在顶表的下标位置,不存在返回-1 int LocateVex(GraphList *g, VertexType v) { int i; for (i = 0; i < g->vexNum; i++) { if (v == g->adjList[i].data ) break; } if (i > g->vexNum) { fprintf(stderr,"no this vertex.\n"); return -1; } return i; } //用邻接表表示法,构造有/无向网g void CreateGraph(GraphList *g) { int i, j; EdgeType w; EdgeNode *e, *f; printf("输入顶点数和边数:\n"); scanf("%d,%d", &(g->vexNum), &(g->arcNum)); //输入顶点 for (i = 0; i < g->vexNum; i++) { printf("顶点%d:", i); g->adjList[i].data = getchar(); g->adjList[i].firstedge = NULL; while(g->adjList[i].data == '\n') { g->adjList[i].data = getchar(); } } getchar(); //建立边表 printf("输入边(vi, vj)上的顶点vi vj和权值w\n"); for (i = 0; i < g->arcNum; i++) { char v1, v2; scanf("%c %c %d", &v1, &v2, &w); getchar(); int m = LocateVex(g, v1); int n = LocateVex(g, v2); if (m == -1 || n == -1) { fprintf(stderr, "no this vertex!\n"); return; } //向内存申请空间,生成边表结点 e = (EdgeNode*)malloc(sizeof(EdgeNode)); if(e == NULL) { fprintf(stderr, "malloc() error.\n"); return; } e->adjvex = n; //邻接序号为n e->weight = w; //权值 e->next = g->adjList[m].firstedge; //单链表的头插法 g->adjList[m].firstedge = e; #if 0 //因为是无向网图 f = (EdgeNode*)malloc(sizeof(EdgeNode)); if(e == NULL) { fprintf(stderr, "malloc() error.\n"); return; } f->adjvex = m; f->weight = w; f->next = g->adjList[n].firstedge; g->adjList[n].firstedge = f; #endif } } void PrintGrapth(GraphList *g) { int i; for (i = 0; i < g->vexNum; i++) { printf("顶点%c ", g->adjList[i].data) ; EdgeNode *e = g->adjList[i].firstedge; while (e != NULL) { printf("—> <%c, %c> %d ", g->adjList[i].data, g->adjList[e->adjvex].data, e->weight); e = e->next; } putchar('\n'); } }
int main() { GraphList g; CreateGraph(&g); PrintGrapth(&g); return 0; }
创建上文中的有向网图,程序运行示例截图:
若创建无向网图,运行截图为:
对于n个顶点e条边,本算法的时间复杂度是O(n*e),因为输入的顶点信息不是顶点的编号,需要通过查找才能够得到顶点在图中的位置。如果输入的是顶点的编号,那么只需要O(n+e)的时间复杂度。
对于有向图,邻接表可以方便的求某一个顶点的出度,但是不方便求入度,需要遍历整个边表。逆邻接表可以方便求入度,但不方便求出度。
另外关于图的存储结构还有:十字链表、邻接多重表。
图的遍历(Traversing Graph):从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次。图的遍历算法是求解图的连通性问题、拓扑排序和求关键路径等算法的基础。
深度优先搜索(Depth First Search),简称DFS,其遍历类似树的前序遍历。
它从图中某个结点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中的所有顶点都被访问到为止。
邻接矩阵存储结构的DFS代码:
bool visited[MAX_VEX];//访问标志数组 //邻接矩阵的深度优先递归算法 void DFS(Graph *g, int i) { int j; visited[i] = true; printf("%c ", g->vexs[i]); //打印顶点,也可以是其他操作 for (j = 0; j < g->vexNum; j++) if (g->arc[i][j] > 0 && g->arc[i][j] != INFINITY && !visited[j]) DFS(g, j); } //邻接矩阵的深度遍历操作 void DFSTraverse(Graph *g) { int i; for (i = 0; i < g->vexNum; i++) visited[i] = false; //初始化所有顶点状态都是未访问过状态 for (i = 0; i < g->vexNum; i++) if (!visited[i]) DFS(g, i); //对未访问的顶点调用DFS.若是连通图,只会执行一次 }
//邻接矩阵的深度优先递归算法 void DFS(GraphList *g, int i) { EdgeNode *e; visited[i] = true; printf("%c ", g->adjList[i].data); //打印顶点,也可以是其他操作 for (e = g->adjList[i].firstedge; e != NULL; e = e->next) if (!visited[e->adjvex]) DFS(g, e->adjvex); } //邻接矩阵的深度遍历操作 void DFSTraverse(GraphList *g) { int i; for (i = 0; i < g->vexNum; i++) visited[i] = false; //初始化所有顶点状态都是未访问过状态 for (i = 0; i < g->vexNum; i++) if (!visited[i]) DFS(g, i); //对未访问的顶点调用DFS.若是连通图,只会执行一次 }
广度优先搜索(Breadth First Search),简称DFS,其遍历类似树的层次遍历。
假设从图中某顶点v出发,在访问了v之后依次访问v的各个未曾访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点“先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到。若此时图中尚有顶点未被访问,则选中图中一个未曾被访问的顶点 作起始点,重复上述过程直至图中所有顶点都被访问到为止。
//邻接矩阵的BFS void BFSTraverse(Graph *g) { SqQueue q; int v; for (v = 0; v < g->vexNum; v++) visited[v] = false; InitQueue(q); //辅助队列 for (v = 0; v < g->vexNum; v++) if (!visited[v]) //第v个顶点尚未访问 { visited[v] = true; printf("%c ", g->vexs[v]); //打印顶点,也可以是其他操作 EnQueue(q, v); //第v个顶点入队列 while (!QueueEmpty(q)) { int i; DeQueue(q, i); for (int j = 0; j < g->vexNum; j++) //判断其他顶点若与当前顶点存在边且未访问过 if (g->arc[i][j] > 0 && g->arc[i][j] != INFINITY && !visited[j]) { visited[j] = true; //j为i尚未访问过的邻接顶点 printf("%c ", g->vexs[j]); EnQueue(q, j); } //if }//while }//if }
//邻接表的BFS void BFSTraverse(GraphList *g) { SqQueue q; int i; for (i = 0; i < g->vexNum; i++) visited[i] = false; InitQueue(q); for (i = 0; i < g->vexNum; i++) if (!visited[i]) { visited[i] = true; printf("%c ", g->adjList[i].data); EnQueue(q, i); while (!QueueEmpty(q)) { int j; DeQueue(q, j); //找到当前顶点边表链表头指针 for (EdgeNode *e = g->adjList[j].firstedge; e != NULL; e = e->next) if (!visited[e->adjvex]) { visited[e->adjvex] = true; printf("%c ", g->adjList[e->adjvex].data); EnQueue(q, e->adjvex); } //if }//while } //if }
图的深度优先遍历与广度优先遍历算法在时间复杂度上是一样的,不同之处仅仅在于对顶点的访问顺序不同。
参考:数据结构(C语言版)
本文全部测试代码:http://download.csdn.net/detail/u013071074/7445893
蓝色梦魔
2014.6.4,16:20
数据结构之图(术语、存储结构、遍历),布布扣,bubuko.com
原文地址:http://blog.csdn.net/u013071074/article/details/28308275