1.定义
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V, E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)。
线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
无向边:若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶数对(vi, vj)来表示。
无向图:如果图中任意两个顶点之间的边都是无向边,则称该图为无向图(Undirected graphs)。
有向边:若从顶点vi到vj的边有方向,则称这条边为有向边,也称弧(Arc)。用有序偶
有向图:如果图中任意两个顶点之间的边都是有向边,则称该图为有向图(Directed graphs)。
简单图:在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。
有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。
含有n个顶点的有向完全图有n*(n-1)条边,
稀疏图:有很少条边或弧的图称为稀疏图。
稠密图:有很多条边或弧的图称为稠密图。
权:有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight)。
网(Network):这种带权的图通常称为网(Network)。
图的顶点与边间关系
对于无向图G=(V, {E}),如果边(v, v’)∈E,则顶点v和v’互为邻接点(Adjacent),即v和v’相邻接。
边(v, v’)依附(incident)于顶点v和v’,或者说(v, v’)与顶点v和v’相关联。
顶点v的度(Degree)是和v相关联的边的数目,记为TD(v)。
对于有向图G=(V, {E}),如果弧
弧
无向图G=(V, {E})中从顶点v到顶点v’的路径(Path)是一个顶点序列(v=vi,0, vi,1, ……, vi,m=v’),其中(vi,j-1, vi,j)∈E,1<=j<=m。
路径的长度是路径上的边或弧的数目。
第一个顶点到最后一个顶点相同的路径称为回路或环(Cycle)。序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
连通图:在无向图G中,如果从顶点v到顶点v’有路径,则称v和v’是连通的。如果对于图中任意两个顶点vi、vj∈V,vi和vj都是连通的,则称G是连通图(Connected Graph)。
无向图中的极大连通子图称为连通分量。
要是子图;
子图要是连通的;
连通子图含有极大顶点数;
具有极大顶点数的连通子图包含依附于这些顶点的所有边。
在有向图G中,如果对于每一对vi、vj∈V、vi!=vj,从vi到vj和从vj到vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。
生成树:所谓连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1条边。不过有n-1条边并不一定是生成树。
如果一个有向图恰有一个顶点的入度为0,其余顶点的入度均为1,则是一棵有向树。
一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。
2.图的抽象数据类型
1.CreateGraph(*G, V, VR):按照顶点集V和边弧集VR的定义构造图G。
2.DestroyGraph(*G):图G存在则销毁。
3.LocateVex(G, u):若图G中存在顶点u,则返回图中的位置。
4.GetVex(G, v):返回图G中顶点v的值。
5.PutVex(G, v, value):将图G中顶点v赋值value。
6.FirstAdjVex(G, *v):返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点返回空。
7.NextAdjVex(G, v, *w):返回顶点v相对于顶点w的下一个邻接顶点,若w是v的最后一个邻接点则返回“空”。
8.InsertVex(*G, v, w):在图G中增添新顶点v。
9.DeleteVex(*G, v):删除图G中顶点v及其相关的弧。
10.InsertArc(*G, v, w):在图G中增添弧
11.DeleteArc(*G, v, w):在图G中删除弧
12.DFSTraverse(G):对图G中进行深度优先遍历,在遍历过程中对每个顶点调用。
13.HFSTraverse(G):对图G中进行广度优先遍历,在遍历过程中对每个顶点调用。
3.图的存储结构
1.邻接矩阵
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
下面是邻接矩阵的实现代码
typedef char VertexType; //顶点类型应由用户定义
typedef int EdgeType; //边上的权值类型应由用户定义
#define MAXVEX 100 //最大顶点数,应由用户定义
#define INFINITY 66535 //用65535来代表无穷大
typedef struct{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可看作表
int numVertexes, numEdges; //图中当前的顶点数和边数
}MGraph;
下面是无向网图的邻接矩阵实现代码
//建立无向网图的邻接矩阵表示
void CreateMGraph(MGraph *G){
int i, j, w;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G -> numVertexes, &G -> numEdges); //输入顶点数和边数
for(i=0; i<G->numVertexes; i++) //读入顶点信息,建立顶点表
scanf(&G->vexs[i]);
for(i=0; i<G->numVertexes; i++)
for(j=0; j<G->numVertexes; j++)
G->arc[i][j] = INFINITY; //邻接矩阵初始化
for(int k=0; k<G->numEdges; k++){
//读入numEdges条边,建立邻接矩阵
printf("请输入边(vi,vj)上的下标i,下标j和权w:\n"); //输入边(vi,vj)上的下标i,下标j,和权w
scanf("%d,%d,%d", &i, &j, &w);
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; //图是无向图,矩阵对称
}
}
2.邻接表
我们把这种数组与链表相结合的存储方法称为邻接表(Adjacency List)。
下面是邻接表的实现代码
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;
下面是邻接表的创建代码
void CreatALGraph(GraphAdjList *G){
int i, j, k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
scanf("%d,%d", &G->numVertexes, &G->numEdges); //输入顶点数和边数
for(i=0; i<G->numVertexes; i++){
//输入顶点信息,建立顶点表
scanf(&G->adjList[i].data); //输入顶点信息
G->adjList[i].firstedge = NULL; //将边表置为空表
}
for(k=0; k<G->numEdges; k++){
//建立边表
printf("输入边(vi, vj)上的顶点序号:\n");
scanf("%d,%d", &i, &j); //输入边(vi,vj)上的顶点序号
e = (EdgeNode *)malloc(sizeof(EdgeNode)); //向内存申请空间,申请边表结点
e->adjvex = j; //邻接序号为j
G->next = G->adjList[i].firstedge; //将e指针指向当前顶点指向的结点
G->adjList[i].firstedge = e; //将当前顶点的指针指向e
e = (EdgeNode *)malloc(sizeof(EdgeNode)); //向内存申请空间,生成边表结点
e->adjvex = i; //邻接序号为i
e->next = G->AdjList[j].firstedge; //将e指针指向当前顶点指向的结点
G->AdjList[j].firstedge = e; //将当前顶点的指针指向e
}
}
3.十字链表
十字链表的好处就是因为把邻接表和逆邻接表整合在了一起,这样既容易找到以vi为尾的弧,也容易找到以vi为头的弧,因而容易求得顶点的出度和入度。而且它除了结构复杂一点外其实创建图算法的时间复杂度是和邻接表相同的,因此,在有向图的应用中,十字链表是非常好的数据结构模型。
4.临接多重表
5.边集数组
边集数组是由两个一维数组构成。一个是存储顶点的信息;另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成。
4.图的遍历
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫图的遍历(Traversing Graph)。
(1)深度优先遍历
深度优先遍历(Depth_First_Search),也有称为深度优先搜索,简称DFS。
深度优先遍历其实就是一个递归的过程,就像是一棵树的前序遍历。它从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发,深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到。事实上,我们讲到的是连通图,对于非连通图,只需要对它的连通分量分别进行深度优先遍历,即在先前一个顶点进行一次深度优先遍历后,若图中尚有顶点未被访问,则另一个选图中一个未曾被访问过的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。
下面是邻接矩阵的深度优先遍历代码
typedef int Boolean; //Boolean是布尔类型,其值是TRUE或FALSE
Boolean 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]) //对未访问过的顶点调用FPS,若是连通图,只会执行一次
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,若是连通图,只会执行一次
DFS(GL, i);
}
(2)广度优先遍历
广度优先遍历(Breadth_First_Search),又称广度优先搜索,简称BFS。
如果说图的深度优先遍历类似树的前序遍历,那么图的广度优先遍历就类似于树的层序遍历了。
下面是邻接矩阵的广度优先遍历代码
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); //将队中元素出队列,赋值给i
for(j=0; j<G.numVertexes; j++){
if(G.arc[i][j] == 1 && !visited[j]){
//判断其他顶点若与当前顶点存在边且未访问过
visited[j] = TRUE; //将找到的此顶点标记为已访问
printf("%c ", G.vex[j]); //打印顶点
EnQueue(&Q, j); //将找到的此顶点入队列
}
}
}
}
}
}
下面是邻接表的广度优先遍历代码
void BFSTraverse(GraphAdjList GL){
int i;
EdgeNode *p;
Queue Q;
for(i=0; i<GL->numVextexes; i++)
visited[i] = FALSE;
InitQueue(&Q);
for(i=0; i<GL->numVextexes; i++){
if(!visited[i]){
visited[i] = TRUE;
printf("%c", GL->adjList[i].data); //打印顶点,也可以其他操作
EnQueue(&Q, i);
while(!QueueEmpty(Q)){
DeQueue(&Q, &i);
p = GL->adjustList[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; //指针指向下一个邻接点
}
}
}
}
}