1.什么是图?
表示多对多的关系。包含:
(1)一组顶点:V(vertex)
在线性表中我们称数据对象为元素;在树中称之为结点;在图中就称之为顶点。线性表和树可以有没有数据对象的空表和空树,但空图必须至少有一个顶点,边可以没有。
(2)一组边:E(edge)
(3)边还可能有权重:代表一些必要信息。当然,顶点也可能有数据。
图的相关术语:
顶点的度:指与该顶点相连的边的个数,又分为入度和出度,入度是指向该顶点的边个数,出度即从该顶点出发的边个数。
邻接点:直接有边相连的两个顶点。
无向图:没有方向的图(也就是全为双向)
有向图:相连两顶点可能是双向也可能是单向。
网图:有权重的图。
概念很多,边看边记录
2.图的实现:
(1)邻接矩阵实现:
说白了就是用二维数组,序号表示结点,数组值表示边(或边的权重)。
下面是一个无向图(相连就是1,不相连就是0)。
先来看最简单的实现方法:也就是直接用二维数组表示
int G[maxN][maxN],Ne,Nv;//Ne,Nv为边和顶点的个数
void BuildGraph()
{
cin>>Nv>>Ne;
int i,j;
for(i=0;i<Nv;i++)
for(j=0;j<Nv;j++)
G[i][j]=0;
G[j][i]=0;
int v,w;//两个邻接点
for(i=0;i<Ne;i++){
cin>>v>>w;
//对于网图,也就是有权重的图,用邻接矩阵表示时边的值(G[][])为权重
G[v][w]=1;
//对于无向图,还要表示另一个边
G[w][v]=1;
}
}
对邻接矩阵的评价:
显然看上面实现的代码也知道这样表示图很简洁,一目了然,而且在进行各种操作时(如遍历顶点查找边插入边)很方便:
但是对于稀疏的图(边数很少)太浪费空间了,比如查找有多少个边,0很多1很少数组元素个数又很多,也会浪费时间。而且对于无向图两个顶点的关系相当于表示了两次,也会降低操作的效率。
一般来说,要邻接矩阵做题时用二维数组足够了(因为二维数组存放可能会浪费空间,应对1000以下数量级的顶点足够了),但是还是看一下用链表来表示的真正意义上的图吧:
#define MaxVertexNum 100 /* 最大顶点数设为100 */
#define INFINITY 65535 /* ∞设为双字节无符号整数的最大值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]; /* 存顶点的数据 */
/* 注意:很多情况下,顶点无数据,此时Data[]可以不用出现 */
};
typedef PtrToGNode MGraph; /* 以邻接矩阵存储的图类型 */
/* 初始化一个有VertexNum个顶点但没有边的图 */
MGraph CreateGraph( int VertexNum )
{
Vertex V, W;
MGraph Graph;
Graph = (MGraph)malloc(sizeof(struct GNode)); /* 建立图*/
Graph->Nv = VertexNum;
Graph->Ne = 0;
/* 初始化邻接矩阵 */
/* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
for (V=0; V<Graph->Nv; V++)
for (W=0; W<Graph->Nv; W++)
Graph->G[V][W] = INFINITY;
return Graph;
}
void InsertEdge( MGraph Graph, Edge E )
{
/* 插入边 <V1, V2> */
Graph->G[E->V1][E->V2] = E->Weight;
/* 若是无向图,还要插入边<V2, V1> */
Graph->G[E->V2][E->V1] = E->Weight;
}
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);
/* 注意:如果权重不是整型,Weight的读入格式要改 */
InsertEdge( Graph, E );
}
}
/* 如果顶点有数据的话,读入数据 */
for (V=0; V<Graph->Nv; V++)
scanf(" %c", &(Graph->Data[V]));
return Graph;
}
(2)用邻接表来实现:
上面已经分析了邻接矩阵的优缺点,我们知道邻接矩阵对于稀疏图来说非常浪费空间,而且当顶点数过多时还要开很大的二维数组来表示,很不划算,所以我们还可以用邻接表:用存放顶点的指针数组G[N]来存放每个顶点的非零元素(也就是邻接点) 还用上面那个例子:
仔细观察每个顶点存的数据,其实邻接表也不是完美的,由于每个顶点都存了所有邻接点,所以每两个邻接点也都存了两次,这样导致对于稠密图(边很多)的情况也不是很划算,所以只有当边足够稀疏时用邻接表才足够划算。
还是先来看简单的实现方法:
vector<int> G[maxN];//用vector来代替指针数组
int Nv,Ne;
void BuildGraph()
{
cin>>Nv>>Ne;
int v,w;//两个邻接点
for(int i=0;i<Ne;i++){
cin>>v>>w;
G[v].push_back(w);
//对于无向图,还要表示另一个边
G[w].push_back(v);
}
}
这样表示这是最简单的情况,也就是只需要表示一个无向图或有向图,如果是带有权重的边就需要用结构体:
struct node{
int v;//邻接点编号
datatype weight;//权重
};
vector<node> G[Nv];//用vector来代替指针数组
int Nv,Ne;
void BuildGraph()
{
cin>>Nv>>Ne;
int v,w,weight;//两个邻接点及它们边的权重
for(int i=0;i<Ne;i++){
cin>>v>>w>>weight;
G[v].push_back({w,weight});
//对于无向图,还要表示另一个边
G[w].push_back({v,weight});
}
}
当然也有真正意义上用链表实现的邻接表:
/* 图的邻接表表示法 */
#define MaxVertexNum 100 /* 最大顶点数设为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; /* 存顶点的数据 */
/* 注意:很多情况下,顶点无数据,此时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->Ne = 0;
/* 初始化邻接表头指针 */
/* 注意:这里默认顶点编号从0开始,到(Graph->Nv - 1) */
for (V=0; V<Graph->Nv; V++)
Graph->G[V].FirstEdge = NULL;
return Graph;
}
void InsertEdge( LGraph Graph, Edge E )
{
PtrToAdjVNode NewNode;
/* 插入边 <V1, V2> */
/* 为V2建立新的邻接点 */
NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
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> */
/* 为V1建立新的邻接点 */
NewNode = (PtrToAdjVNode)malloc(sizeof(struct AdjVNode));
NewNode->AdjV = E->V1;
NewNode->Weight = E->Weight;
/* 将V1插入V2的表头 */
NewNode->Next = Graph->G[E->V2].FirstEdge;
Graph->G[E->V2].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);
/* 注意:如果权重不是整型,Weight的读入格式要改 */
InsertEdge( Graph, E );
}
}
/* 如果顶点有数据的话,读入数据 */
for (V=0; V<Graph->Nv; V++)
scanf(" %c", &(Graph->G[V].Data));
return Graph;
}
对邻接表的评价:
只有当边足够稀疏时用邻接表才足够划算,否则就会一个顶点的指针存很多个邻接点,不方便操作。但是也有用处(肯定不如邻接矩阵):
3.图的遍历:
下面两种方法在(https://mp.youkuaiyun.com/postedit/104179414)中有具体的介绍。分别用两种简单的和两种复杂的邻接矩阵和邻接表各实现一次。
(1)深度优先搜索:
在图中深度优先搜索就是从一个顶点出发再依次从它的邻接点出发递归访问未被访问的邻接点。说白了就是每次都从第一个邻接点下手递归:
用简单的邻接矩阵表示的:
int Ne,Nv;
int visited[Nv]={0};//看顶点是否被访问,初始化为0,被访问后为1
int G[Nv][Nv];//假设已经表示好了
void DFS(int v)
{//从第顶点v开始
int visit[v]=1;
for(int i=0;i<Nv;i++){
if(G[v][i]==1/*有权重的话就是>0*/&&visited[i]==0)
//其他操作(遍历的目的)
DFS(i);
}
}
用链表实现的邻接表表示的:
/* Visited[]为全局变量,已经初始化为false */
void DFS( LGraph Graph, Vertex V)
{ /* 以V为出发点对邻接表存储的图Graph进行DFS搜索 */
PtrToAdjVNode W;
Visited[V] = true; /* 标记V已访问 */
for( W=Graph->G[V].FirstEdge; W; W=W->Next ) /* 对V的每个邻接点W->AdjV */
if ( !Visited[W->AdjV] ) /* 若W->AdjV未被访问 */
DFS( Graph, W->AdjV); /* 则递归访问之 */
}
(2)广度优先搜索:
在图中广度优先搜索就是从一个顶点出发访问它的未被访问的邻接点,开始依次从每个它的邻接点出发访问每个邻接点的所有邻接点,直到所有顶点被访问。说白了就是逐层访问:
用简单的邻接表表示的:
vector<int> G[maxN];//假设数据已经存入
int visited[Nv]={0};//看顶点是否被访问,初始化为0,被访问后为1
void BFS(int v)
{
queue<int> q;
q.push(v);
visited[v]=1;
while(!q.empty()){
int tmp=q.front();
q.pop();
for(int i=0;i<G[tmp].size();i++){
if(!visited[G[v][i]]){
q.push(G[v][i]);
visited[G[v][i]]=1;
//其他操作
}
}
}
用链表实现的邻接矩阵表示的:
/* 邻接矩阵存储的图 - BFS */
/* IsEdge(Graph, V, W)检查<V, W>是否图Graph中的一条边,即W是否V的邻接点。 */
/* 此函数根据图的不同类型要做不同的实现,关键取决于对不存在的边的表示方法。*/
/* 例如对有权图, 如果不存在的边被初始化为INFINITY, 则函数实现如下: */
bool IsEdge( MGraph Graph, Vertex V, Vertex W )
{
return Graph->G[V][W]<INFINITY ? true : false;
}
/* Visited[]为全局变量,已经初始化为false */
void BFS ( MGraph Graph, Vertex S)
{ /* 以S为出发点对邻接矩阵存储的图Graph进行BFS搜索 */
queue<int> Q;
Vertex V, W;
Visited[S] = true; /* 标记S已访问 */
Q.push(S); /* S入队列 */
while ( !Q.Empty() ) {
V = Q.front(); /* 弹出V */
Q.pop();
for( W=0; W<Graph->Nv; W++ ) /* 对图中的每个顶点W */
/* 若W是V的邻接点并且未访问过 */
if ( !Visited[W] && IsEdge(Graph, V, W) ) {
Visited[W] = true; /* 标记W已访问 */
Q.push(w); /* W入队列 */
}
} /* while结束*/
}
上面都是相对于所有顶点都有路径相连的连通图,并不是所有图中的所有顶点都有路径相连,因此还有图不连通的情况。好多概念我们先来看一下:
对于不连通的图,我们可以根据顶点是否相连通把它分成许多连通的子图,如:
而对于有向图和它的连通子图的相关概念:
那么如何遍历不连通的图呢,因为每次用广度或深度优先搜索都是搜索了顶点v所在的连通分量,遍历不连通的图就是遍历它的所有连通分量,那就很简单了,我们只需要遍历所有顶点,如果没有被访问,就DFS或者BFS这个顶点所在的连通分量,直到所有顶点被访问,这样时间复杂度就取决于两个因素了。