几乎快近一个月没有写博客,因为一些事耽误了,或者去看其他方面的书去了。另外关于图,犹豫了很久,是否写这一章,因为也听同学和师兄师姐们说过,对于面试和笔试来说,图难度更大一些,而且代码量也更大一些,所以一般不会作为笔试和面试的首选考察对象,考察最多的就是链表、二叉树和字符串了。但是作为数据结构中最难的一章,其实图的应用应该比较多,另外这也是最后一次系统性的学习数据结构了,学习中没有点迎难而上的精神怎么能行?
好了,言归正传,关于图的术语这些知识略去,可以参考任何一本数据结构的书,但是与本篇文章相关的知识还是要介绍一下的,现在开始了,其中本文的大部分内容和代码参考《大话数据结构》,其中用邻接表的尾插法创建图参考的是《妙趣横生的算法 C语言实现》
1.用邻接矩阵创建图
图的邻接矩阵存储方式是用两个数组来表示图,一个一维数组储存图中定点信息,一个二维数组(邻接矩阵)存储储存图中边或弧的信息。
文字还是没有图来的直观,其实看着图,感觉代码就出来了,下面的邻接表也是根据图来写代码的,邻接表中的图更能体现图形的优势。
代码对应如下:
#define MAXVEX 100
#define INFINITY 65536
struct MGraph{
int vex[MAXVEX]; //顶点表
int arc[MAXVEX][MAXVEX]; //邻接矩阵,可看左边表
int numVertexes,numEdges; //图中的顶点数和边数
};
//用邻接矩阵创建图
void createMGraph(MGraph *mGraph){
printf("请输入图的顶点数和边数:\n");
scanf("%d%d",&mGraph->numVertexes,&mGraph->numEdges);
int i,j,k,w;
printf("请输入个顶点:\n");
for(i = 0;i < mGraph->numVertexes; i++){
scanf("%d",&mGraph->vex[i]);
}
for (i = 0; i < mGraph->numVertexes; i++)
for(j = 0; j < mGraph->numVertexes;j++)
mGraph->arc[i][j] = INFINITY;
for(k = 0;k < mGraph->numEdges; k++){
printf("请输入边(vi,vj)上的下标i,下标j和权值w:\n");
scanf("%d%d%d",&i,&j,&w);
mGraph->arc[i][j]=mGraph->arc[j][i]=w;
}
}
写的过程中发现了一处小问题,当邻接矩阵的图顶点如果用char表示的话,上文是用的int,如果用%c读取顶点,注意在读取顶点之前和之后用getchar()获得敲下的回车符,代码还是非常简单的,一看就明白,先初始化总顶点和边数,然后初始化各顶点和个边的权值,最后输入实际的边的值,最后用了无向图的邻接矩阵是对称矩阵的性质。
2.用邻接表创建图
相比邻接矩阵创建图,用邻接表创建图就复杂多了。采用的是用数组和链表相结合的方法创建图,看图
其实邻接表的方式,就是上面图就是用data和firstedge表示的顶点信息和用adjext和next构成的链表表示边表的信息,最后组合就是图了,我们知道链表的创建有两种方式,一种是头插法和尾插法,既然图中用到了链表,那么肯定也有两种方法
struct EdgeNode{ //边表结点
int adjex; //邻接点域,存储该顶点对应的下标
int weight; //权值
struct EdgeNode *next; //指向下一个邻接点
};
typedef struct VexNode{ //顶点表结点
int data; //顶点
EdgeNode *firstarc; //边表头指针
}AdjList[MAXVEX];
struct LGraph{
int numVertexes,numEdges;
AdjList adjList;
};
//用邻接表创建图,其中边表结点链表使用的是头插法
void createLGraph(LGraph *lGraph){
printf("请输入图的顶点数和边数:\n");
scanf("%d%d",&lGraph->numVertexes,&lGraph->numEdges);
int i,j,k,w;
printf("请输入个顶点:\n");
for(i = 0;i < lGraph->numVertexes; i++){
scanf("%d",&lGraph->adjList[i].data);
lGraph->adjList[i].firstarc = NULL;
}
for(k = 0;k < lGraph->numEdges; k++){
printf("请输入边(vi,vj)上的下标i,下标j:\n");
scanf("%d%d",&i,&j);
EdgeNode *e = (EdgeNode*)malloc(sizeof(struct EdgeNode));
e->adjex = j;
e->next = lGraph->adjList[i].firstarc;
lGraph->adjList[i].firstarc = e;
e = (struct EdgeNode*)malloc(sizeof(struct EdgeNode));
e->adjex = i;
e->next = lGraph->adjList[j].firstarc;
lGraph->adjList[j].firstarc = e;
}
}
//用邻接表创建图,其中边表的链表使用的是尾插法
void createLGraph2(LGraph *graph){
printf("请输入图的总顶点数和总边数:\n");
scanf("%d%d",&graph->numVertexes,&graph->numEdges);
int i,j,k;
printf("请输入个顶点:\n");
for(i=0;i<graph->numVertexes;i++){
scanf("%d",&graph->adjList[i].data);
graph->adjList[i].firstarc = NULL;
}
EdgeNode *e = NULL,*q = NULL,*p = NULL;
for(i=0;i<graph->numVertexes;i++){
printf("创建第%d个顶点的所有边,输入-1结束\n",i);
int x;
scanf("%d",&x);
while(x!=-1){
e = (struct EdgeNode *)malloc(sizeof(struct EdgeNode));
e->adjex = x;
e->next = NULL;
if(graph->adjList[i].firstarc == NULL){
graph->adjList[i].firstarc = e;
}
else{
p->next = e;
}
p = e;
scanf("%d",&x);
}
}
}
由于
用到了指针,相对来说变复杂了,出现的错误也就多了,对此总结
1.直观的感受是用头插法的代码剪短的多,而且还不用像尾插法那样需要最后输入一个-1判断某个顶点输入完毕,其原因是因为头插法每次都保持和firstarc链接上,当然头插法肯定是逆序的,而尾插法每次都是和最后一个结点链接,需要用到一个记录的结点p
2.在用尾插法创建图的时候,一开始以为直接复制粘贴就行了,发现了其实不然,因为头插法用的头结点,可以把j的顶点插入到i的顶点,也可以把i的顶点插入到j的顶点,而用尾插法如果想同时插入i和j顶点的话,是不行了,因为尾插法需要一次性把所有的关联的顶点全部插入完毕
3.用上面的头插法更适合无向图,而尾插法更适用有向图,因为无向图是对称的,而尾插法,按道理说我在输入顶点1的时候输入了2,代表1和2之间有边,当等到顶点2输入的时候,本不应该重复输入1的结点, 但是代码需要每次都输入每个顶点相连的顶点
理解了图的这两种表示方式,那么对于由邻接矩阵转换为邻接表的表示方式就呼之欲出了
//将图的邻接矩阵的表示形式转换为邻接表表示
LGraph* convert(MGraph *mgraph){
int i,j;
LGraph *lgraph = (LGraph *)malloc(sizeof(struct LGraph));
lgraph->numVertexes = mgraph->numVertexes;
lgraph->numEdges = mgraph->numEdges;
for(i=0;i<mgraph->numVertexes;i++){
lgraph->adjList[i].data = mgraph->vex[i];
lgraph->adjList[i].firstarc = NULL; //别忘了初始化
}
for(i=0;i<mgraph->numVertexes;i++){
struct EdgeNode *p = NULL;
for(j=0;j<mgraph->numVertexes;j++){
struct EdgeNode *e = NULL;
if(mgraph->arc[i][j]!=INFINITY){
e = (struct EdgeNode *)malloc(sizeof(struct EdgeNode));
e->next = NULL;
e->adjex = j;
if(lgraph->adjList[i].firstarc == NULL){
lgraph->adjList[i].firstarc = e;
}
else{
p->next = e;
}
p = e;
}
}
}
return lgraph;
}
最后,总的代码如下,最后还加上了关于显示邻接矩阵和邻接表的两个display函数:
#include <stdio.h>
#include <stdlib.h>
#define MAXVEX 100
#define INFINITY 65536
struct MGraph{
int vex[MAXVEX]; //顶点表
int arc[MAXVEX][MAXVEX]; //邻接矩阵,可看左边表
int numVertexes,numEdges; //图中的顶点数和边数
};
struct EdgeNode{ //边表结点
int adjex; //邻接点域,存储该顶点对应的下标
int weight; //权值
struct EdgeNode *next; //指向下一个邻接点
};
typedef struct VexNode{ //顶点表结点
int data; //顶点
EdgeNode *firstarc; //边表头指针
}AdjList[MAXVEX];
struct LGraph{
int numVertexes,numEdges;
AdjList adjList;
};
//用邻接矩阵创建图
void createMGraph(MGraph *mGraph){
printf("请输入图的顶点数和边数:\n");
scanf("%d%d",&mGraph->numVertexes,&mGraph->numEdges);
int i,j,k,w;
//getchar();
printf("请输入个顶点:\n");
for(i = 0;i < mGraph->numVertexes; i++){
//scanf("%c",&mGraph->vex[i]);
scanf("%d",&mGraph->vex[i]); //在处理%c的时候,需要注意前面输入顶点数和边数的最后的回车键,需要用getchar获取到
}
//getchar();
for (i = 0; i < mGraph->numVertexes; i++)
for(j = 0; j < mGraph->numVertexes;j++)
mGraph->arc[i][j] = INFINITY;
for(k = 0;k < mGraph->numEdges; k++){
printf("请输入边(vi,vj)上的下标i,下标j和权值w:\n");
scanf("%d%d%d",&i,&j,&w);
mGraph->arc[i][j]=mGraph->arc[j][i]=w;
}
}
//用邻接表创建图,其中边表结点链表使用的是头插法
void createLGraph(LGraph *lGraph){
printf("请输入图的顶点数和边数:\n");
scanf("%d%d",&lGraph->numVertexes,&lGraph->numEdges);
int i,j,k,w;
printf("请输入个顶点:\n");
for(i = 0;i < lGraph->numVertexes; i++){
scanf("%d",&lGraph->adjList[i].data);
lGraph->adjList[i].firstarc = NULL;
}
for(k = 0;k < lGraph->numEdges; k++){
printf("请输入边(vi,vj)上的下标i,下标j:\n");
scanf("%d%d",&i,&j);
EdgeNode *e = (EdgeNode*)malloc(sizeof(struct EdgeNode));
e->adjex = j;
e->next = lGraph->adjList[i].firstarc;
lGraph->adjList[i].firstarc = e;
e = (struct EdgeNode*)malloc(sizeof(struct EdgeNode));
e->adjex = i;
e->next = lGraph->adjList[j].firstarc;
lGraph->adjList[j].firstarc = e;
}
}
//用邻接表创建图,其中边表的链表使用的是尾插法
void createLGraph2(LGraph *graph){
printf("请输入图的总顶点数和总边数:\n");
scanf("%d%d",&graph->numVertexes,&graph->numEdges);
int i,j,k;
printf("请输入个顶点:\n");
for(i=0;i<graph->numVertexes;i++){
scanf("%d",&graph->adjList[i].data);
graph->adjList[i].firstarc = NULL;
}
EdgeNode *e = NULL,*q = NULL,*p = NULL;
for(i=0;i<graph->numVertexes;i++){
printf("创建第%d个顶点的所有边,输入-1结束\n",i);
int x;
scanf("%d",&x);
while(x!=-1){
e = (struct EdgeNode *)malloc(sizeof(struct EdgeNode));
e->adjex = x;
e->next = NULL;
if(graph->adjList[i].firstarc == NULL){
graph->adjList[i].firstarc = e;
}
else{
p->next = e;
}
p = e;
scanf("%d",&x);
}
}
}
//将图的邻接矩阵的表示形式转换为邻接表表示
LGraph* convert(MGraph *mgraph){
int i,j;
LGraph *lgraph = (LGraph *)malloc(sizeof(struct LGraph));
lgraph->numVertexes = mgraph->numVertexes;
lgraph->numEdges = mgraph->numEdges;
for(i=0;i<mgraph->numVertexes;i++){
lgraph->adjList[i].data = mgraph->vex[i];
lgraph->adjList[i].firstarc = NULL;
}
for(i=0;i<mgraph->numVertexes;i++){
struct EdgeNode *p = NULL;
for(j=0;j<mgraph->numVertexes;j++){
struct EdgeNode *e = NULL;
if(mgraph->arc[i][j]!=INFINITY){
e = (struct EdgeNode *)malloc(sizeof(struct EdgeNode));
e->next = NULL;
e->adjex = j;
if(lgraph->adjList[i].firstarc == NULL){
lgraph->adjList[i].firstarc = e;
}
else{
p->next = e;
}
p = e;
}
}
}
return lgraph;
}
void display(MGraph *graph){
int i,j;
for(i=0;i<graph->numVertexes;i++){
for(j=0;j<graph->numVertexes;j++){
printf("%d ",graph->arc[i][j]);
}
printf("\n");
}
}
void display(LGraph *graph){
int i,j;
for(i=0;i<graph->numVertexes;i++){
EdgeNode *tmp = graph->adjList[i].firstarc;
printf("当前顶点是%d ",graph->adjList[i].data);
while(tmp){
printf("%d ",tmp->adjex);
tmp = tmp->next;
}
printf("\n");
}
}
int main(){
MGraph metrixGraph;
createMGraph(&metrixGraph);
display(&metrixGraph);
LGraph lgraph;
createLGraph(&lgraph);
display(&lgraph);
createLGraph2(&lgraph);
display(&lgraph);
printf("讲邻接矩阵表示的图转换为用邻接表表示:\n");
lgraph = *convert(&metrixGraph);
display(&lgraph);
return 0;
}
未完待续 下一节 图算法二之DFS
如果文章有什么错误或者有什么建议,欢迎提出,大家共同交流,一起进步
文章转载请注明出处,请尊重知识产权