目录
图的定义
图(Graph)是由顶点的有穷非空集合V(G)和顶点之间边的集合E(G)组成,通常表示为:G=(V,E)其中,G表示个图,V是图G中顶点的集合,E是图G中边的集合。若V={v1,v2,...,vn},则用∣V∣表示图G中顶点的个数,也称图G的阶,E={(u,v)∣u∈V,v∈V},用∣E∣表示图G中边的条数。E(G)可以为空集。若E(G)为空,则图G只有顶点而没有边。
对于图G,若边集E(G)为有向边的集合,则称该图为有向图;若边集E(G)为无向边的集合,则称该图为无向图。
在有向图中,顶点对<x,y>是有序的,它称为从顶点x到顶点y的一条有向边。 因此,<x,y>与<y,x>是不同的两条边。顶点对用尖括号括起来,对<x,y>而言,x是有向边的始点,y是有向边的终点。<xy>也称作一条弧, 则x为弧头,y为弧尾。
在无向图中,顶点对(x, y)是无序的,它称为与顶点x和顶点y相关联的一条边。这条边没有特定的方向,(x, y)与(y, x)是同一条边。 为了有别于有向图, 无向图的顶点对用一对圆括号括起来。
图的基本术语
用n表示图中顶点数目,用e表示边的数目,下面介绍图结构中的一些基本术语。
(1)子图:假设有两个图G=(v,E)和G'=(v',E'),如果v'是v的真子集且E'是E的真子集,则称G'为G的子图。下图所示为G1和G2的子图示例。
(2)无向完全图和有向完全图:对于无向图,若具有n(n-1)/2条边, 则称为无向完全图。对于有向图,若具有n(n- 1)条弧,则称为有向完全图。
(3)稀疏图和稠密图:有很少条边或弧(如logn)的图称为稀疏图,反之称为稠密图。
(4)权和网:在实际应用中,每条边可以标上具有某种含义的数值,该数值称为该边上的权值。这些权值可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网。
(5)邻接点:对于无向图G,如果图的边(v,v')属于E,则称顶点v和v'互为邻接点,即v和v'相邻接。边(v,v')依附于顶点v和v',或者说边(v,v')与顶点v和v'相关联。
(6)度、入度和出度:顶点v的度是指和v相关联的边的数目,记为TD(v)。对于有向图,顶点v的度分为入度和出度。入度是以顶点v为头的弧的数目,记为ID(v);出度是以顶点v为尾的弧的数目,记为OD(v)。顶点v的度为TD(v) = ID(v) + OD(v)。一般地, 如果顶点vi的度记为TD(vi),那么一个有n个顶点,e条边的图,满足如下关系:
(7)路径和路径长度:在无向图G中,从顶点v到顶点v'的路径是一个顶点序列
(v = v(i,0),v(i,1),…,v(i,m) = v'),其中(v(i,j-1),v(i,j))属于E,1<=j<=m。如果G是有向图,则路径也是有向的,顶点序列应满足<v(i,j-1),v(i,j)>属于E,1<=j<=m。路径长度是一条路径上经过的边或弧的数目。
(8)回路或环:第一个顶点和最后一个顶点相同的路径称为回路或环。
(9)简单路径、简单回路或简单环:序列中顶点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路,称为简单回路或简单环。
(10)连通、连通图和连通分量:在无向图G中,如果从顶点v到顶点v'有路径,则称v和v'是连通的。如果对于图中任意两个顶点vi,vj属于V, vi和vj都是连通的,则称G是连通图。连通分量,指的是无向图中的极大连通子图。图G1就是一个连通图,而G3则是非连通图,但G3有3个连通分量。
(11)强连通图和强连通分量:在有向图G中,如果对于每一对vi,vj属于V,vi≠vj,从vi到vj和从vi到vj都存在路径,则称G是强连通图。有向图中的极大强连通子图称作有向图的强连通分量。
G1不是强连通图,但它有两个强连通分量,下图为G1的两个强连通分量。
(12)连通图的生成树:一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边,这样的连通子图称为连通图的生成树。如果在一棵生成树上添加一条边, 必定构成一个环,因为这条边使得它依附的那两个顶点之间有了第二条路径。
一棵有n个顶点的生成树有且仅有n-1条边。如果一个图有n个顶点和小于n-1条边,则是非连通图。如果它多于n-1条边,则一定有环。但是,有n-1条边的图不一定是生成树。
(13)有向树和生成森林:有一个顶点的入度为0,其余顶点的入度均为1的有向图称为有向树。一个有向图的生成森林是由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。
图的存储结构
邻接矩阵表示法
邻接矩阵是表示顶点之间相邻关系的矩阵。设G(V,E)是具有n个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵:
下图为G1和G2的邻接矩阵:
若G是网,则邻接矩阵可以定义为:
w0表示边上的权值; ∞表示计算机允许的,大于所有边上权值的数。
下图为一个有向网和它的邻接矩阵。
用邻接矩阵表示法表示图,除了一个用于存储邻接矩阵的二维数组外,还需要用一个一维数组来存储顶点信息。
邻接矩阵表示法创建无向图
#include <stdio.h>
#define OK 1
#define ERROR 0
#define INFEASIBLE -1
#define OVERFLOW -2
typedef int Status;
#define MaxInt 32767 //表示极大值,即∞
#define MVNum 100 //最大顶点数
typedef int VerTexType;//假设顶点的数据类型为整型
typedef int ArcType; //假设边的权值类型为整型
typedef struct
{
VerTexType vexs[MVNum]; //顶点表
ArcType arcs[MVNum][MVNum]; //邻接矩阵
int vexnum,arcnum; //图的当前点数和边数
}AMGraph;
Status LocateVex(AMGraph *G,VerTexType v) //查询顶点v在图G中的下标位置
{
for(int i=0;i<G->vexnum;i++){
if(G->vexs[i]==v){
return i;
}
}
}
//此处定义无向图的创建
Status CreateUDG(AMGraph *G){
scanf("%d",&G->vexnum);
scanf("%d",&G->arcnum);
for(int i=0;i<G->vexnum;i++){
scanf("%d",&G->vexs[i]);//输入点的信息
}
int v1,v2;
int m,n;
for(int j=0;j<G->arcnum;j++){
scanf("%d",&v1);
scanf("%d",&v2);
int m=LocateVex(G,v1);
int n=LocateVex(G,v2);
G->arcs[m][n]=1;
G->arcs[n][m]=1;
}
return OK;
}
//此处定义无向图的邻接矩阵的输出
Status PrintAMGraph(AMGraph *G){
for(int i=0;i<G->vexnum;i++){
for(int j=0;j<G->vexnum;j++){
printf("%d ",G->arcs[i][j]);
}
printf("\n");
}
return OK;
}
int main()
{
AMGraph G;
//调用利用邻接矩阵创建无向图的函数CreateUDG
CreateUDG(&G);
//调用输出邻接矩阵的函数PrintAMGraph
PrintAMGraph(&G);
return 0;
}
邻接矩阵表示法的优缺点
(1)优点
①便于判断两个顶点之间是否有边,即根据4[i][j]=0或1来判断。
②便于计算各个顶点的度。对于无向图,邻接矩阵第i行元素之和就是顶点vi的度;对于有向图,第i行元素之和就是顶点vi的出度,第i列元素之和就是顶点i的入度。
(2)缺点
①不便于增加和删除顶点。
②不便于统计边的数目,需要查找邻接矩阵所有元素才能统计完毕,时间复杂度为O(n^2)。
③空间复杂度高。如果是有向图,n个顶点需要n^2个单元存储边。如果是无向图,因其邻接矩阵是对称的,所以对规模较大的邻接矩阵可以采用压缩存储的方法,仅存储下三角(或上三角)的元素,这样需要n(n- 1)/2个单元即可。但无论以何种方式存储,邻接矩阵表示法的空间复杂度均为O(n^2),这对于稀疏图而言尤其浪费空间。
邻接表表示法
邻接表(Adjacency List)是图的一种链式存储结构。在邻接表中,对图中每个顶点vi建立一个单链表,把与vi相邻接的顶点放在这个链表中。邻接表中每个单链表的第一个节点存放有关顶点的信息,把这一节点看成链表的表头,其余节点存放有关边的信息,这样邻接表便由两部分组成:表头节点表和边表。
(1)表头节点表:由所有表头节点以顺序结构的形式存储,以便可以随机访问任一顶点的边链表。表头节点包括数据域(data)和链域(firstarc)两部分。数据域用于存储顶点vi的名称或其他有关信息;链域用于指向链表中第一个节点(与顶点vi邻接的第一个邻接点)。
(2)边表:由表示图中顶点间关系的2n个边链表组成。边链表中边节点包括邻接点域(adjvex)、数据域(info)和链域(nextarc) 3个部分。邻接点域指示与顶点vi邻接的点在图中的位置;数据域存储和边相关的信息,如权值等;链域指示与顶点vi邻接的下一条边的节点。
邻接表表示法创建无向图
#include<stdio.h>
#include<stack>
#include <iostream>
#define MAXSIZE 100
#define MaxInt 32767 //表示最大值,即正无穷大
#define MVNum 100 //定义最大顶点数
using namespace std;
typedef char VerTexType; //假设顶点数据类型为字符型
typedef int ArcType; //假设边的权值为整型
//定义边结点
typedef struct ArcNode{
int adjvex;//该边所指向的顶点的位置
struct ArcNode *nextarc; //下一条边的指针
}ArcNode;
//定义顶点结点信息
typedef struct VNode{
VerTexType data;
ArcNode *firstarc; //指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNum];
//定义邻接表的结构
typedef struct {
AdjList vertices;
int vexnum, arcnum; //当前图的点数和边数
}ALGraph;
//确定顶点vex在G.vertices中的序号
int LocateVex(ALGraph &G, VerTexType vex){
for(int i=0;i<G.vexnum;i++){
if(G.vertices[i].data==vex){
return i;
}
}
}
//邻接矩阵法构造无向图
void CreateUGD(ALGraph &G){
cin>>G.vexnum;
cin>>G.arcnum;
//输入各点,构造表头结点表
for(int i=0;i<G.vexnum;i++){
cin>>G.vertices[i].data;
G.vertices[i].firstarc=NULL;
}
//输入各边,构造邻接表
for(int k=0;k<G.arcnum;k++){
VerTexType v1;
cin>>v1;
VerTexType v2;
cin>>v2;
//确定v1,v2在图G中的位置,即在G.vertices中的序号
int i= LocateVex(G, v1);
int j= LocateVex(G, v2);
struct ArcNode *p1, *p2;
p1=new ArcNode; //生成一个新的边结点
p1->adjvex=j;
p1->nextarc=G.vertices[i].firstarc;
G.vertices[i].firstarc=p1;
p2=new ArcNode; //生成另一个对称边结点p2
p2->adjvex=i;
p2->nextarc=G.vertices[j].firstarc;
G.vertices[j].firstarc=p2;
}
}
//遍历图的邻接表
void PrintfG(ALGraph &G){
printf("遍历图的邻接表:\n");
for(int i=0;i<G.vexnum;i++){
printf("%c",G.vertices[i].data);
ArcNode *p;
p=G.vertices[i].firstarc;
while(p){
printf("->");
printf("%d", p->adjvex);
p=p->nextarc;
}
printf("^");
printf("\n");
}
}
int main(){
ALGraph G;
CreateUGD(G);
PrintfG(G);
}
邻接表表示法的优缺点
(1)优点
①便于增加和删除顶点。
②便于统计边的数目,按顶点表顺序查找所有边表可得到边的数目,时间复杂度为O(n+e):
③空间效率高。对于一个具有n个顶点、e条边的图G,若G是无向图,则在其邻接表表示中有n个顶点表节点和2e个边表节点;若G是有向图,则在它的邻接表表示或逆邻接表表示中均有n个顶点表节点和e个边表节点。因此,邻接表或逆邻接表表示的空间复杂度为O(n+ e),适合表示稀疏图。对于稠密图,考虑到邻接表中要附加链域,因此常采取邻接矩阵表示法。
(2)缺点
①不便于判断顶点之间是否有边,要判定vi和vj之间是否有边,就需查找第i个边表,最坏情况下时间复杂度为O(n)。
②不便于计算有向图各个顶点的度。对于无向图,在邻接表表示中顶点vi的度是第i个边表中的节点个数。在有向图的邻接表中,第i个边表上的节点个数是顶点vi的出度,但求vi的入度较困难,需遍历各顶点的边表。若有向图采用逆邻接表表示,则与邻接表表示相反,求顶点的入度较容易,而求顶点的出度较困难。