当前位置: 首页 > news >正文

【考研408数据结构-08】 图论基础:存储结构与遍历算法

📚 【考研408数据结构-08】 图论基础:存储结构与遍历算法

🎯 考频:⭐⭐⭐⭐⭐ | 题型:选择题、综合应用题、算法设计题 | 分值:约8-15分

引言

想象你正在规划一次跨省自驾游,面前摊开一张复杂的公路地图。城市是节点,公路是边,有的路是单向的高速公路,有的是双向的国道。如何在计算机中表示这张地图?如何找到从起点到终点的所有可能路径?这正是图论要解决的核心问题。

在408考试中,图论是数据结构部分的重头戏,几乎每年必考,不仅在选择题中频繁出现,更是算法设计题的重要考点。据统计,近5年的408真题中,有超过15次直接考察了图的存储和遍历相关内容,其中DFS/BFS遍历序列、拓扑排序更是高频考点。

本文将帮你彻底掌握图的基础知识,包括两种经典存储结构的对比、深度优先和广度优先遍历的实现细节、连通性判断以及拓扑排序的应用。

学完本文,你将能够:

  1. ✅ 熟练选择和实现图的存储结构
  2. ✅ 手写DFS和BFS的递归与非递归代码
  3. ✅ 快速判断图的连通性并求解拓扑序列
  4. ✅ 准确分析各种操作的时间复杂度

一、知识精讲

1.1 概念定义

图(Graph) 是由顶点集V和边集E组成的数据结构,记为G=(V,E)。与线性表、树不同,图中的元素之间可以有任意的关系。

💡 核心概念对比

  • 有向图 vs 无向图:边是否有方向性
  • 完全图:任意两个顶点间都有边(无向完全图有n(n-1)/2条边,有向完全图有n(n-1)条边)
  • 稀疏图 vs 稠密图:通常|E|<|V|log|V|为稀疏图,否则为稠密图
  • 连通图:无向图中任意两顶点都有路径;有向图中为强连通图
  • 生成树:包含全部顶点的极小连通子图

⚠️ 408考纲要求

  • 掌握:图的存储结构(邻接矩阵、邻接表)
  • 掌握:图的遍历算法(DFS、BFS)
  • 理解:图的连通性、拓扑排序
  • 了解:关键路径、最短路径(下一章重点)

1.2 原理分析

1.2.1 存储结构原理

邻接矩阵(Adjacency Matrix)

  • 用二维数组A[n][n]表示,A[i][j]=1表示存在边(i,j)
  • 空间复杂度:O(n²),适合稠密图
  • 判断边存在:O(1)时间
  • 获取顶点度:O(n)时间

邻接表(Adjacency List)

  • 每个顶点对应一个链表,存储所有邻接点
  • 空间复杂度:O(n+e),适合稀疏图
  • 判断边存在:O(degree)时间
  • 获取顶点度:O(1)或O(degree)时间
1.2.2 遍历算法原理

深度优先搜索(DFS)

  • 类似树的先序遍历,“不撞南墙不回头”
  • 使用栈(递归调用栈或显式栈)
  • 时间复杂度:O(n+e)邻接表,O(n²)邻接矩阵

广度优先搜索(BFS)

  • 类似树的层序遍历,“地毯式搜索”
  • 使用队列实现
  • 时间复杂度:O(n+e)邻接表,O(n²)邻接矩阵

1.3 性质与特点

对比项邻接矩阵邻接表
空间复杂度O(n²)O(n+e)
判断边(i,j)O(1)O(degree[i])
遍历所有边O(n²)O(n+e)
适用场景稠密图、边频繁查询稀疏图、节省空间
408考频⭐⭐⭐⭐⭐⭐⭐⭐⭐

DFS vs BFS 特点对比

  • DFS:适合求解路径问题、连通分量、拓扑排序
  • BFS:适合求解最短路径(无权图)、层次遍历
  • DFS使用栈,空间O(n);BFS使用队列,空间O(n)
  • 两者遍历序列一般不唯一(408常考)

