【数据结构导论】第 5 章:图

news2024/12/28 4:14:05

目录

一、图的基本概念

(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) = { 1234 }
  • E(G1) = { (1,2)(1,3)(1,4)(2,3)(2,4)(3,4) }

【示例 2 】  

  • V(G2) = { 1234 567 }
  • E(G2) = { (1,2)(1,3)(2,4)(2,5)(3,6)(3,7) }  

【示例 3 】 

  • V(G3) = { 123 }
  • E(G3) = { <1,2><2,1><2,3> } 

(2)图的基本术语 

顶点 (Vertex) 图中的数据元素 

<Vi ,Vj有向图中顶点 V到 顶点 V的边也称

  • Vi弧尾(初始点)—— 无箭头端
  • Vj 弧头(终端点)—— 箭头端

 完全图 :(顶点数 n)

  • 无向完全图边数 n*(n-1)/2 的无向图
  • 有向完全图边数 n*(n-1) 的有向图

权 :与图中的边相关的数 

子图 : 图  和  G′ , 若有  V(G′)  ⊆ V(G) E(G′) ⊆ E(G) 则称 G′  为图  的子图

 

邻接 :若 (Vi ,Vj ) ∈ E(G),则称 V和 V互为邻接点 

关联 :若 (Vi ,Vj ) ∈ E(G),则称边 (Vi ,Vj 关联于顶点 Vi 和 Vj

  • 邻接是指顶点之间的关系,而关联是指边与顶点间的关系
  • 若弧 <Vi ,Vj> ∈ E(G),则称 Vj 是 Vi 的邻接点

度 :

● 无向图 D(Vi) :顶点 V的度为与 V相关联的边的个数 

●  有向图:
  • 出度 OD(Vi) :顶点 V的出度为以 V为尾的出边数
  • 入度 ID(Vi) :顶点 V的入度为以 V为头的入边数
  • 度 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 到  V 顶点有路径,则 称  V 和  V 是连通的

连通图  和 连通分量 :针对无向图而言

强连通图  强连通分量 :针对有向图而言

生成树 : 含有该连通图的全部顶点的一个 连通子图

若连通图 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 阶方阵:

【结论】  

  1. 无向图的邻接矩阵是对称矩阵 (∵ (Vi ,Vj ) ∈ E(G),则 (Vj ,Vi ) ∈ E(G) )
  2. 从邻接矩阵容易判断任意两顶点间是否有边相联容易求出各顶点的度
  • 无向图:顶点 V的度 D(Vi ) = 矩阵中第 行或第 列元素之和
  • 有向图:OD (Vi) = 矩阵中第 i 行元素之和  ; ID (Vi) = 矩阵中第 列元素之和

② 带权图(网)的邻接矩阵 

③ 邻接矩阵的类型定义 

【类型定义】

const int vnum = 20;

typedef struct gp
{
    VertexType vexs[vnum];         // 顶点信息数组
    WeightType arcs[vnum][vnum];   // 邻接矩阵数组
    int vexnum, arcnum;            // 顶点数,边数
} WGraph;

【代码详解】 

  • 通过上述代码,定义了一个名为 WGraph 的结构体类型,用于表示带权有向图。
  • WGraph 结构体内部包含了顶点信息数组 vexs 和邻接矩阵数组 arcs,以及两个整型成员变量 vexnum 和 arcnum,分别表示顶点数和边数。
  • 这个结构体可以用来表示和存储带权有向图的相关信息。
  1. const int vnum = 20;: 使用 const 关键字定义了一个名为 vnum 的整型常量,并将其值设置为 20。这个常量 vnum 的值是不可修改的。

  2. typedef struct gp { ... } WGraph;: 使用 typedef 关键字定义了一个结构体类型,名为 gp。结构体中包含了 VertexType 类型的 vexs 数组、WeightType 类型的 arcs 二维数组,以及两个整型成员变量 vexnum 和 arcnum。最后通过 typedef 定义了别名 WGraph,用于表示该结构体类型。

  3. VertexType vexs[vnum];: 结构体中的 vexs 数组是 VertexType 类型的数组,用于存储顶点的信息。

  4. WeightType arcs[vnum][vnum];: 结构体中的 arcs 数组是 WeightType 类型的二维数组,表示顶点之间的邻接关系,用于存储边的权值,构成邻接矩阵。

  5. 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;   // 无向图需要同时更新两个方向的边的权值
    }
}

