5.1.2 图的做题方法

原始讲义 · 非增强校订版 · 原书检索 · 文字用于定位,公式与图表请回原页核验 · 本次导入快照

## 5.1.2 图的做题方法

图的题是所有题目里面最简单的,重点复习遍历,其次可以复习拓扑排序,其他的图算法考察概率比较小。对于这些算法,建议以理解实现思路为主,知道对应算法由几步构成,每一步实现什么功能。在题目中如果遇到,可以先在草稿纸上列出流程,理清思路后再直接在试卷上分块作答:

原资料配图
原资料配图 · 转写内容请回源核验

## 5.2 图的结构和遍历

在处理图的问题时,首先要确定其存储结构。考试中主要考察两种:邻接矩阵和邻接表。邻接表可以进一步演化为适用于无向图的邻接多重表和适用于有向图的十字链表。

<table><tr><td rowspan=1 colspan=1></td><td rowspan=1 colspan=1>邻接矩阵</td><td rowspan=1 colspan=1>邻接表</td><td rowspan=1 colspan=1>邻接多重表</td><td rowspan=1 colspan=1>十字链表</td></tr><tr><td rowspan=1 colspan=1>有向图</td><td rowspan=1 colspan=1>√</td><td rowspan=1 colspan=1>√</td><td rowspan=1 colspan=1>x</td><td rowspan=1 colspan=1>√</td></tr><tr><td rowspan=1 colspan=1>无向图</td><td rowspan=1 colspan=1>J</td><td rowspan=1 colspan=1>√</td><td rowspan=1 colspan=1>√</td><td rowspan=1 colspan=1>x</td></tr></table>

<!-- page 135 -->

## 5.2.1 邻接矩阵

原资料配图
原资料配图 · 转写内容请回源核验
原资料配图
原资料配图 · 转写内容请回源核验

有向图G1

无向图G2

邻接矩阵结构:

<!-- verified-by-hand -->
> [!success] 已结合原文第 135 页及 Markdown 上下文人工校对
> 邻接矩阵结构体缺少左花括号。
#define MAXV 100
typedef struct
{
    int numVertices, numEdges;
    char VerticesList[MAXV];
    int Edge[MAXV][MAXV];
} MGraph;
//最大顶点数//图的定义 //图中实际的顶点数和边数// 可选:每个顶点名称//邻接矩阵 邻接矩阵通常使用0表示不存在边,Edge[i][j] = 0 说明i到j之间不存在边。 无权图使用1表示存在边,有权图使用非0的数表示存在边。 ## 邻接矩阵的遍历:
void TraverseEdges_MGraph(MGraph G)
{
    for (int i = 0; i &lt; G.numVertices; i++)
    for (int j = 0; j &lt; G.numvertices; j++)
    if (G.Edge[i][j] != 0)
    visit(i, j); // 如果i→j存在边则处理
}
## 5.2.2 邻接表
原资料配图
原资料配图 · 转写内容请回源核验

有向图G1邻接表结构:

原资料配图
原资料配图 · 转写内容请回源核验

无向图G2

原资料配图
原资料配图 · 转写内容请回源核验
原资料配图
原资料配图 · 转写内容请回源核验

