数据结构-图
基本概念
图表示多对多的关系。包含
- 一组顶点:通常用V(Vertex)表示顶点集合
- 一组边:通常用E(Edge)表示边的集合
- 边是顶点对:(v,w)是集合E的元素,其中v,w是集合V的元素
- 有向边<v,w>表示从v指向w的边(单行线)
- 不考虑重边和自回路
抽象数据类型定义
类型名称:图
数据对象集:G(V,E)由一个非空的有限顶点集合V和一个有限边集合E组成
操作集:对于任意图G,以及v,e
- Graph Create():建立并返回空图
- Graph InsertVertex(Graph G, Vertex v):将v插入G
- Graph InsertEdge(Graph G, Edge e):将e插入G
- void DFS(Graph G, Vertex v):从顶点v出发深度优先遍历图G
- void BFS(Graph G, Vertex v):从顶点v出发广度优先遍历图G
- void ShortestPath(Graph G, Vertex v, int Dist[]):计算图G中顶点v到任意其他顶点的最短距离
- void MST(Graph G):计算图G的最小生成树
图的表示
图有两种表示方法,邻接矩阵和邻接表
我们可以创建一个二维数组G[N][N],若<vi,vj>是G中的边,则G[i][j]=1,否则为0。对于有向图,邻接矩阵是对称的,而对于无向图则不是。对于网络,G[i][j]的值是边<vi,vj>的权重,如果没有边,则为-∞。
邻接矩阵的优点
- 直观、简单、好理解
- 方便检查任意一对顶点间是否存在边
- 方便找任一顶点的所有“邻接点”(有边直接相连的顶点)
- 方便计算任一顶点的“度”(从该点出发的边数为“出度”,指向该点的边数为“入度”)
- 无向图:对应行(或列)非0元素的个数
- 有向图:对应行非0元素的个数是“出度”,对应列非0元素的个数是“入度”
邻接矩阵的缺点
- 浪费空间:存稀疏图(点很多而边很少)有大量无效元素
- 对稠密图(特别是完全图)还是很合算的
- 浪费时间:统计稀疏图中一共有多少条边
同时,我们可以创建一个指针数组G[N],对应矩阵每行一个链表,只存非0元素。对于网络,链表结构中要增加权重的域。
邻接表的优点
- 方便找任一顶点的所有“邻接点”
- 节约稀疏图的空间
- 需要N个头指针+2E个结点(每个结点至少2个域)
- 方便计算任一顶点的“度”
- 无向图:任一顶点的度就是链表的长度
- 有向图:只能计算“出度”,需要构造“逆邻接表”(存指向自己的边)来方便计算“出度”
邻接表的缺点
- 不方便检查任意一对顶点间是否存在边
邻接矩阵表示
结构体定义
#define MaxVertexNum 100
#define INFINITY 65535
typedef int Vertex; //用顶点下标表示顶点,为整型
typedef int WeightType;
typedef char DataType;
typedef struct ENode *PtrToENode;
struct ENode{ //边的定义
Vertex V1,V2; //有向边<v1,v2>
WeightType Weight; //权重
};
typedef PtrToENode Edge;
typedef struct GNode *PtrToGNode;
struct GNode{ //图结点的定义
int Nv; //顶点数
int Ne; //边数
WeightType G[MaxVertexNum][MaxVertexNum]; //邻接矩阵
DataType Data[MaxVertexNum]; //存顶点的数据,如果顶点不存数据,可以省略
};
typedef PtrToGNode MGraph;
图的初始化
MGraph CreateGraph(int VertexNum){ //初始化一个有VertexNum个顶点但没有边的图
Vertex V,W;
MGraph Graph;
Graph=(MGraph)malloc(sizeof(struct GNode)); //建立图
Graph->Nv=VertexNum;
Graph->Ne=0;
for(V=0;V<Graph->Nv;V++) //初始化邻接矩阵,顶点编号为0-(Nv-1)
for(W=0;W<Graph->Nv;W++)
Graph->G[V][W]=INFINITY;
return Graph;
}
边的插入
void InsertEdge(MGraph Graph, Edge E){
Graph->G[E->V1][E->V2]=E->Weight; //插入边<V1,V2>
Graph->G[E->V2][E->V1]=E->Weight; //若是无向图,还要插入边<V2,V1>
}
图的建立
MGraph BuildGraph(){
MGraph Graph;
Edge E;
Vertex V;
int Nv,i;
scanf("%d",&Nv); //读入顶点个数
Graph=CreateGraph(Nv); //初始化有Nv个顶点但没有边的图
scanf("%d",&(Graph->Ne)); //读入边数
if(Graph->Ne!=0){
E=(Edge)malloc(sizeof(struct ENode)); //建立边结点
for(i=0;i<Graph->Ne;i++){
scanf("%d %d %d",&(E->V1),&(E->V2),&(E->Weight)); //读入边
InsertEdge(Graph,E); //插入边
}
}
for(V=0;V<Graph->Nv;V++)
scanf("%d",&(Graph->Data[V])); //如果顶点有数据,则读入数据
return Graph;
}
邻接表表示
结构体定义
#define MaxVertexNum 100
typedef int Vertex; //顶点下标表示顶点,为整型
typedef int WeightType;
typedef char DataType;
typedef struct ENode *PtrToENode;
struct ENode{ //边的定义
Vertex V1,V2; //有向边<V1,V2>
WeightType Weight;
};
typedef PtrToENode Edge;
typedef struct AdjVNode *PtrToAdjVNode;
struct AdjVNode{ //邻接点的定义
Vertex AdjV; //邻接点下标
WeightType Weight; //边权重
PtrToAdjVNode Next; //指向下一个邻接点的指针
};
typedef struct Vnode{ //顶点表头结点的定义
PtrToAdjVNode FirstEdge; //边表头指针
DataType Data; //存顶点的数据
}AdjList[MaxVertexNum]; //AdjList是邻接表类型的数组
typedef struct GNode *PtrToGNode;
struct GNode{ //图结点的定义
int Nv; //顶点数
int Ne; //边数
AdjList G; //邻接表
};
typedef PtrToGNode LGraph;
图的初始化
LGraph CreateGraph(int VertexNum){ //初始化一个有VertexNum个顶点但没有边的图
Vertex V;
LGraph Graph;
Graph=(LGraph)malloc(sizeof(struct GNode)); //建立图
Graph->Nv=VertexNum;
Graph->Ve=0;
for(V=0;V<Graph->Nv;V++) //初始化邻接表头指针,定点编号0-(Graph->Nv-1)
Graph->G[V].FirstEdge=NULL;
return Graph;
}
边的插入
void InsertEdge(LGraph Graph, Edge E){
PtrToAdjVNode NewNode;
//插入边<V1,V2>
NewNode=(PtrToAdjVNode)malloc(sizeof(struct AdjVNode)); //为V2建立新的邻接点
NewNode->AdjV=E->V2;
NewNode->Weight=E->Weight;
//将V2插入V1的表头
NewNode->Next=Graph->G[E->V1].FirstEdge;
Graph->G[E->V1].FirstEdge=NewNode;
//若是无向图,还要插入边<V2,V1>
NewNode=(PtrToAdjVNode)malloc(sizeof(struct AdjVNode)); //为V1建立新的邻接点
NewNode->AdjV=E->V1;
NewNode->Weight=E->Weight;
//将V1插入V2的表头
NewNode->Next=Graph->G[E->V2].FirstEdge;
Graph->G[E->V1].FirstEdge=NewNode;
}
图的建立
LGraph BuildGraph(){
LGraph Graph;
Edge E;
Vertex V;
int Nv,i;
scanf("%d",&Nv); //读入顶点个数
Graph=CreateGraph(Nv); //初始化有Nv个顶点但没有边的图
scanf("%d",&(Graph->Ne)); //读入边数
if(Graph->Ne!=0){
E=(Edge)malloc(sizeof(struct ENode)); //建立边结点
for(i=0;i<Graph->Ne;i++){
scanf("%d %d %d",&(E->V1),&(E->V2),&(E->Weight)); //读入边的起点、终点、权重
InsertEdge(Graph,E); //插入边
}
}
for(V=0;V<Graph->Nv;V++) //如果顶点有数据,读入数据
scanf("%d",&(Graph->G[V].Data));
return Graph;
}
图的遍历
图的遍历有两种方法,深度优先搜索(DFS)和广度优先搜索(BFS)。
深度优先搜索
图的深度优先搜索类似于树的先序遍历。若有N个顶点、E条边,时间复杂度为
- 用邻接表存储图,有O(N+E)
- 用邻接矩阵存储图,有O(N^2)
伪代码如下:
void DFS(Vertex V){
visited[V]=true;
for(V的每个邻接点W)
if(!visited[W])
DFS(W);
}
C语言代码如下:
void Visit(Vertex V){
printf("正在访问顶点%d",V);
}
void DFS(LGraph Graph, Vertex V, void (*Visit)(Vertex)){ //以V为出发点对邻接表存储的图Graph进行DFS
PtrToAdjVNode W;
Visit(V); //访问第V个顶点
Visited[V]=true; //Visited[]为全局变量,初始化为false,已访问则标记为true
for(W=Graph->G[V].FirstEdge;W;W=W->Next) //对V的每个邻接点W->AdjV
if(!Visited[W->AdjV]) //若W->AdjV未被访问
DFS(Graph,W->AdjV,Visit); //则递归访问
}
广度优先搜索
图的广度优先搜索类似于树的层序遍历,需要使用队列。若有N个顶点、E条边,时间复杂度为
- 用邻接表存储图,有O(N+E)
- 用邻接矩阵存储图,有O(N^2)
伪代码如下:
void BFS(Vertex V){
visited[V]=true;
Enqueue(V,Q);
while(!IsEmpty(Q)){
V=Dequeue(Q);
for(V的每个邻接点W)
if(!visited[W]){
visited[W]=true;
Enqueue(W,Q);
}
}
}
C语言代码如下:
bool IsEdge(MGraph Graph, Vertex V, Vertex W){ //判断<V,W>是否是图Graph中的一条边,即W是否是V的邻接点
return Graph->G[V][W]<INFINITY?true:false;
}
void BFS(MGraph Graph, Vertex S, void (*Visit)(Vertex)){ //以S为出发点对邻接矩阵存储的图Graphj进行BFS
Queue Q;
Vertex V,W;
Q=CreateQueue(MaxSize); //创建空队列
Visit(S);
Visited[S]=true;
AddQ(Q,S); //S入队
while(!IsEmpty(Q)){
V=DeleteQ(Q); //出队
for(W=0;W,Graph->Nv;W++) //对于图中的每个顶点W
if(!Visited[W]&&IsEdge(Graph,V,W)){ //如果W是V的临界点且未被访问过
Visit(W);
Visited[W]=true;
AddQ(Q,W);
}
}
}