【代码详解】 

  1. void CreatGraph(Graph* g): 定义了一个名为 CreatGraph 的函数,参数为指向 Graph 结构体的指针 g。函数类型为 void,即没有返回值。

  2. scanf("%d %d", &n, &e);: 使用 scanf 函数从用户输入中读取两个整数,分别赋值给变量 n 和 e,即顶点数和边数。

  3. g->vexnum = n; 和 g->arcnum = e;: 将变量 n 的值赋给 g 指向的结构体中的成员变量 vexnum 和 arcnum,即更新图的顶点数和边数。

  4. scanf(" %c", &ch);: 使用 scanf 函数从用户输入中读取一个字符,赋值给变量 ch。这里加一个空格可以忽略输入缓冲区中的回车符。

  5. g->vexs[i] = ch;: 将变量 ch 的值赋给 g 指向的结构体中的顶点信息数组 vexs[i],即更新图的顶点信息。

  6. g->arcs[i][j] = MAX_INT;: 将 MAX_INT 的值赋给 g 指向的结构体中的邻接矩阵数组 arcs[i][j],即将所有边的权值设为最大值。

  7. scanf("%d %d %d", &i, &j, &w);: 使用 scanf 函数从用户输入中读取三个整数,分别赋值给变量 ij 和 w,即边的起点、终点和权值。

  8. 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 个链表的结点数为顶点 V的度 
  • 对于有向图,第 i 个链表的结点数为顶点 V的出度
  • 在边稀疏时,邻接表比邻接矩阵省单元
  • 邻接表表示在检测边数方面比邻接矩阵表示效率要高

④ 邻接表的类型定义 

【类型定义】

#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 结构体表示,其中包含了下一条边的顶点编号、权值和指向下一条边的指针。
  • 这样的数据结构可以方便地表示和操作图的信息,例如可以通过遍历邻接表来获取某个顶点的相邻顶点,或者通过访问边的信息来获取边的权值等。
  • 同时,通过邻接表的方式存储图的信息,可以节省存储空间,特别适用于稀疏图的表示。
  1. #define vnum 20: 使用 #define 定义了一个宏常量 vnum,值为 20。此处使用宏定义可以方便地在代码中引用 vnum,避免硬编码。

  2. typedef struct arcnode { ... } ArcNode;: 使用 typedef 关键字定义了一个结构体类型 ArcNode。结构体中包含了一个整数类型的 adjvex(下一条边的顶点编号)、带权图的权值域 weight(WeightType)、以及一个指向下一条边的指针 nextarc

  3. typedef struct vexnode { ... } AdjList[vnum];: 使用 typedef 关键字定义了一个结构体类型 vexnode。结构体中包含了一个整数类型的 vertex(顶点编号)和一个指向第一条边的指针 firstarc。然后使用 AdjList[vnum] 定义了 AdjList 数组类型,数组大小为 vnum,元素类型为 vexnode

  4. typedef struct gp { ... } Graph;: 使用 typedef 关键字定义了一个结构体类型 gp。结构体中包含了一个 AdjList 数组 adjlist,以及两个整数成员变量 vexnum 和 arcnum(顶点和边的个数)。然后使用 Graph 定义了别名,用于表示该结构体类型。

  5. 通过上述代码,我们定义了以下几个数据结构:

  • ArcNode:表示图中的一条边。它包含了一个整数类型的 adjvex 成员,表示下一条边的顶点编号;一个 WeightType 类型的 weight 成员,表示带权图的权值域;以及一个指向下一条边的指针 nextarc

  • AdjList:表示图的邻接表。它是一个 AdjList[vnum] 数组,数组的每个元素是一个 vexnode 结构体。vexnode 结构体包含一个整数类型的 vertex 成员,表示顶点编号;一个指向第一条边的指针 firstarc

  • Graph:表示整个图的结构。它包含了一个 AdjList 类型的 adjlist 数组,用于存储邻接表信息;以及两个整数类型的 vexnum 和 arcnum 成员,表示顶点数和边数。