#define MAXV 100
//边结点
typedef struct ArcNode
//最大顶点数 <!-- page 136 --> <!-- verified-by-hand --> > [!success] 已结合原文第 136 页及 Markdown 上下文人工校对 > 两个结构体均缺少左花括号并混入水印。
typedef struct ArcNode
{
    int adjvex;
    struct ArcNode *next;
    int weight;
} ArcNode;
typedef struct
{
    ArcNode *VerticesList[MAXV];
    int numVertices, arcnum;
} ALGraph;
<!-- verified-by-hand --> > [!success] 已结合原文第 136 页及 Markdown 上下文人工校对 > 代码块开头缺 typedef struct ArcNode 和左花括号,ALGraph 也缺左花括号。
typedef struct ArcNode
{
    int adjvex;
    struct ArcNode *next;
    int weight;
} ArcNode;
typedef struct VNode
{
    char data;
    ArcNode *first;
} VNode;
typedef struct
{
    VNode VerticesList[MAXV];
    int numVertices, numEdges;
} ALGraph;
请大家思考一个问题:顶点VNode 定义中,如果我们不需要用到顶点信息,是不是可以省略这部分定义,从而整个邻接表结构可以写的更简单,该写法可能不太常用,但大家如果看到需要认识(习题中有出现): ## 邻接表的遍历:
void TraverseEdges_ALGraph(ALGraph G)
{
    // 遍历所有顶点
    for (int i = 0; i &lt; G.numVertices; i++)
    {
        //遍历当前顶点i发出的所有边
        ArcNode *p = G.VerticesList[i].first;
        while (p != NULL)
        {
            visit(p); // 访问对应边
            p = p-&gt;next; //处理下一条边
        }
    }
}
<!-- page 137 --> ## 5.2.3 \*邻接多重表
原资料配图
原资料配图 · 转写内容请回源核验

无向图G2

邻接多重表结构:

<!-- verified-by-hand -->
> [!success] 已结合原文第 137 页及 Markdown 上下文人工校对
> EBox 与 AMLGraph 均缺少左花括号,类型名大小写不一致。
#define MAXV 100
typedef struct EBox
{
    bool mark;
    int ivex, jvex;
    struct EBox *ilink, *jlink;
    int weight;
} EBox;
typedef struct VexBox
{
    char data;
    EBox *first;
} VexBox;
typedef struct
{
    VexBox VerticesList[MAXV];
    int numVertices, numEdges;
} AMLGraph;
## 邻接多重表的遍历: <!-- verified-by-hand --> <!-- code-audit: false_positive --> > [!info] 已人工审读:该函数因 PDF 分页和插图被拆成 page 137、138 两个连续代码块;与下一块拼接后花括号平衡且逻辑完整,单块 brace 告警是分页造成的误报。
void TraverseEdges_AMLGraph(AMLGraph G)
{
    for (int i = 0; i &lt; G.numVertices; i++)
    {
原资料配图
原资料配图 · 转写内容请回源核验

//标记是否访问
//该边依附的两个顶点下标
// 指向 ivex 和 jvex 的下一条边
//可选:边权值

//顶点信息//指向第一条边

//顶点表//顶点数、边数

<!-- page 138 -->

<!-- verified-by-hand -->
<!-- code-audit: false_positive -->
> [!info] 已人工审读:这是 page 137 函数的分页续块;与上一代码块拼接后花括号平衡且逻辑完整,单独检查续块才会出现闭括号过多。
        EBox *p = G.VerticesList[i].first;
        while (p != NULL)
        {
            if (!p-&gt;mark)
            {
                visit(p);
                p-&gt;mark = true;
            }
            p = p-&gt;ivex == i ? p-&gt;ilink : p-&gt;jlink;
        }
    }
}
## 5.2.4\*十字链表
原资料配图
原资料配图 · 转写内容请回源核验

(a)有向图

原资料配图
原资料配图 · 转写内容请回源核验

十字链表结构:

(b)十字链表