二、代码实现

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>#define MAXV 100  // 最大顶点数// ========== 邻接矩阵存储结构 ==========
typedef struct {int vexnum, arcnum;           // 顶点数和边数int arcs[MAXV][MAXV];        // 邻接矩阵char vexs[MAXV];              // 顶点信息
} MGraph;// ========== 邻接表存储结构 ==========
typedef struct ArcNode {int adjvex;                   // 邻接点下标struct ArcNode *next;         // 指向下一个邻接点
} ArcNode;typedef struct VNode {char data;                    // 顶点信息ArcNode *first;              // 指向第一个邻接点
} VNode, AdjList[MAXV];typedef struct {AdjList vertices;            // 邻接表int vexnum, arcnum;          // 顶点数和边数
} ALGraph;// ========== 基本操作实现 ==========// 创建无向图的邻接矩阵
void CreateMGraph(MGraph *G) {printf("输入顶点数和边数: ");scanf("%d %d", &G->vexnum, &G->arcnum);// 初始化邻接矩阵for (int i = 0; i < G->vexnum; i++) {for (int j = 0; j < G->vexnum; j++) {G->arcs[i][j] = 0;  // 0表示无边}}// 输入边信息for (int k = 0; k < G->arcnum; k++) {int i, j;printf("输入第%d条边的两个顶点: ", k+1);scanf("%d %d", &i, &j);G->arcs[i][j] = 1;      // 无向图,对称存储G->arcs[j][i] = 1;}
}// 创建无向图的邻接表
void CreateALGraph(ALGraph *G) {printf("输入顶点数和边数: ");scanf("%d %d", &G->vexnum, &G->arcnum);// 初始化顶点表for (int i = 0; i < G->vexnum; i++) {G->vertices[i].data = 'A' + i;  // 顶点命名G->vertices[i].first = NULL;}// 输入边信息,头插法建立邻接表for (int k = 0; k < G->arcnum; k++) {int i, j;printf("输入第%d条边的两个顶点: ", k+1);scanf("%d %d", &i, &j);// 插入边(i,j)ArcNode *p = (ArcNode*)malloc(sizeof(ArcNode));p->adjvex = j;p->next = G->vertices[i].first;G->vertices[i].first = p;// 无向图,插入边(j,i)ArcNode *q = (ArcNode*)malloc(sizeof(ArcNode));q->adjvex = i;q->next = G->vertices[j].first;G->vertices[j].first = q;}
}// ========== DFS遍历(邻接表版本)==========
bool visited[MAXV];  // 访问标记数组void DFS(ALGraph *G, int v) {visited[v] = true;printf("%c ", G->vertices[v].data);  // 访问顶点v// 遍历v的所有邻接点ArcNode *p = G->vertices[v].first;while (p != NULL) {if (!visited[p->adjvex]) {DFS(G, p->adjvex);  // 递归访问未访问的邻接点}p = p->next;}
}// DFS遍历主函数(处理非连通图)
void DFSTraverse(ALGraph *G) {// 初始化访问标记for (int i = 0; i < G->vexnum; i++) {visited[i] = false;}// 对每个连通分量调用DFSfor (int i = 0; i < G->vexnum; i++) {if (!visited[i]) {DFS(G, i);}}
}// ========== BFS遍历(邻接表版本)==========
void BFS(ALGraph *G, int v) {int queue[MAXV], front = 0, rear = 0;  // 循环队列visited[v] = true;printf("%c ", G->vertices[v].data);queue[rear++] = v;  // v入队while (front != rear) {  // 队列非空int u = queue[front++];  // 出队// 遍历u的所有邻接点ArcNode *p = G->vertices[u].first;while (p != NULL) {if (!visited[p->adjvex]) {visited[p->adjvex] = true;printf("%c ", G->vertices[p->adjvex].data);queue[rear++] = p->adjvex;  // 入队}p = p->next;}}
}// BFS遍历主函数
void BFSTraverse(ALGraph *G) {// 初始化访问标记for (int i = 0; i < G->vexnum; i++) {visited[i] = false;}// 对每个连通分量调用BFSfor (int i = 0; i < G->vexnum; i++) {if (!visited[i]) {BFS(G, i);}}
}// ========== 拓扑排序(邻接表)==========
bool TopologicalSort(ALGraph *G) {int indegree[MAXV] = {0};  // 入度数组int stack[MAXV], top = -1;  // 栈int count = 0;  // 已输出顶点数// 计算所有顶点的入度for (int i = 0; i < G->vexnum; i++) {ArcNode *p = G->vertices[i].first;while (p != NULL) {indegree[p->adjvex]++;p = p->next;}}// 入度为0的顶点入栈for (int i = 0; i < G->vexnum; i++) {if (indegree[i] == 0) {stack[++top] = i;}}// 拓扑排序主过程while (top != -1) {int v = stack[top--];  // 出栈printf("%c ", G->vertices[v].data);count++;// 删除v的所有出边ArcNode *p = G->vertices[v].first;while (p != NULL) {indegree[p->adjvex]--;if (indegree[p->adjvex] == 0) {stack[++top] = p->adjvex;  // 新的入度为0的顶点入栈}p = p->next;}}return count == G->vexnum;  // 判断是否有环
}

