目录
一、图的基本概念
(1)图的定义
(2)图的基本术语
(3)图的基本运算
二、图的存储结构
(1)邻接矩阵
① 图的邻接矩阵
② 带权图(网)的邻接矩阵
③ 邻接矩阵的类型定义
④ 建立无向带权邻接矩阵
(2)邻接表
① 定义
② 示例
③ 结论
④ 邻接表的类型定义
⑤ 计算图的度
⑥ 带权图邻接表
三、图的遍历
(1)遍历的含义及方法
① 含义
② 方法
(2)连通图的深度优先搜索(DFS)
① 过程
② 示例
③ 算法
④ 对图按深度优先遍历的递归算法(邻接表)
⑤ 对图按深度优先遍历的递归算法(邻接矩阵)
(3)连通图的广度优先搜索法(BFS)
① 过程
② 示例
③ 算法
④ 广度优先遍历算法基本思想
⑤ 广度优先遍历算法
(4)应用举例 —— 求图的连通分量
① 判断图的连通性
② 求图的连通分量
四、图的应用
(1)生成树
① 生成树定义
② 示例
(2)最小生成树
① 问题的提出
② 最小生成树定义
③ 最小生成树的构造算法
④ 最小生成树的构造方法(Prim 法)
⑤ 最小生成树的构造方法(Kruskal 克鲁斯卡尔法 )
(3)拓扑排序
① 问题的提出
② 拓扑排序定义
③ 拓扑排序方法
④ 拓扑排序算法
一、图的基本概念
(1)图的定义
图 G —— 是由集合 V 和 E 组成,记成 G =(V,E),其中:
- V — 顶点集(非空)
- E — 边集(可空)
边是顶点的有序对或无序对(边反映了两顶点之间的关系)
- 有向图:边是顶点的有序对的图(图中每条边都用箭头指明了方向)
- 无向图:边是顶点的无序对的图
注意:
- 边集可空
- 边集中不允许出现相同的边
【示例 1 】
- V(G1) = { 1,2,3,4 }
- E(G1) = { (1,2),(1,3),(1,4),(2,3), (2,4),(3,4) }
【示例 2 】
- V(G2) = { 1,2,3,4 ,5,6,7 }
- E(G2) = { (1,2),(1,3),(2,4),(2,5), (3,6),(3,7) }
【示例 3 】
- V(G3) = { 1,2,3 }
- E(G3) = { <1,2>,<2,1>,<2,3> }
(2)图的基本术语
顶点 (Vertex) :图中的数据元素
<Vi ,Vj> :有向图中,顶点 Vi 到 顶点 Vj 的边,也称弧
- Vi :弧尾(初始点)—— 无箭头端
- Vj :弧头(终端点)—— 箭头端
完全图 :(顶点数 n)
- 无向完全图:边数 = n*(n-1)/2 的无向图
- 有向完全图:边数 = n*(n-1) 的有向图
权 :与图中的边相关的数
子图 : 图 G 和 G′ , 若有 V(G′) ⊆ V(G) 和 E(G′) ⊆ E(G) , 则称 G′ 为图 G 的子图
邻接 :若 (Vi ,Vj ) ∈ E(G),则称 Vi 和 Vj 互为邻接点
关联 :若 (Vi ,Vj ) ∈ E(G),则称边 (Vi ,Vj ) 关联于顶点 Vi 和 Vj
- 邻接是指顶点之间的关系,而关联是指边与顶点间的关系
- 若弧 <Vi ,Vj> ∈ E(G),则称 Vj 是 Vi 的邻接点
度 :
● 无向图 D(Vi) :顶点 Vi 的度为与 Vi 相关联的边的个数
● 有向图:
- 出度 OD(Vi) :顶点 Vi 的出度为以 Vi 为尾的出边数
- 入度 ID(Vi) :顶点 Vi 的入度为以 Vi 为头的入边数
- 度 D(Vi) : 有向图的度 = 入度 + 出度,即 D(Vi) = OD(Vi) + ID(Vi)
路径 :图中,顶点 Vp 至顶点 Vq 的路径是顶点序列 { Vp,Vi1,Vi2,…,Vin,Vq } 且:
- 对无向图:边 (Vp,Vi1),(Vi1,Vi2),…,(Vin,Vq) ∈ VR(G)
- 对有向图:弧 <Vp,Vi1>,<Vi1,Vi2>,…,<Vin,Vq> ∈ VR(G)
路径长度 :路径上边或弧的数目
简单路径 : 除第一个和最后一个外,其余各顶点均不 相同的路径
回路 :第一个和最后一个顶点相同的路径,也称环
简单回路 :第一个和最后一个顶点相同的简单路径
- 注:回路中可以有多个圈,而简单回路只能有一个圈
连通 : 无向图中,若从顶点 V i 到 V j 顶点有路径,则 称 V i 和 V j 是连通的
连通图 和 连通分量 :针对无向图而言
强连通图 和 强连通分量 :针对有向图而言
生成树 : 含有该连通图的全部顶点的一个 极 小 连通子图若连通图 G 的顶点个数为 n,则 G 的生成树的边数为 n-1
G 的子图 G’ 边数大于 n-1,则 G’ 中一定 有环 G 的子图 G’ 边数小于 n-1,则 G’ 中一定 不连通
生成森林 : 在非连通图中,每个连通分量都可得到一个 极小 连通子图,也就是生成树
- 这些生成树就组成了一个非连通图的生成森林
(3)图的基本运算
➢ 建立图 : GreateGraph(G,V,E)➢ 取顶点信息 : GetVex(G,u)➢ 取边信息 : Getarc(G,u,v)➢ 查询第一个邻接点 : FirstVex(G,u)➢ 查询下一个邻接点 : NextVex(G,u,v)➢ 插入顶点 : InsertVex(G,v)➢ 删除顶点 : DeleteVex(G,v)➢ 插入边 : InsertArc(G,v,w)➢ 删除边 : DeleteArc(G,v,w)➢ 遍历图 : Travers(G,tag)
二、图的存储结构
(1)邻接矩阵
① 图的邻接矩阵
【含义】图的邻接矩阵 :表示图的各顶点之间关系的矩阵
【定义】设 G=(V,E) 是 n 个顶点的图,则 G 的邻接矩阵为下列 n 阶方阵:
【结论】
- 无向图的邻接矩阵是对称矩阵 (∵ (Vi ,Vj ) ∈ E(G),则 (Vj ,Vi ) ∈ E(G) )
- 从邻接矩阵容易判断任意两顶点间是否有边相联容易求出各顶点的度
- 无向图:顶点 Vi 的度 D(Vi ) = 矩阵中第 i 行或第 j 列元素之和
- 有向图:OD (Vi) = 矩阵中第 i 行元素之和 ; ID (Vi) = 矩阵中第 i 列元素之和
② 带权图(网)的邻接矩阵
③ 邻接矩阵的类型定义
【类型定义】
const int vnum = 20; typedef struct gp { VertexType vexs[vnum]; // 顶点信息数组 WeightType arcs[vnum][vnum]; // 邻接矩阵数组 int vexnum, arcnum; // 顶点数,边数 } WGraph;
【代码详解】
- 通过上述代码,定义了一个名为 WGraph 的结构体类型,用于表示带权有向图。
- WGraph 结构体内部包含了顶点信息数组 vexs 和邻接矩阵数组 arcs,以及两个整型成员变量 vexnum 和 arcnum,分别表示顶点数和边数。
- 这个结构体可以用来表示和存储带权有向图的相关信息。
const int vnum = 20;
: 使用 const 关键字定义了一个名为 vnum 的整型常量,并将其值设置为 20。这个常量 vnum 的值是不可修改的。
typedef struct gp { ... } WGraph;
: 使用 typedef 关键字定义了一个结构体类型,名为 gp。结构体中包含了 VertexType 类型的 vexs 数组、WeightType 类型的 arcs 二维数组,以及两个整型成员变量 vexnum 和 arcnum。最后通过 typedef 定义了别名 WGraph,用于表示该结构体类型。
VertexType vexs[vnum];
: 结构体中的 vexs 数组是 VertexType 类型的数组,用于存储顶点的信息。
WeightType arcs[vnum][vnum];
: 结构体中的 arcs 数组是 WeightType 类型的二维数组,表示顶点之间的邻接关系,用于存储边的权值,构成邻接矩阵。
int vexnum, arcnum;
: 结构体中的 vexnum 和 arcnum 是两个整型成员变量,分别表示顶点数和边数。
④ 建立无向带权邻接矩阵
【示例】
- 将矩阵 A 的每个元素都初始化为最大值
- 然后读入边和权值(i,j,wij),将 A 的相应元素设为 wij
【示例代码】算法如下:
void CreatGraph(Graph* g) { int i, j, n, e, w; char ch; scanf("%d %d", &n, &e); // 输入顶点数和边数 g->vexnum = n; g->arcnum = e; for (i = 0; i < g->vexnum; i++) { scanf(" %c", &ch); // 输入顶点信息,这里加一个空格可以忽略输入缓冲区中的回车符 g->vexs[i] = ch; } for (i = 0; i < g->vexnum; i++) { for (j = 0; j < g->vexnum; j++) { g->arcs[i][j] = MAX_INT; // 初始化邻接矩阵,将所有边的权值设为最大值 } } for (k = 0; k < g->arcnum; k++) { scanf("%d %d %d", &i, &j, &w); // 输入边的起点、终点和权值 g->arcs[i][j] = w; // 更新邻接矩阵中边的权值 g->arcs[j][i] = w; // 无向图需要同时更新两个方向的边的权值 } }
【代码详解】
void CreatGraph(Graph* g)
: 定义了一个名为CreatGraph
的函数,参数为指向Graph
结构体的指针g
。函数类型为void
,即没有返回值。
scanf("%d %d", &n, &e);
: 使用scanf
函数从用户输入中读取两个整数,分别赋值给变量n
和e
,即顶点数和边数。
g->vexnum = n;
和g->arcnum = e;
: 将变量n
的值赋给g
指向的结构体中的成员变量vexnum
和arcnum
,即更新图的顶点数和边数。
scanf(" %c", &ch);
: 使用scanf
函数从用户输入中读取一个字符,赋值给变量ch
。这里加一个空格可以忽略输入缓冲区中的回车符。
g->vexs[i] = ch;
: 将变量ch
的值赋给g
指向的结构体中的顶点信息数组vexs[i]
,即更新图的顶点信息。
g->arcs[i][j] = MAX_INT;
: 将MAX_INT
的值赋给g
指向的结构体中的邻接矩阵数组arcs[i][j]
,即将所有边的权值设为最大值。
scanf("%d %d %d", &i, &j, &w);
: 使用scanf
函数从用户输入中读取三个整数,分别赋值给变量i
、j
和w
,即边的起点、终点和权值。
g->arcs[i][j] = w;
和g->arcs[j][i] = w;
: 将变量w
的值赋给g
指向的结构体中的邻接矩阵数组arcs[i][j]
和arcs[j][i]
,即更新图中边的权值。这个过程同时更新了有向图中起点到终点和终点到起点的两个方向的边的权值。
(2)邻接表
① 定义
定义: 对图 G 中每个顶点都建立一个单链表,第 i 个单链表(称边表)链接图中与顶点 Vi 相邻接的所有顶点。
② 示例
③ 结论
- n 个顶点、e 条边的无向图,则其邻接表的表头结点数为 n, 链表结点总数为 2e
- 对于无向图,第 i 个链表的结点数为顶点 Vi 的度
- 对于有向图,第 i 个链表的结点数为顶点 Vi 的出度
- 在边稀疏时,邻接表比邻接矩阵省单元
- 邻接表表示在检测边数方面比邻接矩阵表示效率要高
④ 邻接表的类型定义
【类型定义】
#define vnum 20 typedef struct arcnode { int adjvex; // 下一条边的顶点编号 WeightType weight; // 带权图的权值域 struct arcnode* nextarc; // 指向下一条边的指针 } ArcNode; typedef struct vexnode { int vertex; // 顶点编号 ArcNode* firstarc; // 指向第一条边的指针 } AdjList[vnum]; typedef struct gp { AdjList adjlist; int vexnum, arcnum; // 顶点和边的个数 } Graph;
【代码详解】
- 通过以上定义的数据结构,我们可以使用
Graph
结构来表示一个图,并通过邻接表的方式存储顶点和边的信息。- 每个顶点都有一个
vexnode
结构体,其中包含了顶点编号和指向第一条边的指针。- 每条边都由
ArcNode
结构体表示,其中包含了下一条边的顶点编号、权值和指向下一条边的指针。- 这样的数据结构可以方便地表示和操作图的信息,例如可以通过遍历邻接表来获取某个顶点的相邻顶点,或者通过访问边的信息来获取边的权值等。
- 同时,通过邻接表的方式存储图的信息,可以节省存储空间,特别适用于稀疏图的表示。
#define vnum 20
: 使用#define
定义了一个宏常量vnum
,值为20
。此处使用宏定义可以方便地在代码中引用vnum
,避免硬编码。
typedef struct arcnode { ... } ArcNode;
: 使用typedef
关键字定义了一个结构体类型ArcNode
。结构体中包含了一个整数类型的adjvex
(下一条边的顶点编号)、带权图的权值域weight
(WeightType)、以及一个指向下一条边的指针nextarc
。
typedef struct vexnode { ... } AdjList[vnum];
: 使用typedef
关键字定义了一个结构体类型vexnode
。结构体中包含了一个整数类型的vertex
(顶点编号)和一个指向第一条边的指针firstarc
。然后使用AdjList[vnum]
定义了AdjList
数组类型,数组大小为vnum
,元素类型为vexnode
。
typedef struct gp { ... } Graph;
: 使用typedef
关键字定义了一个结构体类型gp
。结构体中包含了一个AdjList
数组adjlist
,以及两个整数成员变量vexnum
和arcnum
(顶点和边的个数)。然后使用Graph
定义了别名,用于表示该结构体类型。通过上述代码,我们定义了以下几个数据结构:
ArcNode
:表示图中的一条边。它包含了一个整数类型的adjvex
成员,表示下一条边的顶点编号;一个WeightType
类型的weight
成员,表示带权图的权值域;以及一个指向下一条边的指针nextarc
。
AdjList
:表示图的邻接表。它是一个AdjList[vnum]
数组,数组的每个元素是一个vexnode
结构体。vexnode
结构体包含一个整数类型的vertex
成员,表示顶点编号;一个指向第一条边的指针firstarc
。
Graph
:表示整个图的结构。它包含了一个AdjList
类型的adjlist
数组,用于存储邻接表信息;以及两个整数类型的vexnum
和arcnum
成员,表示顶点数和边数。
⑤ 计算图的度
➢ 对于 无向图,第 i 个链表的结点数为顶点 V i 的度➢ 对于 有向图,第 i 个链表的结点数只为顶点 V i 的出度
- 若要求入度,必须遍历整个邻接表
- 在单链表中,其邻接点域的值为 i 的结点个数是顶点 Vi 的入度
➢ 对于有向图,有时候就要建立一个你邻接表
- 即队每个顶点 Vi 建立一个以 Vi 为弧头的邻接点的链表
- 这样,逆邻接表第 i 个单链表中的结点个数就是 Vi 的入度
⑥ 带权图邻接表
结点形式:
建立有向图的邻接表的方法:
- 将邻接表表头数组初始化
- 第 i 个表头的 vertex 域初始化为 i
- first 域初始化为 NULL
- 读入顶点对 <i,j>,产生一个表结点
- 将 j 放入到该结点的 adjvex 域
- 将该结点链到邻接表的表头数组的第 i 个元素的 first 域上
三、图的遍历
(1)遍历的含义及方法
① 含义
图的遍历 : 从图 G 中某一顶点 v 出发,顺 序访问各顶点一次
② 方法
方法:为克服顶点的重复访问,设立辅助数组 visited[n]
遍历方法:
- 深度优先搜索法 —— 类似先序遍历
- 广度优先搜索法 —— 类似层次遍历
(2)连通图的深度优先搜索(DFS)
① 过程
从图 G(V,E) 中任一顶点 V i 开始,首先访问 V i ,然后访问 V i 的 任 一未访问过的邻接点 V j ,再以 V j 为新的出发点继续进行深度优先 搜索,直到所有顶点都被访问过。
② 示例
③ 算法
【分析】
- 为克服顶点的重复访问,设立一标志向量 visited [n]
- 图可用邻接矩阵或邻接表表示
- DFS 规则具有递归性,故需用到栈
【注意】
- 搜索到达某个顶点时(图中仍有顶点未被访问),如果这个顶点的所有邻接点都被访问过,那么搜索就要回到前一个被访问过的顶点,再从该顶点的下一未被访问的邻接点开始深度优先搜索
- 深度搜索的顶点的访问序列不是唯一的
【算法代码】连通图的深度优先搜索的算法:
void Dfs(Graph g, int v) { // 访问顶点v visit(v); // 将顶点v标记为已访问 visited[v] = 1; // visited[v]初值都为0,顶点v已被访问,就置为1 // 找出顶点v的第一个邻接点w int w = g.adjlist[v].firstarc; // 遍历顶点v的所有邻接点w while (w存在) { // 如果邻接点w未被访问,则递归调用Dfs函数访问w if (w 未被访问) { Dfs(g, w); } // 继续找出顶点v的下一个邻接点w w = g.adjlist[v].nextarc; } }
【代码详解】
- 此算法通过递归的方式,以深度优先的顺序来遍历图中的所有顶点,并执行相应的操作。
- 通过标记顶点的访问状态,可以避免重复访问或陷入死循环。
void Dfs(Graph g, int v)
: 定义了一个深度优先搜索(DFS)的函数,用于遍历图。函数接受两个参数,Graph g
表示图的数据结构,int v
表示从顶点v开始遍历。
visit(v)
: 在访问某个顶点v时,执行相应的操作。这里省略了具体的访问操作,你可以根据实际需求自行定义。
visited[v] = 1;
: 将顶点v标记为已访问,将其对应的visited
数组的值设置为1。在函数开始时,visited
数组需要初始化为0,表示所有顶点都未被访问过。
int w = g.adjlist[v].firstarc;
: 找出顶点v的第一个邻接点w。通过g.adjlist[v]
获取顶点v的邻接表,然后访问其firstarc
成员,即可得到顶点v的第一个邻接点。
while (w存在) { ... }
: 通过一个循环来遍历顶点v的所有邻接点w。当w存在时,执行循环内部的操作。
if (w 未被访问) { Dfs(g, w); }
: 判断邻接点w是否未被访问过。如果w未被访问过,则递归调用Dfs函数,以顶点w作为起点,继续进行深度优先搜索。
w = g.adjlist[v].nextarc;
: 找出顶点v的下一个邻接点w。通过访问g.adjlist[v]
的nextarc
成员,即可得到顶点v的下一个邻接点。循环将继续执行,直到遍历完v的所有邻接点。【区别】
④ 对图按深度优先遍历的递归算法(邻接表)
【示例代码】
int visited[N] = {0}; // 对访问标记visited数组初始化 void Dfs(Graph g, int v) { // 从第v个顶点出发递归地深度优先遍历图g,图以邻接表作为存储结构 ArcNode* p; printf("%d", v); // 访问起始顶点v visited[v] = 1; // 置“已访问”标记 p = g.adjlist[v].firstarc; // 取顶点表中v的边表头指针 while (p != NULL) // 依次搜索v的邻接点 { if (!visited[p->adjvex]) // v的一个邻接点未被访问 { Dfs(g, p->adjvex); // 沿此邻接点出发继续DFS } p = p->nextarc; // 取v的下一个邻接点 } }
【示意图】
【代码详解】
- 通过以上代码,可以实现深度优先搜索算法来遍历图,并通过 visited 数组来标记顶点的访问状态。
- 在访问每个顶点时,可以执行相应的操作,如打印顶点值。
int visited[N] = {0};
: 定义一个大小为N的整型数组 visited,并将所有元素初始化为0。该数组用于标记顶点是否被访问过。
void Dfs(Graph g, int v)
: 定义了一个深度优先搜索(DFS)的函数,用于遍历图。函数接受两个参数,Graph g
表示图的数据结构,int v
表示从顶点v开始遍历。
printf("%d", v);
: 输出当前访问的顶点 v。这里使用 printf 语句打印顶点编号,你可以根据实际需求自行改变输出方式。
visited[v] = 1;
: 将顶点 v 标记为已访问,将 visited 数组中 v 对应位置的值设置为1。在函数开始时,visited 数组需要初始化为 0,表示所有顶点都未被访问过。
p = g.adjlist[v].firstarc;
: 获取顶点 v 的第一个邻接点 p。通过 g.adjlist[v] 访问顶点 v 的邻接表,然后取得邻接表的 firstarc 成员,即可得到顶点 v 的第一个邻接点。
while (p != NULL) { ... }
: 通过一个循环来遍历顶点 v 的邻接点 p。只要 p 不等于NULL,就执行循环内部的操作。
if (!visited[p->adjvex]) { Dfs(g, p->adjvex); }
: 判断邻接点 p 是否未被访问过。如果 p 未被访问过,则递归调用 Dfs 函数,以顶点 p->adjvex 作为起点,继续进行深度优先搜索。
p = p->nextarc;
: 获取顶点 v 的下一个邻接点 p。通过访问 p 的 nextarc 成员,即可得到顶点 v 的下一个邻接点。循环将继续执行,直到遍历完顶点 v 的所有邻接点。【时间复杂度】 O(n+e)
【区别】
⑤ 对图按深度优先遍历的递归算法(邻接矩阵)
【示例代码】
int visited[N] = {0}; // 对访问标记visited数组初始化 void Dfs(Graph g, int v) { // 从第v个顶点出发递归地深度优先遍历图g,图以邻接矩阵作为存储结构 int j; printf("%d", v); // 访问起始顶点v visited[v] = 1; // 置“已访问”标记 for (j = 0; j < n; j++) // n为顶点数,j为顶点编号 { m = g->arcs[v][j]; // 顺序访问矩阵的第v行结点 if (m && !visited[j]) // 如果v与j邻接,且j未被访问 { Dfs(g, j); // 递归访问j } } }
【代码详解】
- 这段代码实现了使用邻接矩阵存储结构的深度优先搜索算法,通过递归地遍历图中的顶点。
- 通过以上代码,可以实现深度优先搜索算法来遍历以邻接矩阵作为存储结构的图。
- 在访问每个顶点时,打印顶点值或执行其他相关操作。
- 通过 visited 数组标记顶点的访问状态,以避免重复访问或陷入死循环。
int visited[N] = {0};
: 定义了一个大小为N的整型数组 visited 并将所有元素初始化为 0。用于标记顶点是否被访问过。
void Dfs(Graph g, int v)
: 定义了一个深度优先搜索(DFS)的函数,用于遍历图。函数接受两个参数,Graph g
表示图的数据结构,int v
表示从顶点 v 开始进行遍历。
printf("%d", v);
: 打印当前访问的顶点 v。这里使用 printf 语句输出顶点编号,你可以根据实际需求修改输出内容。
visited[v] = 1;
: 将顶点 v 标记为已访问,将 visited 数组中 v 对应位置的值设置为1。在函数开始时,需要将 visited 数组初始化为 0,表示所有顶点都未被访问过。
for (j = 0; j < n; j++) { ... }
: 通过一个循环来遍历从顶点v出发的所有邻接点。循环从编号 0 开始,到编号 n-1 结束,依次遍历顶点v的邻接矩阵中的列。
m = g->arcs[v][j];
: 获取邻接矩阵中顶点 v 的第 j 列元素的值,通过g->arcs[v][j]
访问矩阵中的对应位置。
if (m && !visited[j]) { Dfs(g, j); }
: 判断顶点 v 与顶点 j 是否邻接,且顶点 j 未被访问过。如果满足条件,则递归调用 Dfs 函数,以顶点j作为起点,继续进行深度优先搜索。
Dfs(g, j);
: 递归调用自身的 Dfs 函数,以顶点j作为起点进行深度优先搜索。这是深度优先搜索算法的核心部分,它会一直递归至没有未访问的邻接顶点为止。【时间复杂度】O(n²)
【区别】
(3)连通图的广度优先搜索法(BFS)
① 过程
从图 G(V,E) 中某一点 V i 出发,首先访问 V i 的所有邻 接点( w 1 , w 2 , … , w t ),然后再顺序访问 w 1 , w 2 , … , w t 的所有未被访问过的邻接点 …., 此过程直到所有 顶点都被访问过。
② 示例
③ 算法
【分析】
- 为克服顶点的重复访问,设立一标志向量 visited [n]
- 图可用邻接矩阵或邻接表表示
- 顶点的处理次序 —— 先进先出,故需用到一队列
④ 广度优先遍历算法基本思想
1. 所有结点标记置为 “未被访问” 标志
2. 访问起始顶点,同时置起始顶点 “已访问” 标记
3. 将起始顶点进队列
4. 当队列不为空时重复执行以下步骤:
① 取当前队头顶点
② 对与队头顶点相邻接的所有未被访问过的顶点依次做:
- 访问该顶点
- 置该顶点为“已访问”标记,并将它进队列
③ 当前队头元素顶点出队;
④ 重复进行,直到队空时结束。
⑤ 广度优先遍历算法
【实现一:示例代码】
/* 广度优先搜索算法(BFS)的第一个实现(bfs函数): 使用了邻接表表示图,通过数组queue实现队列结构; 使用队列来存放已访问过的顶点,实现广度优先搜索 */ int visited[N] = {0}; // 对访问标记visited数组初始化 int queue[N]; // 队列queue存放已访问过的顶点 void bfs(Graph g, int v) { // 从顶点v出发,按广度优先遍历图g,图用邻接表表示 printf("%d", v); // 访问初始顶点v visited[v] = 1; // 访问初始顶点,并标记为已访问 int rear = 1; // 队尾指针 int front = 0; // 队头指针 queue[rear] = v; // 起始顶点(序号)入队 while (front != rear) // 队列不空,则循环 { front = (front + 1) % N; // 置队头,出队一个元素 v = queue[front]; // 队头元素出队 ArcNode* p = g.adjlist[v].firstarc; // 取刚出队顶点v的边表的头指针 while (p != NULL) // 依次搜索v的邻接点 { if (!visited[p->adjvex]) // v的一个邻接点未被访问 { printf("%d", p->adjvex); // 访问此邻接点 visited[p->adjvex] = 1; // 标记为已访问 rear = (rear + 1) % N; // 队尾指针增1 queue[rear] = p->adjvex; // 访问过的顶点入队 } p = p->nextarc; // 找v的下一个邻接点 } } }
【实现二:示例代码】
/* 广度优先搜索算法(BFS)的第二个实现(Bfs函数): 利用了链队列LkQueue来实现队列结构 */ int visited[N] = {0}; // 对访问标记visited数组初始化 int queue[N]; // 队列queue存放已访问过的顶点 void Bfs(Graph g, int v) { LkQueue Q; // Q为链队列 int j; InitQueue(&Q); // 初始化队列Q printf("%d", v); // v为访问的起始结点 visited[v] = 1; // 访问过的标志 EnQueue(&Q, v); // 起始结点入队列 while (!EmptyQueue(Q)) // 判断队列是否为空 { v = Gethead(&Q); // 获取队头结点 OutQueue(&Q); // 出队列 for (j = 0; j < n; j++) // n为顶点数,依次尝试v的可能邻接点 { m = g->arcs[v][j]; // 获取v和j之间的边 if (m && !visited[j]) // 判断是否为邻接点,且未被访问 { printf("%d", j); // 访问此邻接点 visited[j] = 1; // 标记为已访问 EnQueue(&Q, j); // 邻接点入队列 } } } }
【示意图】
【实现一代码详解】
- 此代码实现了广度优先搜索算法,使用邻接表来表示图,并通过使用一个队列来存放已访问过的顶点。
- 请注意,此代码中的 Graph 类型以及 ArcNode 类型没有给出,可以根据自己的需要定义这些类型。
- 首先定义了一个标记顶点是否被访问的 visited 数组和一个存放已访问过的顶点的队列queue。
- bfs 函数是从起始顶点 v 开始进行广度优先搜索。
- 首先打印并访问起始顶点 v,并将其标记为已访问。
- 初始化队列的头尾指针。
- 将起始顶点 v 入队。
- 在队列不为空的情况下,进行循环。
- 从队列头部出队一个元素,并将其赋值给 v。
- 获取顶点 v 的边表的头指针。
- 在顶点 v 的邻接点中进行搜索。
- 如果邻接点未被访问,则访问该邻接点,将其标记为已访问,并将其入队。
- 继续搜索下一个邻接点。
- 最后,当队列为空时,广度优先搜索结束。
【实现二代码详解】
- 该代码实现了广度优先搜索算法的另一种形式,通过链队列(LkQueue)实现队列结构。
- 请注意,这里的 Graph 类型以及 LkQueue 类型没有给出,可以根据自己的需要定义这些类型。
- 首先定义了一个标记顶点是否被访问的 visited 数组和一个用来存放已访问过的顶点的队列 queue。
- Bfs 函数是从起始顶点v开始进行广度优先搜索。
- 创建一个链队列 Q,并初始化该队列。
- 打印并访问起始结点 v,并将其标记为已访问。
- 将起始结点 v 入队列 Q。
- 当队列 Q 非空时,进行循环。
- 获取队列 Q 的头结点,并将其赋值给 v。
- 出队列 Q,将队头结点移除队列。
- 对于顶点 v 的所有可能邻接点,依次尝试。
- 判断j是否为 v 的邻接点且未被访问。
- 如果是邻接点且未被访问,则打印并访问该邻接点,并将其标记为已访问。
- 将该邻接点入队列 Q。
- 最后,当队列 Q 为空时,广度优先搜索结束。
【区别】
(4)应用举例 —— 求图的连通分量
① 判断图的连通性
对图 G 调用一次 DFS 或 BFS ,得到一顶点集合,然后将 之与 V(G) 比较,若两集合相等,则图 G 是连通图,否则就 说明有未访问过的顶点,因此图不连通。
② 求图的连通分量
【说明】求图的连通分量 ← 图遍历的一种应用
- 从无向图的每个连通分量的一个顶点出发遍历,则可求得无向图的所有连通分量。
【算法代码】
void trace(Graph G) { /* G为用邻接矩阵或邻接表表示的有n个顶点的无向图,求该图的连通分量 */ int i; for (i = 0; i < N; ++i) // 遍历所有顶点 { if (!flag[i]) { dfs(i); // 调用DFS算法找到当前未访问过的顶点的连通分量 /* 调用DFS算法的次数仅决定于连通分量个数 */ OUTPUT; // 输出访问到的顶点和依附于这些顶点的边,得到一个连通分量 } } } /* trace */
【代码详解】
- 这段代码实现了求无向图连通分量的函数
trace
。- 请注意,代码中使用的全局变量
flag
以及函数dfs
和OUTPUT
并没有给出具体实现,可以根据需要自行定义。
- 函数参数
G
表示用邻接矩阵或邻接表表示的无向图,有n
个顶点。- 使用一个循环遍历所有顶点。
- 判断顶点是否被访问过,若未被访问过,则调用深度优先搜索算法
dfs
。- 调用
dfs
算法的次数仅决定于连通分量的个数。- 在查找连通分量的过程中,可以输出访问到的顶点及其依附的边,得到一个连通分量。
- 最后,函数
trace
结束。
四、图的应用
(1)生成树
① 生成树定义
生成树定义 :连通图 G=(V,E) ,从任一顶点 遍历,则图中边分成两部分:
- 深度优先生成树:按深度优先遍历而得的生成树
- 广度优先生成树:按广度优先遍历而得的生成树
② 示例
【示例】
【其深度优先生成树为】
【其广度优先生成树为】
【注意】
- 生成树 G’ 是图 G 的极小连通子图。 即 V(G)=V(G’),G’ 是连通的,且在 G 的所有连通子图中边数最少(n 个顶点,n-1 条边)。
- 图的生成树不是唯一的。
(2)最小生成树
① 问题的提出
② 最小生成树定义
给定一个带权图,构造带权图的一棵生成树, 使树中所有边的权总和为最小。
③ 最小生成树的构造算法
- Prim 算法
- 克鲁斯卡尔 kruskal 算法
④ 最小生成树的构造方法(Prim 法)
【基本思想】假设 G=(V,E) 是一个无向带权图,生成的最小生成树为 MinT=(V,T),其中 V 为顶点的集合,T 为边的集合。求 T 的步骤如下:
- 初始化 U={u0 },T={ };其中 U 为一个新设置的顶点的集合,初始 U 中只含有顶点 u0,这里假设在构造最小生成树时,从顶点 u0 出发
- 对所有 u∈U,v∈V-U (其中 u,v 表示顶点) 的边 (u,v) 中,找一条权最小的边 (u’,v’),将这条边加入到集合 T 中,将顶点 v’ 加入到集合 U 中
- 如果 U=V,则算法结束;否则重复第 2、3 步
【说明】最后得到最小生成树 MinT=<V,T>,其中 T 为最小生成树的边的集合
【示例】对下图用 Prim 法构造最小生成树:
【示例代码】最小生成树的构造方法(Prim 法):
- 适合于求边稠密的带权图的最小生成树。
- 设 G=(V,E)是个无向带权图,U 是最小生成树的顶点集合,T 是最小生成树的边集合,则 Prim 算法描述如下:
Prim(Graph G) { /* 构造图G的最小生成树 */ // 从G中任选一顶点p∈V 选取一个顶点 p ∈ V; U = { p }; // U集合存放已经加入最小生成树的顶点 T = { }; // T集合存放最小生成树的边 while (U ≠ V) // 当 U 集合不等于 V 集合时 { // 在 p ∈ U,q ∈ V-U 中找一条权最小的边 (p,q) 找到一条权重最小的边 (p, q),其中 p ∈ U,q ∈ V-U; U = U + { q }; // 将 q 加入到 U 集合中 T = T + {(p, q)}; // 将边 (p, q) 加入到 T 集合中 } } /* Prim */
【代码详解】
- 这段代码实现了 Prim 算法来构造图 G 的最小生成树。
- 从图 G 中任选一个顶点 p 作为初始顶点,该顶点属于顶点集合 V。
- 初始化 U 集合为包含初始顶点 p 的集合,用来存放已经加入最小生成树的顶点。
- 初始化 T 集合为空集合,用来存放最小生成树的边。
- 进入循环:当 U 集合不等于 V 集合时,说明还存在未加入最小生成树的顶点。
- 在顶点 p 属于 U 集合的情况下,在 V-U 集合中找到一条权重最小的边 (p, q)。
- 将顶点 q 加入到 U 集合中,表示将顶点 q 加入最小生成树。
- 将边 (p, q) 加入到 T 集合中,表示将该边加入最小生成树的边集合。
- 继续下一次循环,直到 U 集合等于 V 集合,即所有顶点都已加入最小生成树。
- 最终得到的 T 集合即为图 G 的最小生成树。
- 在代码中,需要注意的是:
- 需要根据具体的实现来获得顶点集合 V、权重信息和具体的边。
- 需要合适的数据结构来处理 U 集合和 T 集合,例如集合、数组或链表。
- 需要选择合适的算法来找到权重最小的边,例如遍历所有边或使用最小堆等数据结构来快速获取最小权重边。
【示意图】
⑤ 最小生成树的构造方法(Kruskal 克鲁斯卡尔法 )
【基本思想】假设 G=(V,E) 是一个无向带权图,生成的最小生成树为 MinT=(V,T),其中 V 为顶点的集合,T 为边的集合。求 T 的步骤如下:
设 G=(V,E), 令最小生成树初始状态为只有 n 个顶 点而无边的非联通图 T= ( V , { } ),每个顶点 自成一个连通分量 在 E 中选取权值最小的边,若该边依附的顶点 落在 T 中不同的连通分量上,则将此边加入到 T 中,否则,舍去此边,选取下一条权值最小的 边 以此类推,重复第 2 步 ,直至 T 中所有顶点都在同一 连通分量上为止【说明】用 Kruskal 方法构造的最小生成树不唯一,但权和相同。
【示例】对下图用 Kruskal 法构造最小生成树:
【示例代码】最小生成树的构造方法(Kruskal 法):
- 适合于求边稀疏的网的最小生成树。
原则: 按权值递增次序构造 T min ; 即每次选权最小且不构成回路的边 , 直至 n-1 条。- Kruskal 方法形式描述:
Kruskal(Graph G) { /* 构造图G的最小生成树 */ n = G的顶点数; V(T) = V(G); E(T) = {}; // T初始化为n个顶点而无边的图 while (边数(E(T)) < n-1) // 当E(T)中边数小于n-1时 { 从E(G)中选择最小权的边 (v,w); 从E(G)中删去边 (v,w); if ((v,w)加到T中不形成回路) { 将边 (v,w) 加入T中; } } } /* Kruskal */
【代码详解】
- 这段代码实现了 Kruskal 算法来构造图 G 的最小生成树。
- 请注意,代码中使用的函数
边数(E(T))
和判断(v,w)加到 T
中不形成回路
并没有给出具体实现,可以根据需要自行定义。
- 函数参数
G
表示要构造最小生成树的图。- 特定变量
n
表示图 G 的顶点数。- 初始化 T 图,使其有图 G 相同的顶点集合
V(T)
,但没有边E(T)
。- 循环直到 T 图中边数达到
n-1
时,执行以下步骤。- 从边集合
E(G)
中选择最小权重的边(v,w)
。- 从边集合
E(G)
中移除边(v,w)
。- 判断将边
(v,w)
加入 T 图是否会形成回路。- 如果将边
(v,w)
加入 T 图不会形成回路,则将该边加入 T 图。- 继续下一次循环,直到 T 图中边数达到
n-1
,即构造出最小生成树。【算法时间复杂度】O(eloge)
【示意图】
(3)拓扑排序
① 问题的提出
② 拓扑排序定义
【定义】
- 若有向图 G 中,顶点表示活动或任务,边表示活动或任务之间的优先关系,则称 G 为 AOV网络,即顶点表示活动的网络 (Activity on vertex network)。
- 对 AOV 网构造顶点线性序列 (…i,…,k,…j,…) i 是 j 的前趋,则 i 在 j 之前,若 i、k 间无路径,则或 i 在 k 前,或 k 在 i 前,这样的线性序列称为拓扑有序序列。
- 拓扑有序序列的构造过程称为拓扑排序。
③ 拓扑排序方法
【过程】
- 在 AOV 网中选一个无前趋的顶点并输出之
- 从 AOV 网中删去该顶点及以它为尾的所有弧
- 重复第 1、2 步直至没有无前趋的顶点为止
【结果】
④ 拓扑排序算法
【物理表示】AOV网的物理表示:AOV网用邻接表表示,且在顶点表中为每个顶点增加一个存放顶点入度的域in,以指示各顶点当前的入度值,即:
【基本思想】拓扑排序算法的基本思想:为避免重复查找,可将入度为 0 的顶点入度域串链成一个链式栈。
1. 将全体入度为 0 的顶点入栈
2. 链栈非空时,反复执行:
- 弹出栈顶元素 Vj 并将其输出
- 检查 Vj 的出边表,将每条出边(Vj,Vk)的终点 Vk 的入度域减 1
- 若 Vk 的入度为 0,则 Vk 入栈
3. 若输出的顶点数小于 N,则输出有回路;否则,拓扑排序结束