⑤ 计算图的度 

对于 无向图,第  个链表的结点数为顶点  V 的度
对于 有向图,第  个链表的结点数只为顶点  V 的出度
  • 若要求入度,必须遍历整个邻接表
  • 在单链表中,其邻接点域的值为 i 的结点个数是顶点 Vi 的入度
对于有向图,有时候就要建立一个你邻接表
  • 即队每个顶点 Vi 建立一个以 Vi 为弧头的邻接点的链表
  • 这样,逆邻接表第 i 个单链表中的结点个数就是 Vi 的入度

⑥ 带权图邻接表 

结点形式:

 

建立有向图的邻接表的方法: 

  • 将邻接表表头数组初始化
  • 第 个表头的 vertex 域初始化为 i
  • first 域初始化为 NULL
  • 读入顶点对 <i,j>,产生一个表结点
  • 将 放入到该结点的 adjvex 
  • 将该结点链到邻接表的表头数组的第 个元素的 first 域上



三、图的遍历 

 


(1)遍历的含义及方法 

① 含义 

图的遍历 : 从图 G 中某一顶点 v 出发,顺 序访问各顶点一次 

② 方法 

方法:为克服顶点的重复访问,设立辅助数组 visited[n]

遍历方法:

  • 深度优先搜索法 —— 类似先序遍历
  • 广度优先搜索法 —— 类似层次遍历

(2)连通图的深度优先搜索(DFS) 

① 过程 

从图  G(V,E)  中任一顶点 V i 开始,首先访问  V ,然后访问  V 一未访问过的邻接点  V j ,再以  V 为新的出发点继续进行深度优先 搜索,直到所有顶点都被访问过。

② 示例 

 

③ 算法 

【分析】
  • 为克服顶点的重复访问,设立一标志向量 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;
    }
}