复杂度分析

  • DFS/BFS遍历:时间O(n+e),空间O(n)
  • 拓扑排序:时间O(n+e),空间O(n)
  • 邻接矩阵DFS/BFS:时间O(n²),空间O(n)

三、图解说明

【图1】DFS遍历过程示意

初始图:        步骤1(访问A):     步骤2(访问B):     步骤3(访问D):A              [A]              [A]              [A]/|\             /|\              /|\              /|\B C D           B C D            [B] C D          [B] C [D]|   |           |   |             |   |            |   |E   F           E   F             E   F            E   F步骤4(访问F):     步骤5(访问C):     步骤6(访问E):[A]              [A]              [A]/|\              /|\              /|\[B][C][D]        [B][C][D]        [B][C][D]|   |             |   |            |   |E  [F]            E  [F]          [E] [F]DFS序列: A-B-D-F-C-E 或 A-C-F-D-B-E(序列不唯一)

【图2】BFS遍历过程示意

使用队列:
步骤1: 队列[A]      访问A
步骤2: 队列[B,C,D]  访问B
步骤3: 队列[C,D,E]  访问C  
步骤4: 队列[D,E,F]  访问D
步骤5: 队列[E,F]    访问E
步骤6: 队列[F]      访问FBFS序列: A-B-C-D-E-F(层序遍历)

【图3】存储结构对比

原图:          邻接矩阵:           邻接表:0---1        0 1 2 3            0 -> 1 -> 3|\ /|        0[0 1 1 1]         1 -> 0 -> 2 -> 3| X |        1[1 0 1 1]         2 -> 1 -> 3|/ \|        2[1 1 0 1]         3 -> 0 -> 1 -> 22---3        3[1 1 1 0]

四、真题演练

📝 真题1(2023年408第41题)

题目:对如下有向图进行深度优先遍历,假设从顶点1开始,且邻接点按编号从小到大的顺序访问,则可能的DFS序列是?

解题思路

  1. 从顶点1开始,标记visited[1]=true
  2. 访问1的邻接点(按编号顺序):2、3、4
  3. 递归访问2,再访问2的邻接点
  4. 回溯并继续访问其他未访问顶点

标准答案:1-2-5-4-3-6(示例)

易错点

  • ⚠️ 忽略"按编号从小到大"的条件
  • ⚠️ 混淆DFS和BFS的访问顺序

📝 真题2(2022年408第23题)

题目:已知一个有向图的邻接表如下,从顶点0开始进行广度优先遍历,访问序列是____。

