图的基本概念
主要掌握深度优先算法和广度优先算法、图的存储结构及其特性、存储结构之间的转化、遍历以及应用
图的定义
- 图G由顶点集V和边集E组成,|V|标识G中顶点的个数,也称为阶;
- 图不可以是空图,顶点集不能空,边集可以为空;
- 有向图:E是有向边的有限集合;<>
- 无向图:边是顶点的无序对;()
- 简单图:不存在重复边,不存在顶点到自身的边——>多重边;
- 完全图(简单完全图):任意两个顶点之间都存在边/任意两个顶点之间都存在方向相反的弧;
- 子图:并非V和E的任何子集都能构成G的子图,因为这样的子集可能不是图;
- 连通图:无向图中,若图中任意两个顶点之间都是连通(都有路径存在)则是连通图;
- 极大连通子图称为连通分量;
- 强连通图:有向图中,如果有一对顶点v和w,从v到w和从w到v之间都有路径,称强连通;
- 生成树:包含图中全部顶点的极小连通子图;去掉一条边不连通,加上一条边就有回路;
- 顶点的度、入度和出度:无向图的全部顶点的度的和等于边数的2倍;
- 简单路径:顶点不重复的路径称为简单路径-------简单回路;
- 距离:最短路径若存在则称为距离
🐖:图的遍历要求每个结点只能被访问一次,若图非连通,则从某一顶点出发无法访问到其他顶点;
图的存储和基本操作
邻接矩阵法
用一维数组存储图中顶点的信息,用二维数组存储图中边的信息,存储顶点之间邻接关系的二维数组称为邻接矩阵;
//图的邻接矩阵存储结构定义
#define MaxVertexNum 100 //顶点数目的最大值
typedef char VertexType; //顶点的数据类型
typedef int EdgeType; //带权图中边上权值的数据类型
typedef struct { //顶点表
VertexType Vex[MaxVertexNum][MaxVertexNum];//邻接矩阵,边表
int vexnum, arnum; //图的当前定点数和弧数
}MGraph;
注意:
- 无向图的邻接矩阵是对称矩阵(且唯一),有向图的邻接矩阵是非对称的!
- 对于无向图,邻接矩阵的第i行或者第i列非零元素的个数正好是顶点i的度TD;
- 对于有向图,第i行的非零元素是i的出度,第i列的非零元素是i的入度;
- 稠密图适合用邻接矩阵的存储表示
邻接表法
图为稀疏图的时候使用此方法,减少存储空间,结合了顺序存储和链式存储的方法
表中存在顶点表(顺序存储)和边表
//图的邻接表存储结构
#define MaxVertexNum 100 //图中顶点数目的最大值
typedef struct ArcNode{ //边表的结点
int adjvex; //该弧所指向的顶点的位置
struct ArcNode* next; //指向下一条弧的指针
//InfoType info; //网的边权值
}ArcNode;
typedef struct VNode { //顶点表结点
VertexType data; //顶点的信息
ArcNode* first; //指向第一条依附该顶点的弧的指针
}VNode,AdjList[MaxVertexNum];
typedef struct {
AdjList vertics; //邻接表
int vexnumm, arnum; //以邻接表存储的图类型
}ALGraph;
邻接表表示并不唯一,因为各边结点的链接次序可以是任意的,取决于建立邻接表的算法以及边的输入次序
十字链表法
有向图的链式存储结构,顶点结点之间是顺序存储的
邻接多重法
无向图的链式存储结构
图的基本操作
图的基本操作是独立于图的存储结构的
//从图的邻接表表示转换成邻接矩阵表示的算法
void Convert(ALGraph& G, int arcs[M][N]) {
//将邻接表方式表示的图G转换成邻接矩阵arcs
int n;
for (int i = 0;i < n;i++) { //依次遍历各顶点表结点为头的边链表
int p = (G->v[i]).firstarc; //取出顶点i的第一条出边
while (p != NULL) { //遍历链表
arcs[i][p->data] = 1;
p = p->nextarc; //取出下一条边
}
}
}
图的遍历
从图中某一顶点出发按照某种搜索方法沿着图中的边对图中所有的顶点访问一次且仅访问一次,树是一种特殊的图,所以树的遍历是一种特殊的图的遍历
广度优先搜索(BFS)
类似于二叉树的层序遍历算法
//广度优先的伪代码
bool visited[MAX_VERTEX_NUM]; //访问标记数组,表示每个结点是否被访问过
void BFSTraverse(Grah G) { //对图G进行广度优先遍历
for(int i=0;i<G.vexnum;++i){
visited[i] = FALSE; //对标记数组进行初始化
}
InitQueue(Q); //初始化辅助队列
for (int i = 0;i < G.vexnum;++i)//从0号顶点开始遍历
if (!visited[i]) //对每个连通分量调用一次BFS
BFS(G, i); //v[i]未访问过,则从vi开始BFS
}
void BFS(Graph G;int v) { //从顶点v出发,广度优先遍历图G
visit(v); //访问初始顶点v
visited[v] = true; //对v做访问标记
Enqueue(Q, v); //顶点v入队列Q
while (!isEmpty(Q)) {
DeQueue(Q, v); //顶点Q出队列
//检测v所有的邻接点
for (w = FisrtNeighbor(G, v);w >= 0;w = NextNeighbor(G, v, w)) {
if (!visited[w]) { //当邻接点未被访问时
visit[w]; //访问顶点w
visited[w] = TRUE; //设置标记
EnQueue(Q, w); //顶点w入队列
}
}
}
}
空间复杂度:均需要一个辅助队列O(v)
时间复杂度:邻接表需要O(v+e),邻接矩阵需要O(v*v)
BFS算法可以求单源最短路径问题
//BFS求单源最短路径算法
void min_BFS_distance(Graph G, int u) {
//d[i]表示从u到i结点的最短路径
for (int i = 0;i < G.vexnum;++i)
d[i] = 无穷;
visited[u] = TRUE;
d[u] = 0;
EnQueue(Q, u);
while (!isEmpty(Q)) {
DeQueue(Q, u);
for (w = FisrtNeighbor(G, u);w >= 0;w = NextNeighbor(G, u, w)) {
if (!visited[w]) { //当邻接点未被访问时
visited[w] = TRUE; //设置标记
d[w] = d[u] + 1;
EnQueue(Q, w); //顶点w入队列
}
}
}
}
广度优先生成树
广度遍历的过程中可以得到一颗遍历树,称为广度优先生成树
邻接矩阵的广度优先生成树是唯一的,但是邻接表的生成树不是唯一
深度优先搜索(DFS)
类似于树的先序遍历;是一个递归算法
//DFS的实现
bool visited[MAX_VERTEX_NUM]; //访问标记数组,表示每个结点是否被访问过
void DFSTraverse(Grah G) { //对图G进行广度优先遍历
for (int i = 0;i < G.vexnum;++i) {
visited[i] = FALSE; //对标记数组进行初始化
}
for (int i = 0;i < G.vexnum;++i)//从0号顶点开始遍历
if (!visited[i]) //对每个连通分量调用一次DFS
DFS(G, i); //v[i]未访问过,则从vi开始DFS
}
void DFS(Grapg G, int v) {
visit(v);
visited[v] = TRUE;
for (w = FisrtNeighbor(G, v);w >= 0;w = NextNeighbor(G, v, w)) {
if (!visited[w]) { //当邻接点未被访问时
DFS(G, w);
}
}
}
空间复杂度:需要一个递归空间栈,O(v);
时间复杂度:邻接矩阵O(v*v),邻接表O(v+e);
深度优先的生成树和生成森林
邻接表的深度优先生成树是不唯一的
图的遍历与图的连通性
遍历算法可以用来判断图的连通性
//设计一个算法判断无向图G是否为一棵树,若是一棵树则算法返回true,若不是返回false
//算法思想,判断方法:G必须是无回路的连通图或者是n-1条边的连通图
//所以可以采取深度优先的方法,探寻是否一次遍历就能访问到全部n个顶点和n-1条边
//DFS算法的实现
bool visited[MAX_VERTEX_NUM]
void DFSTraverse(Graph G) {
for (int t = 0;i < G.vernum;++i) {
visited[i] = FALSE; //对标记数组进行初始化
}
for (int i = 0;i < G.vernum;++i) {
if (!visited[i])
DFS(G, i);
}
}
void DFS(Graph G, int v) {
visit(v);
visited[v] = true;
for (int w = FirstNeighbor(G, v);w >= 0;w = NextNeighbor(G, v)) {
if (!visited[w])
DFS(G, w);
}
}
//用DFS遍历一次看是否访问到全部n个顶点和n-1条边
bool isTree(Graph& G) {
for (int t = 0;i < G.vernum;++i) {
visited[i] = FALSE; //对标记数组进行初始化
}
int Vnum = 0; //记录顶点数
int Enum = 0; //记录边数
DFS(G, 1, Vnum, Enum, visited);
if (Vnum == G.vexnum && Enum == (2 * G.vexnum - 1))
return true;
}
void DFS(Graph& G, int v, int& Vnum, int& Enum, int visited) {
//深度优先遍历图G,统计访问过的顶点数和边数,通过Vnum和Enum返回
visited[v] = TRUE;
Vnum++;
int w = FirstNeighbor(G, v);
while (w != -1) {
Enum++;
if (!visited[w])
DFS(G, w, Vnum, Enum, visited);
w = NextNeighbor(G, v, w);
}
}