算法与数据结构-6.图
6.1 图的定义和基本术语
6.1.1 图的定义
图G由V、E集合组成,记为G=(V,E)。其中,V是顶点的有穷非空集合,E是V中顶点偶对的有穷集合。这些顶点偶对称为边。
V(G),E(G)通常分别表示G的顶点集合和边集合,E(G)可以为空集,表示只有顶点没有边。
若E(G)为有向边的集合,则称为有向图;为无向边集合,则称为无向图。
有向图中,顶点对<x,y>有序,称为边,也可以称为弧。
无向图中,顶点对(x,y)无序。
有向图所有顶点入度之和等于所有顶点出度之和。
6.1.2 图的基本术语
- 子图
- 无向完全图:具有n(n-1)/2条边。
- 有向完全图:具有n(n-1) 条弧。
- 稀疏图和稠密图:有很少条边或弧(如e<nlog2n)的图。反之称为稠密图。
- 权:每条边可以标上具有某种含义的数值,该数值称为该边上的权。
- 网:带权的图。
- 邻接点
- 度、入度、出度:顶点v的度是指和v相关联的边的数目,记为TD(v1)。对于有向图,度分为出度和入度。入度是以顶点v为头的弧的数目,记为ID(v)。出度是以顶点v为尾的弧的数目,记为OD(v)。TD(v)=ID(v)+OD(v)。度数和相加=边数的两倍。
- 路径:是一个顶点序列,在有向图中是有向的。
- 路径长度:是一条路径上经过的边或弧的数目。
- 回路/环:第一个顶点和最后一个顶点相同的路径。
- 简单路径:序列中顶点不重复出现的路径。
- 简单回路/简单环:除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路。
- 连通:在无向图中,若顶点v到顶点v1有路径,则称v和v1是连通的。
- 连通图:对于任意两个顶点都是连通的。
- 连通分量:在非连通图中,无向图中的极大连通子图。
- 强连通图:在有向图中,对于每一对vi和vj∈V,vi≠vj,从vi到vj都存在路径。
- 强连通分量:有向图中的极大强连通子图。
- 连通图的生成树:一个极小连通子图,它含有图中全部顶点,但只有足以构成一棵树的n-1条边。如果在树上添加一条边,必定构成一个环(多于n-1条边)。小于n-1条边则是非连通图。但是,有n-1条边的图不一定是生成树。
- 有向树:有一个顶点的入度为0,其余顶点的入度均为1的有向图。
- 生成森林:由若干棵有向树组成,含有图中全部顶点,但只有足以构成若干棵不相交的有向树的弧。
6.2 图的存储结构
6.2.1 邻接矩阵
邻接矩阵表示法
表示顶点之间相邻关系的矩阵。
设G=(V,E)是具有n个顶点的图,则G的邻接矩阵是具有如下性质的n阶方阵:
【举例】下图中无向图G5和有向图G6的邻接矩阵分别为A1和A2。
若G是网,则邻接矩阵可以定义为:
其中:
wij表示边上的权值;
∞表示一个计算机允许的、大于所有边上权值的数。
【举例】下面带权图的两种邻接矩阵分别为A3和A4。
邻接矩阵表示法创建无向网
void create(AMGrapg &g){
int vex,arc; //定义顶点数、边数
int vexs[50],arcs[50][50]; //定义顶点集、边集
cin>>vex>>arc;
//输入顶点集
for(int i=0;i<vex;i++)
cin>>vexs[i];
//初始化
for(int i=0;i<vex;i++)
for(int j=0;j<vex;j++)
arcs[i][j]=0;
//构造邻接矩阵
for(int i=0;i<arc;i++){
int v1,v2,w; //输入一条边依附的顶点和权值
arcs[v1][v2]=w;
arcs[v2][v1]=w;
}
}
该算法时间复杂度为O(n2)。
邻接矩阵表示法的优缺点
优点:
- 便于判断两个顶点之间是否有边。
- 便于计算各个顶点的度。对于无向图,邻接矩阵第i行元素之和就是顶点i的度。对于有向图,第i行元素之和就是顶点i的出度,第i列元素之和就是顶点i的入度。
缺点:
- 不便于增加和删除顶点。
- 不便于统计边的数目,需要扫描邻接矩阵所有元素,时间复杂度为O(n2)。
- 空间复杂度高。稀疏图尤其浪费空间。
6.2.2 邻接表
邻接表表示法
图的一种链式存储结构。
在邻接表中,对图中每个顶点vi建立一个单链表,把与vi相邻接的顶点放在这个链表中。邻接表中每个单链表的第一个结点存放有关顶点的信息,把这一结点看做链表的表头,其余结点存放有关边的信息。由两部分组成:
(1)表头结点表:所有表头结点以顺序结构的形式存储,以便访问任一顶点的边链表。包括数据域和链域。数据域用于存储顶点vi的名称或其他有关信息。链域用于指向链表中第一个结点。
(2)边表:由表示图中顶点间关系的2n个边链表组成。边链表中边界点包括邻接点域、数据域、链域。邻接点域指示与顶点vi邻接的点在图中的位置。数据域存储和边有关的信息,如权值。链域指示与顶点vi邻接的下一条边的结点。
6.2.3 邻接表、邻接矩阵、图三者之间的转换
6.3 图的遍历
6.3.1 深度优先遍历
广度优先遍历通常借助队列来实现算法,深度优先遍历通常借助栈来实现算法。
图的广度优先遍历类似于二叉树的层次遍历,深度优先遍历类似于二叉树的先序遍历。
图的BFS生成树的树高比DFS生成树的树高小或相等。
对于一些特殊的图,比如只有一个顶点的图,其BFS生成树的树高和DFS生成树的树高相等。
一般的图,根据图的BFS生成树和DFS树的算法思想,BFS生成树的树高比DFS生成树的树高小。
树高的话,可以理解为顶点到结点的距离。对于 BFS生成树,每个结点到根结点都是最短距离;而DFS没有这个限制。
深度优先遍历(邻接矩阵)
#include<iostream>
#include<string.h>
using namespace std;
bool visited[100];
int jz[30][30];
char s[500];
int n,e;
void dfs(int x){
visited[x]=true;
cout<<s[x];
for(int i=0;i<n;i++){
if(visited[i]==false&&i!=x&&jz[x][i]==1)
dfs(i);
}
}
int main(){
while(cin>>n>>e){
memset(visited,false,sizeof(visited));
memset(jz,0,sizeof(jz));
for(int i=0;i<n;i++){
cin>>s[i];
}
for(int i=0;i<e;i++){
int a,b;
cin>>a>>b;
jz[a][b]=1;
jz[b][a]=1;
}
dfs(0);
cout<<endl;
}
}
6.3.2 广度优先遍历
广度优先遍历(邻接矩阵)
#include<iostream>
#include<string.h>
#include<queue>
using namespace std;
bool visited[100];
int jz[30][30];
char s[500];
int n,e;
void dfs(int x){
visited[x]=true;
cout<<s[x];
for(int i=0;i<n;i++){
if(visited[i]==false&&i!=x&&jz[x][i]==1)
dfs(i);
}
}
void bfs(int x){
queue<int>q;
visited[x]=true;
q.push(x);
while(!q.empty()){
for(int i=0;i<n;i++){
if(visited[i]==false&&i!=q.front()&&jz[q.front()][i]==1){
q.push(i);
visited[i]=true;
}
}
cout<<s[q.front()];
q.pop();
}
}
int main(){
while(cin>>n>>e){
memset(visited,false,sizeof(visited));
memset(jz,0,sizeof(jz));
for(int i=0;i<n;i++){
cin>>s[i];
}
for(int i=0;i<e;i++){
int a,b;
cin>>a>>b;
jz[a][b]=1;
jz[b][a]=1;
}
bfs(0);
cout<<endl;
}
}
6.4 最小生成树
6.4.1 普里姆算法
算法的时间复杂度为O(n2),与网中的边数无关,因此适用于稠密网。
“加点法”:
6.4.2 克鲁斯卡尔算法
算法的时间复杂度为O(elog2e),与网中的边数有关,因此适用于稀疏网。
“加边法”:
6.5 最短路径
两个顶点之间带权路径长度最短的路径称为最短路径。
迪杰斯特拉算法
6.6 拓扑排序
判断一个有向图是否有环
- 有向无环图(DAG图)
- AOV网:若用一个DAG图表示一个工程,其顶点表示活动,用有向边<vi,vj>表示活动vi先于活动vj进行的传递关系,则将这种DAG称为顶点表示活动网络,记为AOV网。
- 拓扑排序:对DAG所有顶点的一种排序,使若存在一条从顶点A到顶点B的路径,在排序中B排在A的后面。
过程:在有向图中选一个无前驱的顶点并输出它,然后删除该顶点和所有以它为尾的弧,重复操作,直至不存在无前驱的顶点。若此时输出的顶点数小于总顶点数,则存在环,否则输出的顶点序列即为一个拓扑排序。
易错点
图的 深度优先搜索(DFS) 使用了一种数据结构,这种数据结构是栈。
图的 广度优先搜索(BFS) 使用了一种数据结构,这种数据结构是队列。
在有n个顶点的有向图中,若要使任意两点间可以互相到达,则至少需要n条弧。