【代码详解】

  • 此算法通过递归的方式,以深度优先的顺序来遍历图中的所有顶点,并执行相应的操作。
  • 通过标记顶点的访问状态,可以避免重复访问或陷入死循环。
  1. void Dfs(Graph g, int v): 定义了一个深度优先搜索(DFS)的函数,用于遍历图。函数接受两个参数,Graph g 表示图的数据结构,int v 表示从顶点v开始遍历。

  2. visit(v): 在访问某个顶点v时,执行相应的操作。这里省略了具体的访问操作,你可以根据实际需求自行定义。

  3. visited[v] = 1;: 将顶点v标记为已访问,将其对应的 visited 数组的值设置为1。在函数开始时,visited 数组需要初始化为0,表示所有顶点都未被访问过。

  4. int w = g.adjlist[v].firstarc;: 找出顶点v的第一个邻接点w。通过 g.adjlist[v] 获取顶点v的邻接表,然后访问其 firstarc 成员,即可得到顶点v的第一个邻接点。

  5. while (w存在) { ... }: 通过一个循环来遍历顶点v的所有邻接点w。当w存在时,执行循环内部的操作。

  6. if (w 未被访问) { Dfs(g, w); }: 判断邻接点w是否未被访问过。如果w未被访问过,则递归调用Dfs函数,以顶点w作为起点,继续进行深度优先搜索。

  7. 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 数组来标记顶点的访问状态。
  • 在访问每个顶点时,可以执行相应的操作,如打印顶点值。
  1. int visited[N] = {0};: 定义一个大小为N的整型数组 visited,并将所有元素初始化为0。该数组用于标记顶点是否被访问过。

  2. void Dfs(Graph g, int v): 定义了一个深度优先搜索(DFS)的函数,用于遍历图。函数接受两个参数,Graph g 表示图的数据结构,int v 表示从顶点v开始遍历。

  3. printf("%d", v);: 输出当前访问的顶点 v。这里使用 printf 语句打印顶点编号,你可以根据实际需求自行改变输出方式。

  4. visited[v] = 1;: 将顶点 v 标记为已访问,将 visited 数组中 v 对应位置的值设置为1。在函数开始时,visited 数组需要初始化为 0,表示所有顶点都未被访问过。

  5. p = g.adjlist[v].firstarc;: 获取顶点 v 的第一个邻接点 p。通过 g.adjlist[v] 访问顶点 v 的邻接表,然后取得邻接表的 firstarc 成员,即可得到顶点 v 的第一个邻接点。

  6. while (p != NULL) { ... }: 通过一个循环来遍历顶点 v 的邻接点 p。只要 p 不等于NULL,就执行循环内部的操作。

  7. if (!visited[p->adjvex]) { Dfs(g, p->adjvex); }: 判断邻接点 p 是否未被访问过。如果 p 未被访问过,则递归调用 Dfs 函数,以顶点 p->adjvex 作为起点,继续进行深度优先搜索。

  8. 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 数组标记顶点的访问状态,以避免重复访问或陷入死循环。
  1. int visited[N] = {0};: 定义了一个大小为N的整型数组 visited 并将所有元素初始化为 0。用于标记顶点是否被访问过。

  2. void Dfs(Graph g, int v): 定义了一个深度优先搜索(DFS)的函数,用于遍历图。函数接受两个参数,Graph g 表示图的数据结构,int v 表示从顶点 v 开始进行遍历。

  3. printf("%d", v);: 打印当前访问的顶点 v。这里使用 printf 语句输出顶点编号,你可以根据实际需求修改输出内容。

  4. visited[v] = 1;: 将顶点 v 标记为已访问,将 visited 数组中 v 对应位置的值设置为1。在函数开始时,需要将 visited 数组初始化为 0,表示所有顶点都未被访问过。

  5. for (j = 0; j < n; j++) { ... }: 通过一个循环来遍历从顶点v出发的所有邻接点。循环从编号 0 开始,到编号 n-1 结束,依次遍历顶点v的邻接矩阵中的列。

  6. m = g->arcs[v][j];: 获取邻接矩阵中顶点 v 的第 j 列元素的值,通过 g->arcs[v][j] 访问矩阵中的对应位置。

  7. if (m && !visited[j]) { Dfs(g, j); }: 判断顶点 v 与顶点 j 是否邻接,且顶点 j 未被访问过。如果满足条件,则递归调用 Dfs 函数,以顶点j作为起点,继续进行深度优先搜索。

  8. Dfs(g, j);: 递归调用自身的 Dfs 函数,以顶点j作为起点进行深度优先搜索。这是深度优先搜索算法的核心部分,它会一直递归至没有未访问的邻接顶点为止。

【时间复杂度】O(n²)

【区别】


(3)连通图的广度优先搜索法(BFS) 

① 过程 

从图  G(V,E)  中某一点  V 出发,首先访问  V 的所有邻 接点( w 1 w 2 w t ),然后再顺序访问  w 1 w 2 w 的所有未被访问过的邻接点 …., 此过程直到所有 顶点都被访问过。

② 示例 

 

③ 算法 

【分析】  

  • 为克服顶点的重复访问,设立一标志向量 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 类型没有给出,可以根据自己的需要定义这些类型。
  1. 首先定义了一个标记顶点是否被访问的 visited 数组和一个存放已访问过的顶点的队列queue。
  2. bfs 函数是从起始顶点 v 开始进行广度优先搜索。
  3. 首先打印并访问起始顶点 v,并将其标记为已访问。
  4. 初始化队列的头尾指针。
  5. 将起始顶点 v 入队。
  6. 在队列不为空的情况下,进行循环。
  7. 从队列头部出队一个元素,并将其赋值给 v。
  8. 获取顶点 v 的边表的头指针。
  9. 在顶点 v 的邻接点中进行搜索。
  10. 如果邻接点未被访问,则访问该邻接点,将其标记为已访问,并将其入队。
  11. 继续搜索下一个邻接点。
  12. 最后,当队列为空时,广度优先搜索结束。