解题思路

  1. 初始化队列Q,visited数组
  2. 顶点0入队,visited[0]=true
  3. 0出队,将0的所有未访问邻接点入队
  4. 重复直到队列为空

评分要点

  • 正确使用队列(2分)
  • 访问标记正确(2分)
  • 序列完整正确(3分)

📝 真题3(2021年408算法设计题)

题目:设计算法判断有向图是否存在环。

参考答案

// 方法1:DFS+递归栈标记
bool hasCycleDFS(ALGraph *G) {int color[MAXV];  // 0:白色, 1:灰色, 2:黑色for(int i = 0; i < G->vexnum; i++) color[i] = 0;for(int i = 0; i < G->vexnum; i++) {if(color[i] == 0) {if(dfs(G, i, color)) return true;}}return false;
}// 方法2:拓扑排序
bool hasCycleTopo(ALGraph *G) {return !TopologicalSort(G);  // 无法完成拓扑排序则有环
}

五、在线练习推荐

LeetCode精选题目

  • 200. 岛屿数量(Medium,DFS/BFS经典)
  • 207. 课程表(Medium,拓扑排序)
  • 133. 克隆图(Medium,图的遍历)
  • 797. 所有可能的路径(Medium,DFS)

牛客网408专项

  • 图的存储与遍历专项练习
  • 2024考研真题模拟

练习顺序建议

  1. 先练习基础的DFS/BFS模板题
  2. 进阶到连通分量、环检测
  3. 掌握拓扑排序的应用
  4. 最后综合练习图论算法设计题

六、思维导图

图论基础
├── 基本概念
│   ├── 有向图/无向图
│   ├── 完全图/稀疏图/稠密图
│   ├── 连通性(连通图/强连通图)
│   └── 路径/环/生成树
├── 存储结构
│   ├── 邻接矩阵
│   │   ├── 二维数组实现
│   │   ├── 空间O(n²)
│   │   └── 适合稠密图
│   └── 邻接表
│       ├── 数组+链表实现
│       ├── 空间O(n+e)
│       └── 适合稀疏图
├── 遍历算法
│   ├── DFS深度优先
│   │   ├── 递归/栈实现
│   │   ├── 回溯思想
│   │   └── 时间O(n+e)
│   └── BFS广度优先
│       ├── 队列实现
│       ├── 层次遍历
│       └── 最短路径(无权)
└── 经典应用├── 连通性判断├── 拓扑排序├── 环的检测└── 强连通分量

七、复习清单

✅ 本章必背知识点清单

概念理解

  • 能准确区分有向图、无向图、完全图的定义
  • 理解稀疏图与稠密图的区别(|E|<|V|log|V|)
  • 掌握连通图、强连通图、连通分量的概念
  • 记住完全图的边数公式(无向n(n-1)/2,有向n(n-1))

代码实现

  • 能手写邻接矩阵和邻接表的结构定义
  • 能手写DFS的递归版本(5分钟内)
  • 能手写BFS的队列实现(5分钟内)
  • 掌握拓扑排序的完整代码
  • 记住DFS/BFS时间复杂度:邻接表O(n+e),邻接矩阵O(n²)

应用能力

  • 会根据图的特点选择存储结构
  • 能手动模拟DFS/BFS遍历过程
  • 能判断给定序列是否为合法的DFS/BFS序列
  • 掌握用DFS判断图的连通性
  • 掌握用拓扑排序判断有向图是否有环

真题要点

  • 理解DFS/BFS序列不唯一的原因
  • 掌握"按编号顺序访问"的处理方法
  • 记住常见陷阱:未处理非连通图
  • 熟悉算法设计题的标准答题模板

八、知识拓展

🔍 常见误区

  1. 误区:认为DFS一定比BFS快
    正解:两者时间复杂度相同,选择取决于具体问题

  2. 误区:邻接矩阵一定浪费空间
    正解:对于稠密图,邻接矩阵反而更高效

  3. 误区:拓扑排序结果唯一
    正解:拓扑序列可能有多个

