图结构
第一题:无向图连通分量个数
[问题描述]
采用邻接表存储结构,编写一个求无向图的连通分量个数的算法。
[输入]
图的结点个数,图的边数,各边的起点以及终点
[输出]
图的连通分量
[存储结构]
邻接表
[算法的基本思想]
创建邻接表:按照邻接表的结构特点,将对应边及其对应点的关系进行确定。连通分量的计算:采用dfs遍历个点,得到连通子图的连通分量,在从中选取最大值,作为图的连通分量。
#include<stdio.h> #include<malloc.h> #define MAXSIZE 10 #define NULL 0 typedef struct ArcNode{ //表结点 int adjvex; //邻接点,数组下标 struct ArcNode *next; //下一个表结点 }ArcNode, *anode; typedef struct VNode{ //头节点 ArcNode *firstNode; //第一个邻接点 }VNode; typedef struct Graph{ //邻接表 VNode adjNode[MAXSIZE]; //头结点集 int visit[MAXSIZE]; //初始值为0,被访问后变为1 int n; //顶点个数 int e; //边数 }Graph, *graph; int num[MAXSIZE]; //记录不同结点的连通分量 void creatGraph(graph &g){ //构造邻接表 g = (graph)malloc(sizeof(Graph)); printf("请输入顶点个数和边数:"); scanf("%d", &g->n); //记录结点数 scanf("%d", &g->e); //记录边数 int x, y, i; for(i = 0; i < MAXSIZE; i++){ //初始化visit数组并且初始化第一个邻接点 g->visit[i] = 0; g->adjNode[i].firstNode = (anode)malloc(sizeof(ArcNode)); g->adjNode[i].firstNode->next = NULL; //舍弃一个结点空间,作为头结点方便插入 } for(i = 0; i < g->e; i++){ //遍历各边,创建对应的表结点 printf("请输入第%d条边的起点和终点:", i + 1); scanf("%d", &x); //记录起点 scanf("%d", &y); //记录终点 anode p = (anode)malloc(sizeof(ArcNode)); //因为是无向图 anode q = (anode)malloc(sizeof(ArcNode)); //所以同时有两个表结点生成 p->adjvex = x; q->adjvex = y; p->next = g->adjNode[y].firstNode->next; //采用头插法 q->next = g->adjNode[x].firstNode->next; g->adjNode[y].firstNode->next = p; g->adjNode[x].firstNode->next = q; } } int dfs(graph g, int x, int i){ //采用dfs对一个指定结点开始进行遍历 //i的作用保证num数组计数的稳定性,所以在整体的一次递归中i不变 g->visit[x] = 1; //标记为已访问 anode p = g->adjNode[x].firstNode->next; while(p != NULL){ //对一个头结点后的表结点进行循环 if(g->visit[p->adjvex] == 0){ //判断是否被访问(是否满足递归条件) num[i]++; //结点数的记录(连通分量的记录) dfs(g, p->adjvex, i); } p = p->next; } return num[i]; } int main(){ graph g; creatGraph(g); int i, max = 0; for(i = 1; i < g->n; i++){ //遍历每个结点,求出连通分量 num[i] = dfs(g, i, i); if(max < num[i]){ //判断结点的最大连通分量,为图的连通分量 max = num[i]; } } printf("连通分量为:%d", max + 1); //在求连通分量时,忽略了头结点,故加1 }
结构演示:
结果与分析:
优点:通过dfs完成了实验要求,对子图的连通分量均可进行计算,空间利用率较高。缺点:不易操作两点间的关系。时间复杂度:O(n*2e),空间复杂度:O(n+2e),n为图的结点个数,e为边数。
第二题:有向图连通性判断
[问题描述]
试基于图的深度优先搜索策略编写一程序,判别以邻接表方式存储的有向图中是否存在有顶点Vi到Vj顶点的路径(i≠j)。
[输入]
图的结点个数,图的弧数,各弧的起点以及终点,输入要判断的结点vi, vj。
[输出]
若vi,vj之间存在路径为“vi, vj存在路径”
若vi,vj之间不存在路径为“vi, vj不存在路径”
[存储结构]
邻接表。
[算法的基本思想]
创建邻接表:按照邻接表的结构特点,将对应弧及其对应点的关系进行确定。是否存在路径的判断:采用dfs遍历vi点,若过程中有vj点存在则说明存在路径,若过程中没有vj则说明不存在路径。
#include<stdio.h> #include<malloc.h> #define MAXSIZE 10 #define NULL 0 typedef struct ArcNode{ //表结点 int adjvex; //邻接点,数组下标 struct ArcNode *next; //下一个表结点 }ArcNode, *anode; typedef struct VNode{ //头节点 ArcNode *firstNode; //第一个邻接点 }VNode; typedef struct Graph{ //邻接表 VNode adjNode[MAXSIZE]; //头结点集 int visit[MAXSIZE]; //初始值为0,被访问后变为1 int n; //顶点个数 int e; //边数 }Graph, *graph; void creatGraph(graph &g){ //构造邻接表 g = (graph)malloc(sizeof(Graph)); printf("请输入顶点个数和边数:"); scanf("%d", &g->n); //记录结点数 scanf("%d", &g->e); //记录边数 int x, y, i; for(i = 0; i < MAXSIZE; i++){ //初始化visit数组并且初始化第一个邻接点 g->visit[i] = 0; g->adjNode[i].firstNode = (anode)malloc(sizeof(ArcNode)); g->adjNode[i].firstNode->next = NULL; //舍弃一个结点空间,作为头结点方便插入 } for(i = 0; i < g->e; i++){ //遍历各边,创建对应的表结点 printf("请输入第%d条边的起点和终点:", i + 1); scanf("%d", &x); //记录起点 scanf("%d", &y); //记录终点 anode p = (anode)malloc(sizeof(ArcNode)); //因为有向图,所以只需一个表结点 p->adjvex = y; p->next = g->adjNode[x].firstNode->next;//采用头插法 g->adjNode[x].firstNode->next = p; } } int flag = 0; //利用flag记录是否访问到过y void dfs(graph g, int x, int y){ //采用dfs对一个指定结点开始进行遍历 g->visit[x] = 1; //标记为已访问 anode p = g->adjNode[x].firstNode->next; while(p != NULL){ //对一个头结点后的表结点进行循环 if(g->visit[p->adjvex] == 0){ //判断是否被访问(是否满足递归条件) if(p->adjvex == y){ //判断是否可以经过y flag = 1; } dfs(g, p->adjvex, y); } p = p->next; } } int main(){ graph g; creatGraph(g); printf("请输入要判断的俩个结点,不能为同一结点:"); int x, y; scanf("%d", &x); scanf("%d", &y); if(x != y){ dfs(g, x, y); if(flag == 1){ printf("V%d,V%d存在路径", x, y); } else{ printf("V%d,V%d不存在路径", x, y); } } else{ printf("输入不合法"); } }
结过演示:
结果与分析:
优点:在第一题的基础上进行改造,将无向图的结构改为有向图,通过dfs判断是否经过点确定两点间有无路径。缺点:对于特殊情况的处理措施较少。时间复杂度:O(n*e),空间复杂度:O(n+e),n为图的结点个数,e为弧数。
第三题:有向图最短边数 bfs
[问题描述]
给定一个图,设计一个程序,找出一条从某一顶点A到另一顶点B边数最少的一条路径。要求使用邻接表方式实现。
[输入]
图的结点个数,图的弧数,各弧的起点以及终点,输入要判断的结点vi, vj。
[输出]
若A到B无路径,则输出“A,B间不存在路径”,否则输出A到B路径上各顶点。
[存储结构]
邻接表。
[算法的基本思想]
创建邻接表:按照邻接表的结构特点,将对应弧及其对应点的关系进行确定。是否存在路径的判断:创建队列结构采用bfs遍历A点,若过程中有B点存在则说明存在路径且为最短路径,若过程中没有vj则说明不存在路径。使用num数组
#include<stdio.h> #include<malloc.h> #define MAXSIZE 10 #define NULL 0 typedef struct ArcNode{ //表结点 int adjvex; //邻接点,数组下标 struct ArcNode *next; //下一个表结点 }ArcNode, *anode; typedef struct VNode{ //头节点 ArcNode *firstNode; //第一个邻接点 }VNode; typedef struct Graph{ //邻接表 VNode adjNode[MAXSIZE]; //头结点集 int visit[MAXSIZE]; //初始值为0,被访问后变为1 int n; //顶点个数 int e; //边数 }Graph, *graph; int num[MAXSIZE]; //通过num数组进行bfs遍历时,该结点对应的连接点的记录,方便回溯 void creatGraph(graph &g){ //构造邻接表 g = (graph)malloc(sizeof(Graph)); printf("请输入顶点个数和边数:"); scanf("%d", &g->n); //记录结点数 scanf("%d", &g->e); //记录边数 int x, y, i; for(i = 0; i < MAXSIZE; i++){ //初始化visit数组并且初始化第一个邻接点 g->visit[i] = 0; g->adjNode[i].firstNode = (anode)malloc(sizeof(ArcNode)); g->adjNode[i].firstNode->next = NULL; //舍弃一个结点空间,作为头结点方便插入 } for(i = 0; i < g->e; i++){ //遍历各边,创建对应的表结点 printf("请输入第%d条边的起点和终点:", i + 1); scanf("%d", &x); //记录起点 scanf("%d", &y); //记录终点 anode p = (anode)malloc(sizeof(ArcNode)); //因为有向图,所以只需一个表结点 p->adjvex = y; p->next = g->adjNode[x].firstNode->next;//采用头插法 g->adjNode[x].firstNode->next = p; } } void bfs(graph g, int A){ //采用广度优先进行遍历 ArcNode *p; int queue[MAXSIZE]; //创建队列记录结点 int front = 0; //队列初始化 int rear = 0; g->visit[A] = 1; //结点被访问 queue[rear++] = A; //结点入队 while(front != rear){ //判断队列是否为空 p = g->adjNode[queue[front]].firstNode->next; while(p != NULL){ //遍历一个头结点的表结点 if(g->visit[p->adjvex] == 0){ //是否满足入队条件 g->visit[p->adjvex] = 1; num[p->adjvex] = queue[front]; //使用num记录其bfs遍历时的相连结点 queue[rear++] = p->adjvex; } p = p->next; } front++; } } int main(){ graph g; creatGraph(g); printf("请输入要判断最短路径的俩结点:"); int A, B; scanf("%d", &A); scanf("%d", &B); bfs(g, A); int i; i = num[B]; //查看使用bfs遍历时,B的直接关联结点 printf("%d", B); while(true){ //通过对num数组的遍历可以确定A到B的完整路径 if(i == A){ //若找到A,路径完成,退出 break; } else printf("<-%d", i); i = num[i]; } printf("<-%d", A); }
结果演示:
结果与分析:
优点:使用num数组记录bfs遍历时的直接相连结点,解决了输出路径的问题。缺点:若存在同样短的路时,只能判断随机一条,对特殊没有路径的情况没有判断。时间复杂度:O(n*e),空间复杂度:O(n+e),n为图的结点个数,e为弧数。
心得
- 学会了使用邻接表的存储方式对图进行存取。
- 邻接表的理解:邻接表由头结点集,以及表结点组成,则头结点集可通过头节点数据类型的数组实现,而表结点可通过单个申请空间获得。无向图一个边,对应两个表结点,通过头结点与表结点之间建立类似链表的关系,便可实现边的表示。
- 头结点中的firstNode指针可以看作类似于链表中的头结点,为了方便插入表结点时使用头插法,故舍弃一个结点空间,方便操作。
- dfs遍历图的理解:图的深度优先遍历与树的先序遍历类似,在树中,可以通过左右子树实现递归规模的控制,而在图中有多个分支,就需要while(p != NULL)与p = p->next来进行图的递归规模的控制,在树的非递归中我使用过flag来记录左右子树是否被访问过,那么在图中同样也需要visit数组判断结点是否被访问过。把图dfs中经过的边保留,其余边删去,就可得到图的生成树。
- 判断两点间是否存在路径,只需对起点经行dfs遍历,通过遍历过程中有无经过终点,便可判断有无路径存在。
- bfs遍历图的理解:bfs类似于树的层次遍历,需要队列的数据结构进行辅助实现(队列可以是int型,而非结点的数据类型,因为数组下标可以代表图中结点)。在图为非加权图时,bfs的遍历顺序其实就是两点最短路径的结点次序。
- 最短路径:在bfs在每次遍历时:一次出队列的结点与因为其出队列而入队列的结点的是相连接,要解决的问题就是记录对应结点是因为那个结点而入队的,在此题中使用了一个全局变量num进行记录,在根据对num的访问使用终点,逆推到起点便可得到最短路径。