【实现二代码详解】

  • 该代码实现了广度优先搜索算法的另一种形式,通过链队列(LkQueue)实现队列结构。
  • 请注意,这里的 Graph 类型以及 LkQueue 类型没有给出,可以根据自己的需要定义这些类型。
  1. 首先定义了一个标记顶点是否被访问的 visited 数组和一个用来存放已访问过的顶点的队列 queue。
  2. Bfs 函数是从起始顶点v开始进行广度优先搜索。
  3. 创建一个链队列 Q,并初始化该队列。
  4. 打印并访问起始结点 v,并将其标记为已访问。
  5. 将起始结点 v 入队列 Q。
  6. 当队列 Q 非空时,进行循环。
  7. 获取队列 Q 的头结点,并将其赋值给 v。
  8. 出队列 Q,将队头结点移除队列。
  9. 对于顶点 v 的所有可能邻接点,依次尝试。
  10. 判断j是否为 v 的邻接点且未被访问。
  11. 如果是邻接点且未被访问,则打印并访问该邻接点,并将其标记为已访问。
  12. 将该邻接点入队列 Q。
  13. 最后,当队列 Q 为空时,广度优先搜索结束。

【区别】


(4)应用举例 —— 求图的连通分量 

① 判断图的连通性 

对图  调用一次  DFS  或  BFS ,得到一顶点集合,然后将 之与  V(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 并没有给出具体实现,可以根据需要自行定义。
  1. 函数参数 G 表示用邻接矩阵或邻接表表示的无向图,有 n 个顶点。
  2. 使用一个循环遍历所有顶点。
  3. 判断顶点是否被访问过,若未被访问过,则调用深度优先搜索算法 dfs
  4. 调用 dfs 算法的次数仅决定于连通分量的个数。
  5. 在查找连通分量的过程中,可以输出访问到的顶点及其依附的边,得到一个连通分量。
  6. 最后,函数 trace 结束。


四、图的应用 

(1)生成树 

① 生成树定义 

生成树定义 :连通图  G=(V,E) ,从任一顶点 遍历,则图中边分成两部分:

 

  • 深度优先生成树:按深度优先遍历而得的生成树
  • 广度优先生成树:按广度优先遍历而得的生成树

② 示例 

【示例】 

其深度优先生成树为

 

其广度优先生成树为

【注意】

  • 生成树 G’ 是图 极小连通子图。 即 V(G)=V(G)G’ 是连通的,且在 所有连通子图中边数最少(个顶点,n-1 )。
  • 图的生成树不是唯一的。

(2)最小生成树 

① 问题的提出 

 

② 最小生成树定义 

给定一个带权图,构造带权图的一棵生成树, 使树中所有边的权总和为最小。 

③ 最小生成树的构造算法 

  • Prim 算法
  • 克鲁斯卡尔 kruskal 算法 

④ 最小生成树的构造方法(Prim 法) 

【基本思想】假设 G=(V,E) 是一个无向带权图,生成的最小生成树为 MinT=(V,T),其中 V 为顶点的集合,T 为边的集合。求 T 的步骤如下:

  1. 初始化 U={u0 }T={ };其中 为一个新设置的顶点的集合,初始 中只含有顶点 u0,这里假设在构造最小生成树时,从顶点 u0 出发
  2. 对所有 uUvV-U (其中 u表示顶点的边 (u,v) 中,找一条权最小的边 (u’,v’),将这条边加入到集合 中,将顶点 v’ 加入到集合 
  3. 如果 U=V,则算法结束;否则重复第 2

【说明】最后得到最小生成树 MinT=<V,T>,其中 为最小生成树的边的集合

【示例】对下图用 Prim 法构造最小生成树:

【示例代码】最小生成树的构造方法(Prim 法):

  • 适合于求边稠密的带权图的最小生成树。
  • 设 G=VE)是个无向带权图,是最小生成树的顶点集合,是最小生成树的边集合,则 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 的最小生成树。
  1. 从图 G 中任选一个顶点 p 作为初始顶点,该顶点属于顶点集合 V。
  2. 初始化 U 集合为包含初始顶点 p 的集合,用来存放已经加入最小生成树的顶点。
  3. 初始化 T 集合为空集合,用来存放最小生成树的边。
  4. 进入循环:当 U 集合不等于 V 集合时,说明还存在未加入最小生成树的顶点。
  5. 在顶点 p 属于 U 集合的情况下,在 V-U 集合中找到一条权重最小的边 (p, q)。
  6. 将顶点 q 加入到 U 集合中,表示将顶点 q 加入最小生成树。
  7. 将边 (p, q) 加入到 T 集合中,表示将该边加入最小生成树的边集合。
  8. 继续下一次循环,直到 U 集合等于 V 集合,即所有顶点都已加入最小生成树。
  9. 最终得到的 T 集合即为图 G 的最小生成树。
  • 在代码中,需要注意的是:
  1. 需要根据具体的实现来获得顶点集合 V、权重信息和具体的边。
  2. 需要合适的数据结构来处理 U 集合和 T 集合,例如集合、数组或链表。
  3. 需要选择合适的算法来找到权重最小的边,例如遍历所有边或使用最小堆等数据结构来快速获取最小权重边。

【示意图】

 

⑤ 最小生成树的构造方法(Kruskal 克鲁斯卡尔法 ) 

【基本思想】假设 G=(V,E) 是一个无向带权图,生成的最小生成树为 MinT=(V,T),其中 V 为顶点的集合,T 为边的集合。求 T 的步骤如下:

  1. 设  G=(V,E), 令最小生成树初始状态为只有  个顶 点而无边的非联通图  T= V { } ),每个顶点 自成一个连通分量
  2. 在  中选取权值最小的边,若该边依附的顶点 落在  中不同的连通分量上,则将此边加入到  T 中,否则,舍去此边,选取下一条权值最小的
  3. 以此类推,重复第  2 步 ,直至  中所有顶点都在同一 连通分量上为止