💡 记忆技巧

  • DFS:想象走迷宫,一条路走到黑
  • BFS:想象水波扩散,一圈一圈向外
  • 邻接表:稀疏(Sparse)用表(List)
  • 拓扑排序:删边减度,入度为零就输出

📊 自测题

  1. 一个有n个顶点的强连通图至少有多少条边?
    A. n-1 B. n C. n+1 D. n(n-1)

  2. 用邻接表存储有1000个顶点、2000条边的有向图,DFS遍历的时间复杂度是?
    A. O(1000) B. O(2000) C. O(3000) D. O(1000²)

答案:1.B 2.C 3.C

结语

图论是数据结构的核心内容,也是408考试的必考知识点。本章我们重点掌握了:

🎯 核心要点总结

  1. 存储结构选择:稀疏图用邻接表,稠密图用邻接矩阵
  2. DFS本质:递归回溯,适合路径搜索
  3. BFS本质:层次遍历,适合最短路径
  4. 拓扑排序:判环利器,AOV网的基础
  5. 复杂度分析:永远不要忘记O(n+e) vs O(n²)

图的遍历是后续学习最短路径、最小生成树的基础。这些算法看似独立,实际上都是DFS/BFS的变体和应用。下一篇文章,我们将深入探讨**《图论进阶:最短路径与最小生成树》**,学习Dijkstra、Floyd、Prim、Kruskal等经典算法,这些都是408算法设计题的高频考点。

💪 学习建议:图论算法较为抽象,建议多画图模拟算法执行过程,多写代码加深理解。记住:图论不难,关键在于把抽象的概念具象化!

加油,考研人!图论虽然是难点,但也是拿高分的关键。掌握了图论,你就掌握了408数据结构的半壁江山!🚀

http://www.lryc.cn/news/626638.html

相关文章:

  • Linux的奇妙冒险——进程pcb第二讲
  • 云原生俱乐部-k8s知识点归纳(5)
  • SpringTask入门
  • 关于多个el-input的自动聚焦,每输入完一个el-input,自动聚焦到下一个
  • Rust并发编程:解锁高性能系统的密钥
  • 第12课_Rust项目实战
  • 批处理指令常见问题
  • 软考高级--系统架构设计师--案例分析真题解析
  • 【clion】cmake脚本1:调试脚本并构建Fargo项目win32版本
  • 无需驱动!单文件实现键盘按键禁用的技术方案
  • 使用Jmeter轻松实现AES加密测试
  • 01-Docker概述
  • 云计算学习100天-第26天
  • FreeRTOS入门知识(任务通知(二)以及定时器浅析)(七)
  • 2025年8月技术问答第2期
  • AI 与 OCR 识别:深度融合的智能信息提取技术
  • Cobbler 自动化部署服务介绍与部署指南
  • 微服务自动注册到ShenYu网关配置详解
  • 亚矩阵:跨境卖家 YouTube 私域矩阵搭建的高效解决方案
  • 使用acme.sh自动申请AC证书,并配置自动续期,而且解决华为云支持问题,永久免费自动续期!
  • 5.k8s控制器-Replicaset-Deployment、pod 反亲和性
  • 基于截止至 2025 年 6 月 4 日,在 App Store 上进行交易的设备数据统计,iOS/iPadOS 各版本在所有设备中所占比例详情
  • 宿主机与容器通过 rmw_cyclonedds_cpp中间件进行ros2结点之间的通讯的相关注意事项
  • Gin自定义Error中间件
  • synchronized锁,ReentrantLock 锁
  • 路由器NAT的类型测定
  • ios八股文 -- Objective-c
  • 机器翻译 (Machine Translation) 经典面试笔试50题(包括详细答案)
  • 游戏本不插电源适配器不卡设置教程
  • 面试 TOP101 二分查找/排序专题题解汇总Java版(BM17 —— BM22)