图是一种较线性表和树更加复杂的数据结构。在图形结构中,结点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关
一、图的定义
图是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中的顶点的集合,E是图G中边的集合
- 线性表中数据元素成为元素,树中称为结点,在图中称为顶点
- 在定义中,若V是顶点的集合,则强调来了顶点集合V有穷非空
- 在图中,任意两个顶点都可能有关系,顶点之间的逻辑关系用边来表示
1、无向边
若顶点Vi到Vj之间的边没有方向,则称这条边为无向边,用无序偶对(Vi,Vj)来表示
2、有向边
若从顶点Vi到Vj的边有方向,则称这条边为有向边,也称为弧,用有序偶<Vi,Vj>来表示,Vi称为弧伟,Vj称为弧头
连接顶点A到D的有向边就是弧,A是弧尾,D是弧头,<A,D>表示弧,不能写成<D,A>
3、简单图
若不存在顶点到其自身的边,且同一条边不重复出现
4、无向完全图
在无向图种,如果任意两个顶点之间都存在边,则称改图为无向完全图,n个顶点有n*(n-1)/2条边
5、有向完全图
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称改图为有向完全图。n个顶点有n*(n-1)条边
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权 (Weight),这些权可以表示从一个顶点到另一个顶点的距离或耗费。
这种带权的图通 常称为网(Network)。
假设有两个图G= (V,{E})和G’ = (V’,{E’}),如果V’∈V且E’∈E,则称G’为G的子图
例如下面带底纹的图均为左侧无向图与有向图的子图
(2)图的顶点与边间关系
(3)连通图相关术语
在无向图G中,如果从顶点V到顶点V’有路径,则称V和V’是连通的。
如果对于图中任意两个顶点Vi,Vj∈V,Vi和Vj都是连通的,则称G是连通图
无向图中的极大连通子图称为连通分量
- 要是子图
- 子图要是连通的
- 连通子图含有极大顶点数
- 具有极大顶点数的连通子图包含依附于这些顶点的所有边
二、图的抽象数据类型
ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合。
Operation
CreateGraph (*G,V,VR):按照顶点集V和边弧集VR的定义构造图G。
DestroyGraph ( *G):图 G 存在则销毁。
LocateVex(G,u):若图G中存在顶点u,则返回图中的位亶。
GetVex(G,v):返回图G中顶点v的值。
PutVex(G,v, value):将图 G 中顶点 v 赋值 value。
FirstAdjVex(G,*v):返回顶点v的一个邻接顶点,若顶点在G中无邻接顶点返回空。
NextAdjVex (G, v, *w) : 返回顶点v相对于顶点w的下一个邻接顶点,若w是v的最后 一个邻接点则返回"空"。
InsertVex ( *G,v):在图G中増添新顶点V。
DeleteVex (*G,v):删除图G中顶点v及其相关的弧。
InsertArc ( *G, v, w):在图G中増添孤<v,w>,若G是无向图,还需要增添对称弧
DeleteArc (*G,v,w):在图G中删除弧<v,w>,若G是无向图,则还删除对称弧
DFSTraverse ( G ):对图G中进行深度优先遍历,在遍历过程对每个顶点调用。
HFSTraverse (G):对图G中进行广度优先遍历,在遍历过程对每个顶点调用。 endADT
三、图的存储结构
3.1、邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息
对称矩阵:n阶矩阵的元满足aij=aji(0<=i,j<=n),即从矩阵的左上角到右下角的主对角线为轴,右上角的元与左下角对应的元全都是相等的
有了这个矩阵,我们就可以很容易地知道图中的信息
- 我们要判定任意两顶点是否有边无边就非常容易了
- 我们要只读某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和。比如顶点V1的度就是1+0+1+0=2
- 求顶点Vi的所有邻接点就是将矩阵中第i行元素扫描一遍,
arc[i][j]
为1就是邻接点
顶点数组为
vertex[4]={ v0, v1, v2, v3}
,孤数组arc[4][4]为图7-4-3右图这样的一个 矩阵。主对角线上数值依然为0。但因为是有向图,所以此矩阵并不对称,比如由Vi 到Vo有弧,得到arc[1][0]=1,而v。V0到V1没有弧,因此arc[0][l]=1。
有向图讲究入度与出度,顶点V1的入度为1,正好是第V1列各数之和。顶点VI 的出度为2,即第V1行的各数之和。
与无向图同样的办法,判断顶点Vi到Vj是否存在弧,只需要査找矩阵中arc[i][j] 是否为1即可。要求用的所有邻接点就是将矩阵第i行元素扫描一遍,査找arc[i][j] 为1的顶点。
在图的术语中,我们提到了网的概念,也就是每条边上带有权的图叫做网。那么 这些权值就需要存下来,如何处理这个矩阵来适应这个需求呢?我们有办法。
这里Wij表示(Vi,Vj)或<Vi,Vj>上的权值。∞表示一个计算机允许的、大于所有边 上权值的值,也就是一个不可能的极限值。
如图7-4-4左图就是一个有向网图,右图就是它 的邻接矩阵。
#define MAXVEX 100 /* 最大顶点数,应由用户定义 */
#define GRAPH_INFINITY 65535 /* 用65535来代表∞ */
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef char VertexType; /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */
typedef struct
{
VertexType vexs[MAXVEX]; /* 顶点表 */
EdgeType arc[MAXVEX][MAXVEX];/* 邻接矩阵,可看作边表 */
int numNodes, numEdges; /* 图中当前的顶点数和边数 */
}MGraph;
有了这个结构定义,我们构造一个图,其实就是给顶点表和边表输入数据的过程。
我们来看看无向网图的邻接矩阵的创建代码
/* 建立无向网图的邻接矩阵表示 */
void CreateMGraph(MGraph *G)
{
int i,j,k,w;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
for(i = 0;i <G->numNodes;i++) /* 读入顶点信息,建立顶点表 */
scanf(&G->vexs[i]);
for(i = 0;i <G->numNodes;i++)
for(j = 0;j <G->numNodes;j++)
G->arc[i][j]=GRAPH_INFINITY; /* 邻接矩阵初始化 */
for(k = 0;k <G->numEdges;k++) /* 读入numEdges条边,建立邻接矩阵 */
{
printf("输入边(vi,vj)上的下标i,下标j和权w:\n");
scanf("%d,%d,%d",&i,&j,&w); /* 输入边(vi,vj)上的权w */
G->arc[i][j]=w;
G->arc[j][i]= G->arc[i][j]; /* 因为是无向图,矩阵对称 */
}
}
从代码中也可以得到,n个顶点和e条边的无向网图的创建,时间复杂度为 0(n+n²+e),其中对邻接矩阵Garc的初始化耗费了 O(n²)的时间。
3.2、邻接表
邻接表的处理办法:
- 图中顶点用一个一维数组存储,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息
- 图中每个顶点Vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点Vi的边表,有向图则称为顶点Vi作为弧尾的出边表
顶点表的各个顶点由data和firstedge两个域表示,data是数据域,存储顶点的信息,firstedge是指针域,指向边表的第一个结点,即此顶点的第一个邻接点
边表结点由adjvex和next两个域组成。adjvex是邻接点域,存储某顶点的邻接点在顶点表中的下标,next则存储指向边表中下一个结点的指针
比如V1顶点与V0,V2互为邻接点,则在V1的边表中,adjvex分别为V0的0和V2的2
获取某个顶点的度
去查找这个顶点的边表中结点的个数
判断顶点Vi到Vj是否存在边
只需要测试顶点Vi的边表中adjvex是否存在结点Vj的下标j就可以了
求顶点的所有邻接点
就是对此顶点的边表进行遍历,得到的adjvex域对应的顶点就是邻接点
若是有向图,邻接表结构是类似的,但要注意的是有向图由于有方向,我们是以顶点为弧尾来存储边表的,这样很容易就可以得到每个顶点的出度
有时为了便于确定顶点的入度或以顶点为弧头的弧,我们可以建立一个有向图的逆邻接表,即对每个顶点Vi都建立一个链接为Vi为弧头的表
此时我们很容易就可以算出某个顶点的入度或出度是多少
判断两顶点是否存在弧也很容易实现
对于带权值的网图,可以在边表结点定义中再增加一个weight的数据域,存储权值信息即可
结点定义的代码
typedef char VertexType; /* 顶点类型应由用户定义 */
typedef int EdgeType; /* 边上的权值类型应由用户定义 */
typedef struct EdgeNode /* 边表结点 */
{
int adjvex; /* 邻接点域,存储该顶点对应的下标 */
EdgeType info; /* 用于存储权值,对于非网图可以不需要 */
struct EdgeNode *next; /* 链域,指向下一个邻接点 */
}EdgeNode;
typedef struct VertexNode /* 顶点表结点 */
{
VertexType data; /* 顶点域,存储顶点信息 */
EdgeNode *firstedge;/* 边表头指针 */
}VertexNode, AdjList[MAXVEX];
typedef struct
{
AdjList adjList;
int numNodes,numEdges; /* 图中当前顶点数和边数 */
}GraphAdjList;
无向图的邻接表的创建
/* 建立图的邻接表结构 */
void CreateALGraph(GraphAdjList *G)
{
int i,j,k;
EdgeNode *e;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numNodes,&G->numEdges); /* 输入顶点数和边数 */
for(i = 0;i < G->numNodes;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 */
e->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.3、十字链表
对于有向图来说,邻接表是有缺陷的。关心了出度问题,想了解入度就必须 要遍历整个图才能知道,反之,逆邻接表解决了入度却不了解出度的情况。有没有可 能把邻接表与逆邻接表结合起来呢?答案是肯定的,就是把它们整合在一起。
这就是 我们现在要讲的有向图的一种存储方法:十字链表(Orthogonal List)
重新定义顶点表结构
firstin表示入边表头指针,指向该顶点的入边表中的第一个结点,firstout表示出边表头指针,指向该顶点的出边表中的第一个结点
重新定义的边表结点结构
其中tailvex是指弧起点在顶点表的下标,headvex是指弧终点在顶点表中的下 标,headlink是指入边表指针域,指向终点相同的下一条边,taillink是指边表指针 域,指向起点相同的下一条边。如果是网,还可以再增加一个weight域来存储权值。
3.4、临界多重表
3.5、边集数组
四、图的遍历
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历
4.1、深度优先遍历
深度遍历其实就是一个递归的过程,更像一棵树的前序遍历。
它从图中某个顶点v出发,访问此顶点,然后从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到
邻接矩阵的方式
typedef int Boolean; /* Boolean是布尔类型,其值是TRUE或FALSE */
Boolean visited[MAXVEX]; /* 访问标志的数组 */
/* 邻接矩阵的深度优先递归算法 */
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]) /* 对未访问过的顶点调用DFS,若是连通图,只会执行一次 */
DFS(G, i);
}
如果图结构是邻接表结构,其DFSTraverse函数的代码是几乎相同的,只是在递归函数中因为数组换成了链表而有不同
Boolean visited[MAXSIZE]; /* 访问标志的数组 */
/* 邻接表的深度优先递归算法 */
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);
}
4.2、广度优先遍历
类似树的层级遍历
邻接矩阵结构的广度优先遍历算法
/* 邻接矩阵的广度遍历算法 */
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.vexs[j]); /* 打印顶点 */
EnQueue(&Q,j); /* 将找到的此顶点入队列 */
}
}
}
}
}
}
邻接表的广度优先遍历
/* 邻接表的广度遍历算法 */
void BFSTraverse(GraphAdjList GL)
{
int i;
EdgeNode *p;
Queue Q;
for(i = 0; i < GL->numVertexes; i++)
visited[i] = FALSE;
InitQueue(&Q);
for(i = 0; i < GL->numVertexes; i++)
{
if (!visited[i])
{
visited[i]=TRUE;
printf("%c ",GL->adjList[i].data);/* 打印顶点,也可以其它操作 */
EnQueue(&Q,i);
while(!QueueEmpty(Q))
{
DeQueue(&Q,&i);
p = GL->adjList[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; /* 指针指向下一个邻接点 */
}
}
}
}
}
深度优先更适合目标比较明确,以找到目标为主要目的的情况
广度优先更适合在不断扩大遍历范围时找到相对最优解的情况