【说明】用 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 中不形成回路 并没有给出具体实现,可以根据需要自行定义。
  1. 函数参数 G 表示要构造最小生成树的图。
  2. 特定变量 n 表示图 G 的顶点数。
  3. 初始化 T 图,使其有图 G 相同的顶点集合 V(T),但没有边 E(T)
  4. 循环直到 T 图中边数达到 n-1 时,执行以下步骤。
  5. 从边集合 E(G) 中选择最小权重的边 (v,w)
  6. 从边集合 E(G) 中移除边 (v,w)
  7. 判断将边 (v,w) 加入 T 图是否会形成回路。
  8. 如果将边 (v,w) 加入 T 图不会形成回路,则将该边加入 T 图。
  9. 继续下一次循环,直到 T 图中边数达到 n-1,即构造出最小生成树。

【算法时间复杂度】O(eloge)

【示意图】

  


(3)拓扑排序 

① 问题的提出 

 

 

② 拓扑排序定义 

【定义】

  • 若有向图 中,顶点表示活动或任务,边表示活动或任务之间的优先关系,则称 为 AOV网络,即顶点表示活动的网络 (Activity on vertex network)

  • 对 AOV 网构造顶点线性序列 i,,k,j,是 的前趋,则 在 之前,若 i间无路径,则或 在 前,或 在 前,这样的线性序列称为拓扑有序序列
  • 拓扑有序序列的构造过程称为拓扑排序

③ 拓扑排序方法 

【过程】 

  1. 在 AOV 网中选一个无前趋的顶点并输出之
  2. 从 AOV 网中删去该顶点及以它为尾的所有弧
  3. 重复第 1、2 步直至没有无前趋的顶点为止

【结果】

 

 

④ 拓扑排序算法 

【物理表示】AOV网的物理表示:AOV网用邻接表表示,且在顶点表中为每个顶点增加一个存放顶点入度的域in,以指示各顶点当前的入度值,即:

 

【基本思想】拓扑排序算法的基本思想:为避免重复查找,可将入度为 的顶点入度域串链成一个链式栈。

1. 将全体入度为 0 的顶点入栈 