<!-- verified-by-hand -->
> [!success] 已结合原文第 138 页及 Markdown 上下文人工校对
> 混入 OCR 字符,OLGraph 缺少左花括号。
#define MAXV 100
typedef struct ArcBox
{
    int tailvex, headvex;
    struct ArcBox *hlink, *tlink;
    int weight;
} ArcBox;
typedef struct VexNode
{
    char data;
    ArcBox *firstin, *firstout;
} VexNode;
typedef struct
{
    VexNode VerticesList[MAXV];
    int numVertices, numEdges;
} OLGraph;
//弧尾(起点)和弧头(终点)在顶点表中的下//指向同终点的下一条边(入边链)//指向同起点的下一条边(出边链)// 可选:边权值 //顶点信息 <!-- page 139 --> ## 十字链表的遍历:
void TraverseEdges_OLGraph(OLGraph G)
{
    //遍历所有顶点
    for (int i = 0; i &lt; G.numVertices; i++)
    {
        //遍历当前顶点i发出的所有边
        ArcBox *p = G.VerticesList[i].firstout; // 请注意是出边
        while (p != NULL)
        {
            visit(p); //访问对应边
            p = p-&gt;tlink; //起点相同
        }
    }
}
## 5.3 图的经典算法 图可以有多种定义,因此对于图算法,核心应该是理清: • 功能是什么?适用于什么题目? • 步骤有哪些? ## 5.3.1 深度优先遍历 基本思想:沿着图的某一路径尽可能“深入”,直到不能继续为止,再回溯到上一个结点,尝试探索其他路径。其核心是“一条路走到黑,走不通再退回换条路”。这非常类似于树的先序遍历,也像在走迷宫时,沿着一条路走到死胡同,然后原路返回到上一个岔路口,选择另一条路继续探索。 实现原理:DFS的“后进先出”特性天然与栈这种数据结构相匹配。假设未访问的结点用蓝色,表示平静;已加入递归栈中的用橙黄色,表示正在处理中;已处理完的用绿色,表示已经探索完毕。 ## 算法步骤(递归版) 1. 选择一个起始顶点 v,访问它,并标记为已访问(visited [v] = true )。 <!-- page 140 --> 2. 查找 ν 的所有邻接点 W。 3. 如果邻接点 w 未被访问,则从 w 开始进行深度优先遍历(递归调用 DFS(G,w))。 4. 如果V 的所有邻接点都已被访问,则函数返回,实现回溯。 邻接表的 DFS 代码如下: <!-- verified-by-hand --> > [!success] 已结合原文第 140 页及 Markdown 上下文人工校对 > OCR 丢失 p = 赋值与 DFS 的闭括号,并把 ans 错嵌进 DFS。
bool visited[MAXV] = {false};
void DFS_ALGraph(ALGraph G, int v)
{
    visit(v);
    visited[v] = true;
    ArcNode *p = G.VerticesList[v].first;
    while (p != NULL)
    {
        if (!visited[p-&gt;adjvex])
            DFS_ALGraph(G, p-&gt;adjvex);
        p = p-&gt;next;
    }
}
void ans(ALGraph G)
{
    for (int i = 0; i &lt; G.numVertices; i++)
        if (!visited[i]) DFS_ALGraph(G, i);
}
复杂度分析 假设图有 n 个顶点和 e 条边。 ## • 时间复杂度: 。邻接表:每个顶点和每条边都只会被访问一次,所以时间复杂度为O(n+e)。 。 邻接矩阵:需要遍历矩阵来查找每个顶点的邻接点,所以时间复杂度为O(n²)。 • 空间复杂度:主要取决于递归深度或栈的大小,最坏情况下需要存储所有顶点,所以空间复杂度为O(n)。 ## 应用场景 • 寻找连通分量:在一个非连通图中,可以确定有多少个独立的子图。 • 检测环路:在遍历过程中,如果遇到一个已经访问过且不在当前路径上的祖先结点的顶点,则说明存在环。 • 拓扑排序:用于有向无环图(DAG),可以得到一个线性的任务序列。 • 寻找路径:寻找从一个点到另一个点的路径。 <!-- page 141 --> ## 5.3.2 广度优先遍历 基本思想:相当于树与二叉树的层次遍历。从一个起始顶点开始,首先访问其所有直接相邻的邻接点,然后按距离逐层向外扩展,访问更远的顶点。其核心是“逐层扩散,齐头并进”。这非常类似于树的层次遍历,也像是往平静的水面扔一块石头,水波从中心一圈一圈向外扩散。 IN 实现原理:BFS的“先进先出”特性需要用队列来实现。假设未扫描的结点用蓝色,表示平静;已加入队列的用橙色,表示跃跃欲试待处理;已处理完的用绿色,表示已经探索完毕。
原资料配图
原资料配图 · 转写内容请回源核验

