图表示是一种多对多的关系的数据结构。因为线性表表示的是一种一对一的关系的数据结构,树表示的是一种一对多的数据结构,所以图把线性表和树都包含在内。图由一个非空的有限顶点集合和一个有限边集合组成。当我们描述图的时候,一定要包含以下两个元素:
1、一组顶点:例如用Vertex表示顶点的集合。
2、一组边:用Edge表示边的集合,一条边是一组顶点对。
•无向边 :(i,j)∈Edge,i,j∈Vertex。即双向的,即可从i走到j,也可以从j走到i。
•有向边 :< i,j >∈Edge,i,j∈Vertex。即单向的(单行线),只可从i走到j或从j走到i。
•图不考虑重边和自己连自己的情况。
初始化一个图是,可以一条边也没有,但是不能一个顶点也没有。
那么怎么用代码表示一个图?
第一种方法是邻接矩阵表示法:即用矩阵,一个二维数组来表示一个图。
#include <stdio.h>
#include <stdlib.h>
#define MaxVertex 100 /*最大顶点数*/
#define INFINITY 65535 /*双字节无符号整数的最大值*/
typedef int Vertex; /*二维数组的下标表示顶点,为整型*/
typedef int WeightType; /*边的权值的类型*/
typedef char DataType; /*顶点存储的数据类型为字符型*/
/*图的数据结构*/
typedef struct GraphNode *PtrlGraphNode;
struct GraphNode {
int numv; /*顶点数*/
int nume; /*边数*/
WeightType wt[MaxVertex][MaxVertex]; /*邻接矩阵*/
DataType Data[MaxVertex]; /*存放顶点的数据*/
/*很多情况下,顶点无数据,此时Data[]可以不用*/
};
typedef PtrlGraphNode Graph;/*用邻接矩阵存储的图*/
用一个二维数组来表示图中一组顶点对的关系,还有一个一维数组来存放顶点的数据。
/*边的数据结构*/
typedef struct EdgeNode *PtrlEdgeNode;
struct EdgeNode {
Vertex V1, V2; /*有向边<V1, V2>*//*无向边(V1, V2)*/
WeightType weight; /*权重*/
};
typedef PtrlEdgeNode Edge;
边的数据结构,一条边由一组顶点对和权重表示(即顶点对的关系)。
表示图的各个数据结构都建立好后,就开始建立一个图,思路是先创建一个有全部顶点但没有边的图,在逐条插入边。
/*建立一个有VertexNum个顶点但没有边的空图*/
Graph CreatGraph(int VertexNum)
{
Vertex i,j;
Graph MyGraph;
MyGraph=(Graph)malloc(sizeof(struct GraphNode));/*申请空间建立图*/
MyGraph->numv=VertexNum;
MyGraph->nume=0; //初始化边数为0;
/*对二维数组矩阵初始化*/
for (i=0;i<MyGraph->numv;i++) {
for (j=0;j<MyGraph->numv;j++) {
MyGraph->wt[i][j]=INFINITY;
}
}
return MyGraph;
}
CreatGraph函数中输入一个值来创建有Vertexnum个结点的图,然后初始化二维数组中的元素为无穷(或0)。
/*插入边*/
void InsertEdge(Graph MyGraph, Edge MyEdge)
{
/*插入边<V1, V2>*/
MyGraph->wt[MyEdge->V1][MyEdge->V2]=MyEdge->weight;
/*如果是无向图,还要加上插入边<V2, V1>*/
MyGraph->wt[MyEdge->V2][MyEdge->V1]=MyEdge->weight;
}
然后插入边,把边的V1,V2,也就是两个顶点(对应二维数组的两个下标),以及权值插入到对应二维数组的位置。
Graph BuildGraph()
{
Graph MyGraph; /*建图*/
Edge MyEdge; /*建边*/
Vertex v;
int VertexNum,i;
printf("请输入顶点个数");
scanf("%d", &VertexNum);
MyGraph=CreatGraph(VertexNum);/*建立一个有numv个顶点,没有边的图*/
printf("请输入边数:");
scanf("%d", &MyGraph->nume); /* 读入边数 */
MyEdge=(Edge)malloc(sizeof(struct EdgeNode));/*建立边结点*/
/*插入边:格式为:"起点->终点->权重"。插入到邻接矩阵*/
for (i=0;i<MyGraph->nume;i++) {
scanf("%d %d %d",&MyEdge->V1, &MyEdge->V2, &MyEdge->weight);
InsertEdge(MyGraph, MyEdge);
}
/*如果顶点有数据,就读入数据*/
for (i=0;i<MyGraph->numv;i++) {
scanf("%c", &MyGraph->Data[i]);
}
return MyGraph;
}
Graph[ n ][ n ]二维数组中,用n个顶点从0到n-1编号。如果一组顶点对< i,j >是图Graph中的边的话,就令Graph[ i ][ j ]=1,否则等于0。
用邻接矩阵表示法表示出来的矩阵图,有以下特点:
1、矩阵上的对角线全都是0。因为图中的顶点对不会出现自己连自己的情况,所以对于矩阵图中Graph[ n ][ n ]一定是等于0。
2、邻接矩阵表示法表示出来的矩阵无向图,一定是沿对角线对称的,也就是说一定是一个对称矩阵。这是因为,假设图中结点1和2是有一条边的,所以2和1之间也是有一条边的,所以Graph[ 1 ][ 2 ]和Graph[ 2 ][ 1 ]一定是相等都等于1的。
用矩阵的方式,好处是容易检查某条边(顶点对)是否存在,容易找出一个顶点的邻接点,方便计算顶点的度。不过矩阵表示的不好在于,对于稀疏图来说,邻接矩阵的方式会浪费空间,因为稀疏图即边很少,也就是二维数组里很多元素都是0。矩阵在稀疏图的情况下浪费空间也浪费时间,比如我想统计稀疏图里一共有多少条边,把二维数组遍历一遍,统计元素1有多少个,问题是1很少,所以造成很多对0的遍历,是浪费时间的。
那么如何解决邻接矩阵浪费空间和时间的问题?我们知道线性表,线性表顺序存储需要实现分配好内存,这样有可能造成空间浪费或空间不够用,于是就有了链表的方法,同样图的表示方法也可以用链表的方式,就是邻接表表示法。
邻接表表示法,即用一个链表的集合,一个一维指针数组,依然用数组下标表示顶点。所以数组的大小就是图中顶点的个数。
#include <stdio.h>
#include <stdlib.h>
#define MaxVertex 100 /*最大顶点数*/
typedef int Vertex; /*顶点的下标表示顶点*/
typedef int WeightType; /*边的权值类型为整型*/
typedef char DataType; /*顶点存储的数据的类型*/
/*边的数据结构*/
typedef struct EdgeNode *PtrlEdgeNode;
struct EdgeNode {
Vertex V1, V2; /* 向边<V1, V2>*/
WeightType weight; /*权重*/
};
typedef PtrlEdgeNode Edge;
/*邻接点的数据结构*/
typedef struct PointNode *PtrlPointNode;
struct PointNode {
Vertex index; /*邻接点的下标*/
WeightType weight; /*边的权重*/
PtrlPointNode Next; /*指向下一个邻接点的指针*/
};
/*顶点表头结点的数据结构*/
typedef struct HeadNode {
PtrlPointNode HeadEdge; /*边的表头结点指针*/
DataType Data; /*顶点的数据*/
} PointList[MaxVertex]; /*邻接表类型*/
/*图结点的数据结构*/
typedef struct GraphNode *PtrlGraphNode;
struct GraphNode {
int numv; /*顶点数*/
int nume; /*边数*/
PointList PL; /*邻接表*/
};
typedef PtrlGraphNode ListGraphNode; /*邻接表方式存储图*/
第26到29行,顶点的表头就是一个结构体指针数组。指针类型是第18到22行的邻接点的数据结构。
/*创建一个有Vertexnum个顶点但没有边的图*/
ListGraphNode CreatGraph(int Vertexnum)
{
Vertex i;
ListGraphNode MyGraph;
MyGraph=(ListGraphNode)malloc(sizeof(struct GraphNode));
MyGraph->nume=0;
MyGraph->numv=Vertexnum;
for (i=0;i<MyGraph->numv;i++) {
MyGraph->PL[i].HeadEdge=NULL; //初始化数组里各个头结点
}
return MyGraph;
}
/*插入边*/
void InsertEdge(ListGraphNode MyGraph, Edge E)
{
PtrlPointNode Node; /*邻接点*/
/*插入边<V1, V2>*/
/*为 V2建立新的邻接点*/
Node=(PtrlPointNode)malloc(sizeof(struct PointNode));
Node->index=E->V2; /*将V2插入V1的表头*/
Node->weight=E->weight;
Node->Next=MyGraph->PL[E->V1].HeadEdge;
MyGraph->PL[E->V1].HeadEdge=Node;
/*如果是无向图,还要插入边<V2, V1>*/
Node=(PtrlPointNode)malloc(sizeof(struct PointNode));
Node->index=E->V1; /*将V2插入V1的表头*/
Node->weight=E->weight;
Node->Next=MyGraph->PL[E->V2].HeadEdge;
MyGraph->PL[E->V2].HeadEdge=Node;
}
/*创建图*/
ListGraphNode BuildGraph()
{
ListGraphNode MyGraph;
Edge E;
Vertex i,j,numv;
printf("请输入要读入的顶点个数:");
scanf("%d", &numv);
MyGraph=CreatGraph(numv);
printf("请输入要读入的边数:");
scanf("%d", &MyGraph->nume);
if (MyGraph->nume!=0) {
E=(Edge)malloc(sizeof(struct EdgeNode)); /*建立边结点*/
/*读入边:格式为"起点 终点 权重"*/
for (i=0;i<MyGraph->numv;i++) {
scanf("%d %d %d", &E->V1, &E->V2, &E->weight);
/*插入边到图里*/
InsertEdge(MyGraph, E);
}
}
/*如果顶点有数据的话,读入数据*/
for (i=0;i<MyGraph->numv;i++) {
scanf("%c", &MyGraph->PL[i].Data);
}
return MyGraph;
}
但是对于邻接表表示法来说,一条边必定是被存了两次的,例如顶点对< 5, 9 >,在指针数组5的链表里必定有9,在指针数组9的链表里也必定有5。所以用邻接表的话,表示的图要是稀疏图才算省空间,越稀疏越好。而对于完全图来说,用邻接矩阵的方式更方便后面的操作。