2. 链栈非空时,反复执行:

  • 弹出栈顶元素 Vj 并将其输出
  • 检查 Vj 的出边表,将每条出边(Vj,Vk)的终点 Vk 的入度域减 1
  • 若 Vk 的入度为 0,则 Vk 入栈

3. 若输出的顶点数小于 N,则输出有回路;否则,拓扑排序结束

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/748034.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【UE4】在控件蓝图上播放视频

UE版本&#xff1a;4.26 在上一篇文章中&#xff08;【UE】场景内播放视频、音频&#xff09;介绍了如何在场景中播放视频&#xff0c;本篇文章将介绍如何在UI上播放视频 效果 步骤 1. 首先在“Content”文件夹中新建一个名为“Movies”的文件夹 2. 在文件夹中随便添加一个.…

iManager for K8S 站点定制(以MongoDB为例)

作者&#xff1a;ls 目录 背景前期准备实现效果实现过程附录YAML中的属性配置占位符列表 背景 SuperMap iManager支持一键创建用户定制的站点&#xff0c;可将已添加的站点模板创建为站点环境&#xff0c;并通过站点使用应用。   定制站点与其他站点相同&#xff0c;在监管方…

学习PostgreSQL的优势

学习 PostgreSQL 可以为您打开许多就业机会。 PostgreSQL 是一种强大的关系型数据库管理系统&#xff0c;被广泛用于企业和组织中的数据管理和应用程序开发。 以下是一些学习 PostgreSQL 可能帮助您找到的工作领域&#xff1a; **1.数据库管理员&#xff1a;**作为 PostgreSQ…

负载均衡详解

负载均衡可以简单分为服务端负载均衡和客户端负载均衡这两种。 根据 OSI 模型&#xff0c;服务端负载均衡还可以分为&#xff1a; 二层负载均衡三层负载均衡四层负载均衡七层负载均衡 最常见的是四层和七层负载均衡 四层负载均衡 工作在 OSI 模型第四层&#xff0c;也就是传…

TIA博途中FC或FB块被多次调用后,监控单个块执行情况的具体方法

TIA博途中FC或FB块被多次调用后,监控单个块执行情况的具体方法 本文以简单的电机启保停程序为例进行说明: 如下图所示,首先添加一个“启保停”FC块,定义块的接口变量,并编写梯形图程序, 如下图所示,在PLC数据类型中添加一个motorControl数据类型,其中包含start、stop…

Python replace()函数使用详解,Python替换字符串

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;小白零基础《Python入门到精通》 replace函数使用详解 1、不改变原字符串2、指定替换次数3、转义符4、替换列表、元…

opencv 基础图像操作-彩色图像

opencv 基础图像操作-彩色图像 彩色图像 相比二值图像和灰度图像&#xff0c;彩色图像是更常见的一类图像&#xff0c;它能表现更丰富的细节信息。 神经生理学实验发现&#xff0c;在视网膜上存在三种不同的颜色感受器&#xff0c;能够感受三种不同的颜色&#xff1a;红色、绿色…

边缘计算在智慧校园应用,实现校园智能化管理

随着科技的发展和互联网技术进步&#xff0c;校园管理正逐步实现数字化、智能化转型。边缘计算作为一种新兴技术&#xff0c;通过在离数据源较近的地方进行数据处理&#xff0c;实现了实时性分析与响应&#xff0c;为校园带来了更智能、安全的管理方式。 学生学习状态监控 AI动…

LCD1602屏幕简介(全网最详细教程)

目录 1.接线说明 2.LCD1602显示原理 3.LCD1602时序分析 4.LCD1602显示一个字符 5.LCD1602显示一行 1.接线说明 第1引脚&#xff1a;GND为电源地 第2引脚&#xff1a;VCC接5V电源正极 第3引脚&#xff1a;V0为液晶显示器对比度调整端&#xff0c;接正电源时对比度最弱&…

关于海外的Apple搜索广告

随着Apple平台成为大多数应用的服务支柱&#xff0c;我们需要比以往任何时候都更加关注iOS搜索广告&#xff0c;从而成功与用户建立联系。Apple Search Ads能够通过搜索为我们的应用带来流量&#xff0c;让用户在App Store中输入相关关键词时能够高效、简单地发现应用。 Apple …

