图与图的遍历
本文参考自《大话数据结构》
定义
图(graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
注意
- 在图中数据元素,我们则称之为顶点(Vertex);
- 在图结构中,不允许没有顶点;
- 在图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的;
各种图定义
无向边 :若顶点vi到vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶对(vi,vj)
表示。如果图中任意两个顶点之间的边都是无向边,则称该图为无向图;
有向边 :若从顶点vi到vj的边有方向,则称这条边为有向边,也称为弧(Arc),用有序偶对<vi,vj>
来表示,vi称为弧尾(Tail),vj称为弧头(Head)。如果途中任意两个顶点之间的边都是有向边,则称该图为有向图。
无向边用()
表示,而有向边用<>
表示;
在图中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AFotjgxu-1605454325635)(pic/简单图定义.png)]
在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2
条边。
在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)
条边。
结论:对于具有n个顶点和e条边数的图,无向图0<=e<=n*(n-1)/2
,有向图0<=e<=n*(n-1)
;
有很少条边或弧的图称为稀疏图,反之称为稠密图(稀疏和稠密是相对而言的)。
有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数字叫做权(Weight)。这些权可以表示从一个顶点到另一个顶点的距离或耗费。这种带权的图通常称为网。
假设有两个图G=(V,{E})
和G'=(V',{E'})
,如果V'∈V
且E'∈E
,则称G’为G的子图
对于无向图,边数=各顶点度数和的一半
;
对于有向图,边数=各顶点出度和=各顶点入度和
;
树中根结点到任意结点的路径是唯一的,但是图中顶点和顶点之间的路径却不是唯一的。
路径的长度是路径上的边或弧的数目。
第一个顶点到最后一个顶点相同的路径称为回路或环。序列中定点不重复出现的路径称为简单路径。除了第一个顶点和最后一个顶点之外,其余顶点不重复出现的回路称为简单回路或简单环。
连通图相关术语
在无向图G中,如果从顶点v到顶点v’有路径,则称v和v’是连通的。如果对于图中任意两个顶点vi,vj∈E
,vi和vj都是连通的,则称G是连通图。
无向图中的极大连通子图称为连通分量。注意连通分量的概念,它强调:
- 要是子图;
- 子图要是连通的;
- 连通子图含有极大顶点数;
- 具有极大顶点数的连通子图包含依附于这些顶点的所有边。
在有向图G中,如果对于每一对vi,vj∈V,vi≠vj
,从vi到vj和vj到vi都存在路径,则称G是强连通图。有向图中的极大强连通子图称做有向图的强连通分量。
一个连通图的生成树是一个极小的连通子图,它含有图中全部的n个顶点,但只有足以构成一棵树的n-1
条边;
如果一个有向图恰有一个顶点入度为0,其余顶点的入度均为1,则是一棵有向树。一个有向图的生成森林由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。
抽象数据类型
ADT 图(Graph)
Data
顶点的有穷非空集合和边的集合
Operation
CreateGraph(*G, V, VR):按照顶点集V和边弧集VR的定义构造图G
DestoryGraph(*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是无向图,还需要增添对称弧<w,v>
DeleteArc(*G,v,w):在图G中,删除弧<v,w>,若G是无向图,则还删除对称弧<w,v>
DFSTraverse(G):对图G中进行深度优先遍历,在遍历过程对每个顶点调用
BFSTraverse(G):对图G中进行广度优先遍历,在遍历过程对每个顶点调用
endADT
存储结构
邻接矩阵
图的邻接矩阵存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
无向图邻接矩阵也是一个对称矩阵。
有了这个矩阵,我们就可以很容易地知道图中的信息:
- 我们要判定任意两顶点是否有边无边就非常容易了;
- 我们要知道某个顶点的度,其实就是这个顶点vi在邻接矩阵中第i行(或第i列)的元素之和;
- 求顶点vi的所有邻接点就是将矩阵中第i行元素扫描一遍,
arc[i][j]
为1就是邻接点;
typedef char VertexType; //顶点类型由用户定义
typedef int EdgeType; //边上的权值类型由用户定义
#define MAXVEX 100 //最大顶点数,由用户定义
#define INFINITY 65535 //代表无穷大
struct MGraph{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,可看作边表
int numVertexes, numEdges; //图中当前的顶点数和边数
};
/*建立无向网的邻接矩阵表示*/
void CreateMGraph(MGraph* G){
int i, j, k, w;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numVextexes, &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(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条边的无向网图的创建,时间复杂度为O(n+n^2+e)
,其中对邻接矩阵Garc的初始化耗费了O(n^2)
的时间。
邻接表
对于边数相对顶点数较少的图,用邻接矩阵存储边会造成很大的内存空间浪费,所以这里考虑另外一种存储结构:邻接表。
- 图中顶点用一个一维数组存储,当然,顶点也可以用单链表来存储,不过数组可以较容易地读取顶点信息,更加方便。另外,对于顶点数组中,每个数据元素还需要存储指向第一个邻接点的指针,以便于查找该顶点的边信息。
- 图中每个顶点vi的所有邻接点构成一个线性表,由于邻接点的个数不定,所以用单链表存储,无向图称为顶点vi的边表,有向图则称为顶点vi作为弧尾的出边表。
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 CreateALGraph(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);
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
}
}
算法的时间复杂度:对于n个顶点e条边来说,很容易得出是O(n+e)
。
十字链表,邻接多重表,边集数组的相关介绍本文没有介绍,可以查查网上其他资料。
图的遍历
定义
从图中某一顶点出发访遍图中其余顶点,且使每一个顶点仅被访问一次,这一过程就叫做图的遍历。
深度优先遍历
深度优先遍历,简称DFS。
typedef int Boolean;
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]) //对未访问过的结点调用DFS,如果是连通图,只会执行一次
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(GL, i);
}
对比两个不同数据结构的深度优先遍历算法,对于n个顶点e条边的图来说,邻接矩阵由于是二维数组,要查找每个顶点的邻接点需要访问矩阵中的所有元素,因此都需要O(n^2)
的时间。而邻接表作存储结构时,找邻接点所需的时间取决于顶点和边的数量,所以是O(n+e)
。显然对于点多边少的稀疏图来说,邻接表结构使得算法在时间效率上大大提高。
广度优先遍历
广度优先遍历,简称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.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)){ //若当前队列不为空
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; //指针指向下一个邻接点
}
}
}
}
广度和深度遍历算法没有优劣之分,不过如果图顶点和边非常多,不能在短时间内遍历完成,遍历的目的是为了寻找合适的顶点,那么选择哪种遍历就要仔细斟酌了。深度优先更适合目标比较明确,以找到目标为主要目的的情况,而广度优先更适合在不断扩大遍历范围时找到相对最优解的情况。
例子
邻接矩阵实现
#include <stdio.h>
#include <stdlib.h>
typedef char VertexType; //顶点类型
typedef int EdgeType; //边上的权值类型
#define MAXVEX 100 //最大顶点数
#define INFINITY 65535 //代表无穷大
bool visited[MAXVEX]; //标记结点是否遍历过
struct MGraph{
VertexType vexs[MAXVEX]; //顶点表
EdgeType arc[MAXVEX][MAXVEX]; //邻接矩阵,边表
int numVertexes, numEdges; //图中顶点数和边数
};
struct QueueNode{
int data; //数据域:指向图结点的索引
QueueNode* next; //下一个结点
};
struct Queue{
QueueNode* top; //头指针
QueueNode* real; //尾指针
int NodeNum; //队列结点数
};
//初始化队列
void InitQueue(Queue* Q){
Q->NodeNum = 0;
Q->top = Q->real = NULL;
}
//入队
void EnQueue(Queue* Q, int i){
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
newNode->data = i;
newNode->next = Q->top;
if(Q->top != NULL)
Q->top->next = newNode;
Q->top = newNode;
Q->NodeNum++;
if(Q->NodeNum == 1) //头尾指向同一个
Q->real = Q->top;
}
//出队
void DeQueue(Queue* Q, int* i){
QueueNode* deNode;
if(Q->NodeNum != 0){
*i = Q->real->data;
deNode = Q->real;
Q->real= deNode->next;
free(deNode);
Q->NodeNum--;
}
}
//判断队列是否为空
bool QueueEmpty(Queue Q){
if(Q.NodeNum == 0)
return 1;
return 0;
}
/* 建立无向图的邻接矩阵表示 */
void CreateMGraph(MGraph* G){
int i, j, k, w;
printf("输入顶点数和边数:\n");
scanf("%d,%d",&G->numVertexes, &G->numEdges);
printf("输入顶点值(字符):\n");
for(i=0;i<G->numVertexes;i++){//读取顶点信息,建立顶点表
getchar(); //吃掉回车
scanf("%c", &G->vexs[i]);
}
for(i=0;i<G->numVertexes;i++)
for(j=0;j<G->numVertexes;j++)
G->arc[i][j] = INFINITY; //邻接矩阵初始化
for(k=0;k<G->numEdges;k++){ //读取numEdges条边,建立邻接矩阵
printf("输入边(vi,vj)上的下标i,下标j和权w:\n");
scanf("%d,%d,%d",&i,&j,&w);
G->arc[i][j] = w;
G->arc[j][i] = G->arc[i][j]; //无向图的邻接矩阵关于主对角线对称
}
}
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);
}
/* 广度优先遍历 */
void BFSTraverse(MGraph* G){
int i, j, k;
Queue Q;
for(i=0;i<G->numVertexes;i++) //初始化遍历数组
visited[i] = false;
InitQueue(&Q); //初始化辅助用的队列
for(i=0;i<G->numVertexes;i++){
if(!visited[i]){
k = i;
visited[i] = true;
printf("%c",G->vexs[i]);
EnQueue(&Q, i);
while(!QueueEmpty(Q)){
DeQueue(&Q, &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);
}
}
}
i = k;
}
}
}
int main(){
MGraph* G = (MGraph*)malloc(sizeof(MGraph));
CreateMGraph(G);
printf("深度优先遍历:\n");
DFSTraverse(G);
printf("\n广度优先遍历:\n");
BFSTraverse(G);
return 0;
}
结果
邻接表实现
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#define MAXVEX 10
typedef char VertexType; //顶点类型
typedef int EdgeType; //边上的权值类型
bool visited[MAXVEX]; //标记访问的结点
struct QueueNode{
int data; //数据域:指向图结点的索引
QueueNode* next; //下一个结点
};
struct Queue{
QueueNode* top; //头指针
QueueNode* real; //尾指针
int NodeNum; //队列结点数
};
//初始化队列
void InitQueue(Queue* Q){
Q->NodeNum = 0;
Q->top = Q->real = NULL;
}
//入队
void EnQueue(Queue* Q, int i){
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
newNode->data = i;
newNode->next = Q->top;
if(Q->top != NULL)
Q->top->next = newNode;
Q->top = newNode;
Q->NodeNum++;
if(Q->NodeNum == 1) //头尾指向同一个
Q->real = Q->top;
}
//出队
void DeQueue(Queue* Q, int* i){
QueueNode* deNode;
if(Q->NodeNum != 0){
*i = Q->real->data;
deNode = Q->real;
Q->real = deNode->next;
free(deNode);
Q->NodeNum--;
if(Q->NodeNum == 0)
Q->top = NULL;
}
}
//判断队列是否为空
bool QueueEmpty(Queue Q){
if(Q.NodeNum == 0)
return 1;
return 0;
}
struct EdgeNode{
int adjvex; //邻接点域,存储下标
EdgeType weight; //权值
EdgeNode* next; //下一个邻接点
};
struct VertexNode{ //顶点表结点
VertexType data; //顶点域,存储顶点信息
EdgeNode* firstedge; //边表头指针
};
struct GraphAdjList{ //邻接表
VertexNode adjList[MAXVEX];
int numVertexes, numEdges; //图中当前顶点数和边数
};
/*无向图邻接表创建*/
void CreateALGraph(GraphAdjList* G){
int i, j, k;
EdgeNode* newNode;
printf("输入顶点和边数:\n");
scanf("%d,%d",&G->numVertexes, &G->numEdges);
printf("输入顶点值:\n");
for(i=0;i<G->numVertexes;i++){
getchar(); //吃掉回车
scanf("%c",&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);
//生成2个结点链接到头结点
newNode = (EdgeNode*)malloc(sizeof(EdgeNode));
newNode->adjvex = i;
newNode->next = G->adjList[j].firstedge;
G->adjList[j].firstedge = newNode;
newNode = (EdgeNode*)malloc(sizeof(EdgeNode));
newNode->adjvex = j;
newNode->next = G->adjList[i].firstedge;
G->adjList[i].firstedge = newNode;
}
}
/* 广度优先遍历 */
void BFSTraverse(GraphAdjList* G){
int i, k;
Queue Q;
EdgeNode* e;
InitQueue(&Q); //初始化队列
for(i=0;i<G->numVertexes;i++)
visited[i] = false;
for(i=0;i<G->numVertexes;i++){
k = i;
if(!visited[i]){
visited[i] = true;
printf("%c",G->adjList[i].data);
EnQueue(&Q, i);
while(!QueueEmpty(Q)){
DeQueue(&Q, &i);
e = G->adjList[i].firstedge; //拿到i结点的头指针
while(e){
if(!visited[e->adjvex]){
visited[e->adjvex] = true;
printf("%c",G->adjList[e->adjvex].data);
EnQueue(&Q, e->adjvex);
}
e = e->next;
}
}
}
i = k;
}
}
void DFS(GraphAdjList* G, int i){
EdgeNode* e;
visited[i] = true;
printf("%c",G->adjList[i].data);
e = G->adjList[i].firstedge; //拿到头指针
while(e){
if(!visited[e->adjvex])
DFS(G, e->adjvex);
e = e->next;
}
}
/* 深度优先遍历 */
void DFSTraverse(GraphAdjList* G){
int i;
for(i=0;i<G->numVertexes;i++)
visited[i] = false;
for(i=0;i<G->numVertexes;i++){
if(!visited[i]){
DFS(G, i);
}
}
}
/* 销毁图 */
void DestoryGraph(GraphAdjList* G){
int i;
EdgeNode* e;
EdgeNode* temp;
if(G){
for(i=0;i<G->numVertexes;i++){
e = G->adjList[i].firstedge; //拿到头指针
if(e){
temp = e;
e = temp->next;
free(temp);
}
}
free(G);
}
}
int main(){
GraphAdjList* G = (GraphAdjList*)malloc(sizeof(GraphAdjList));
CreateALGraph(G);
printf("\n深度优先遍历:");
DFSTraverse(G);
printf("\n广度优先遍历:");
BFSTraverse(G);
printf("\n");
DestoryGraph(G);
return 0;
}