## 算法步骤

1. 选择一个起始(蓝色)顶点ν,将其加入队列,标记为已入队(蓝→橙)。

2. 当队列不为空时,循环执行以下操作:

a. 从队头取出一个顶点 u,访问该结点。

b. 遍历 u 的所有邻接点 w:如果 w 未入队,则访问 w,并将其加入队列,标记为已入队(蓝→橙)。遍历完后u由橙→绿。

邻接表的 BFS 遍历:

bool visited[MAXV] = {false};
void BFS_ALGraph(ALGraph G, int v)
{
    int queue[MAXV], front = 0, rear = 0;
    // 1.将 ν 入队,标记
    queue[rear++] = v;
    visited[v] = true;
    //2.如果队列不为空则循环
    while (front &lt; rear)
    {
        // 2-a.选择队头元素 u 访问
        int u = queue[front++];
        visit(u);
        // 2-b.将 u 出发可到达的所有未访问顶点加入队列
        ArcNode *p = G.VerticesList[u].first;
        while (p != NULL)
        {
            if (visited[p-&gt;adjvex] != true)
            {
                queue[rear++] = p-&gt;adjvex;
                visited[p-&gt;adjvex] = true;
            }
            p = p-&gt;next;
        }
    }
}
void ans(ALGraph G)
{
    // 无向图调用 BFS 次数等于连通分量数
    for (i=0; i &lt; G.numvertices; i++)
    if (visited[i] != true) BFS_ALGraph(G, i);
}
<!-- page 142 --> 复杂度分析:假设图有V个顶点和E条边。 • 时间复杂度: 。邻接表:每个顶点和每条边都只会被访问一次,所以时间复杂度为O(n+e)。 。邻接矩阵:同样需要查找每个顶点的所有邻接点,时间复杂度为O(n²)。 • 空间复杂度:主要取决于队列的大小,最坏情况下需要存储所有顶点,所以空间复杂度为O(n)。 ## 应用场景 • 寻找无权图的最短路径:BFS保证了最先找到目标顶点的路径就是最短的(边数最少)。 • 检测二分图:一个图是二分图,当且仅当它不包含奇数长度的环。我们可以使用BFS对图用两种颜色进行着色。 <table><tr><td rowspan=1 colspan=1>特性</td><td rowspan=1 colspan=1>深度优先遍历 (DFS)</td><td rowspan=1 colspan=1>广度优先遍历(BFS)</td></tr><tr><td rowspan=1 colspan=1>数据结构</td><td rowspan=1 colspan=1>栈</td><td rowspan=1 colspan=1>队列</td></tr><tr><td rowspan=1 colspan=1>搜索方式</td><td rowspan=1 colspan=1>莽夫,无路可走再回溯</td><td rowspan=1 colspan=1>谨慎,一层层前进麦研</td></tr><tr><td rowspan=1 colspan=1>空间复杂度</td><td rowspan=1 colspan=1>O(n)</td><td rowspan=1 colspan=1>O(n)</td></tr><tr><td rowspan=1 colspan=1>时间复杂度</td><td rowspan=1 colspan=1>邻接表 O(n+e),邻接矩阵 O(n²)</td><td rowspan=1 colspan=1>邻接表 O(n+e),邻接矩阵 O(n²)</td></tr><tr><td rowspan=1 colspan=1>应用</td><td rowspan=1 colspan=1>拓扑排序、找环、找连通分量</td><td rowspan=1 colspan=1>无权图最短路径,二分图</td></tr><tr><td rowspan=1 colspan=1>访问顺序</td><td rowspan=1 colspan=1>不唯一,取决于邻接点的顺序和起始点</td><td rowspan=1 colspan=1>不唯一,取决于邻接点的顺序,但层级固定</td></tr></table> <!-- page 143 -->