超详细的学习笔记:CSS浮动(附代码示例)

笔记参考b站网课&#xff1a;【前端开发入门教程&#xff0c;web前端零基础html5 css3前端项目视频教程】https://www.bilibili.com/video/BV1Kg411T7t9?p124&vd_source06e5549bf018e111f4275c259292d0da 目录 一、结构伪类选择器 二、伪元素 三、标准流 四、浮动 1、…

WEB APIs day3 (1)

一、表单全选反选案例 <!DOCTYPE html><html><head lang"en"><meta charset"UTF-8"><title>全选反选案例</title><style>* {margin: 0;padding: 0;}table {border-collapse: collapse;border-spacing: 0;border…

【ROS2】仿真入门

一、说明 在机器人项目中,仿真是一个具有多种用途的重要方面。首先,您可以测试希望机器人执行的行为代码。其次,您可以使用仿真来测试不同类型的硬件,例如距离传感器、相机或 3D 点云传感器,看看哪种效果最好。第三,可视化模拟的相同软件可以与真正的机器人实时使用,在机…

基于51单片机和proteus的智能垃圾桶系统

此系统是基于51单片机和proteus的仿真设计&#xff0c;功能如下&#xff1a; 1. LCD1602实时显示系统状态。 2. 超声波测距模拟检测人体靠近垃圾桶 3. 舵机模拟开启或关闭垃圾桶桶盖。 4. 垃圾桶满溢后报警指示。 5. LED指示人体状态满溢状态及系统状态。 功能框图如下&am…

BTP Integration Suite学习笔记 - (Unit2) Developing with SAP Integration Suite

BTP Integration Suite学习笔记 - (Unit1) Developing with SAP Integration Suite 这章内容比较大而空 Unit2 iPaaS介绍 2.1 SAP的集成策略 这张图应该不陌生&#xff0c;很多地方都可以看到&#xff0c;SAP对于智能企业的集成策略。它有四个原则&#xff1a; Predefined in…

剑指 offer 动态规划算法题:斐波那契数列(青蛙普通跳台阶)

题目描述&#xff1a;写一个函数&#xff0c;输入 n &#xff0c;求斐波那契&#xff08;Fibonacci&#xff09;数列的第 n 项&#xff08;即 F(N)&#xff09;。斐波那契数列的定义如下&#xff1a;F(0) 0, F(1) 1&#xff0c;F(N) F(N - 1) F(N - 2), 其中 N > 1.斐…

wordpress 导航栏 调用

环境&#xff1a;wordpress6、twentytwentyone模板 一、wp-content/themes/twentytwentyone/functions.php 添加以下代码&#xff1a; 1、注册 (左边是别名&#xff0c;右边是名称。别名会用在导航栏的调用上&#xff0c;名称则显示在菜单后台页面上&#xff1a;外观->菜单…

解密Sketch文件打开秘籍:简单两步操作!

虽然Figma&#xff0c;sketch,xd都很好用&#xff0c;但是设计师在设计工作流中经常会遇到无法在这三者软件中自由导入导出的情况。但是只要我们转变一下思路&#xff0c;因为这三种软件都支持导入sketch格式,所以我们只要将文件格式转成sketch&#xff0c;就能自由的在不同软件…

Visual Studio 新功能:Include 语句清理

Visual Studio 17.7 预览版 3 引入了一项新功能&#xff0c;用来提升开发者的生产力。我们很高兴地宣布这项新功能&#xff1a;Include 语句清理&#xff0c;这是一个帮助你维护干净代码的工具。如需使用此功能&#xff0c;请确保更新到最新版本的 Visual Studio 预览版。 Inc…

macOS 14 Sonama - 小记

文章目录 Sonoma 官方资讯关于 Sonama 命名关于 壁纸Sonoma 官方资讯 macOS Sonoma Preview https://www.apple.com/hk/en/macos/sonoma-preview/官方视频介绍 Apple Events --> Watch the Keynote --> 00:43:13 (约14min) https://www.apple.com/hk/en/apple-events/mac…