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

图论入门:从基础到遍历算法

引言

在前面的数据结构系列中,我们学习了线性结构(数组、链表、栈、队列)和树形结构(二叉树、B 树、哈希表)。今天要讲的,是比树更复杂、表达能力更强的非线性结构。

如果说树表达的是"一对多"的层级关系(一个文件目录、一个公司组织架构),那图表达的就是"多对多"的网状关系——社交网络中的好友关系、地图上的城市道路、互联网中的路由器连接、编译器中的模块依赖……这些全都是图。

图论是数据结构与算法中内容最丰富的领域之一,也是面试中的常客。本文作为图系列的第一篇,将详细讲解图的基本概念、两种存储方式和两种核心遍历算法。

第一部分:图的基本概念

一、图的定义

图(Graph)是由顶点集合和边集合组成的数据结构。形式化地定义为:

通俗地说:顶点就是图中的"点"(人、城市、任务),就是点之间的"连线"(好友关系、道路、依赖)。

二、图的分类

1. 无向图 vs 有向图

2. 带权图 vs 无权图

3. 完全图

完全图:图中每一对顶点之间都有边。

图的类型最多边数
无向完全图n(n-1)/2
有向完全图n(n-1)
4. 稀疏图 vs 稠密图
类型定义判断标准
稀疏图边数远小于 n²|E| < n log n
稠密图边数接近 n²|E| ≈ n²

为什么区分稀疏和稠密?因为它们适合不同的存储方式——稀疏图用邻接表省空间,稠密图用邻接矩阵更快。

三、图的核心术语

1. 度(Degree)

2. 路径与回路

3. 连通性

第二部分:图的存储方式

一、邻接矩阵

用一个 n×n 的二维数组存储顶点之间的连接关系

代码实现

#define MAX_V 100 typedef struct { int vertexNum; // 顶点数量 int edgeNum; // 边数量 int matrix[MAX_V][MAX_V]; // 邻接矩阵 } GraphMatrix; // 初始化 void initGraph(GraphMatrix* g, int n) { g->vertexNum = n; g->edgeNum = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { g->matrix[i][j] = 0; // 无边 } } } // 添加边(无向图) void addEdge(GraphMatrix* g, int u, int v) { g->matrix[u][v] = 1; g->matrix[v][u] = 1; // 无向图对称 g->edgeNum++; } // 添加带权边(无向图) void addWeightedEdge(GraphMatrix* g, int u, int v, int w) { g->matrix[u][v] = w; g->matrix[v][u] = w; g->edgeNum++; }

邻接矩阵的复杂度

操作复杂度
判断 u、v 是否有边O(1)
获取某个顶点的所有邻居O(n)
空间O(n²)
添加/删除边O(1)

适用场景:稠密图(边多)、需要快速判断两点是否相邻。

二、邻接表

每个顶点维护一个链表,存储它连接的所有邻居

代码实现

#include <stdio.h> #include <stdlib.h> #define MAX_V 100 // 邻接表的边节点 typedef struct EdgeNode { int adjVertex; // 邻接顶点下标 int weight; // 权重(带权图用) struct EdgeNode* next; // 下一条边 } EdgeNode; // 邻接表 typedef struct { EdgeNode* adjList[MAX_V]; // 每个顶点的边链表 int vertexNum; int edgeNum; } GraphList; // 初始化 void initGraphList(GraphList* g, int n) { g->vertexNum = n; g->edgeNum = 0; for (int i = 0; i < n; i++) { g->adjList[i] = NULL; } } // 添加边(无向图,头插法) void addListEdge(GraphList* g, int u, int v, int w) { // u → v EdgeNode* node = (EdgeNode*)malloc(sizeof(EdgeNode)); node->adjVertex = v; node->weight = w; node->next = g->adjList[u]; g->adjList[u] = node; // v → u(无向图对称) node = (EdgeNode*)malloc(sizeof(EdgeNode)); node->adjVertex = u; node->weight = w; node->next = g->adjList[v]; g->adjList[v] = node; g->edgeNum++; }

邻接表的复杂度

操作复杂度
判断 u、v 是否有边O(degree(u))
获取某个顶点的所有邻居O(degree(u))
空间O(n + e)
添加/删除边O(1)

三、邻接矩阵 vs 邻接表

对比项邻接矩阵邻接表
空间O(n²)O(n + e)
判断 u、v 是否有边O(1)O(degree(u))
获取所有邻居O(n)O(degree(u))
添加边O(1)O(1)
适合场景稠密图稀疏图

选择原则:边数少用邻接表(省空间),边数多用邻接矩阵(快)。实际开发中邻接表更常用,因为大多数现实图都是稀疏的。


第三部分:深度优先搜索(DFS)

一、DFS 的核心思想

DFS = 一条路走到黑,走不通再回头。从起点开始,沿着一条路径一直走到最深处,然后回溯,换一条路继续走。

二、DFS 代码实现(邻接矩阵)

int visited[MAX_V]; // DFS 递归实现 void DFS_Matrix(GraphMatrix* g, int v) { visited[v] = 1; printf("%c ", 'A' + v); // 访问顶点 v // 遍历 v 的所有邻居 for (int i = 0; i < g->vertexNum; i++) { if (g->matrix[v][i] != 0 && !visited[i]) { DFS_Matrix(g, i); // 递归访问未访问的邻居 } } } // 遍历整个图(处理不连通的情况) void DFS_Traverse(GraphMatrix* g) { // 初始化访问标记 for (int i = 0; i < g->vertexNum; i++) { visited[i] = 0; } // 对每个未访问的顶点调用 DFS // (如果图不连通,需要多次调用) for (int i = 0; i < g->vertexNum; i++) { if (!visited[i]) { DFS_Matrix(g, i); printf("(连通分量结束)\n"); } } }

三、DFS 代码实现(邻接表)

void DFS_List(GraphList* g, int v) { visited[v] = 1; printf("%c ", 'A' + v); // 遍历 v 的边链表 EdgeNode* p = g->adjList[v]; while (p != NULL) { if (!visited[p->adjVertex]) { DFS_List(g, p->adjVertex); } p = p->next; } }

四、DFS 的非递归实现(用栈模拟递归)

#include <stdbool.h> void DFS_NonRecursive(GraphMatrix* g, int start) { int stack[MAX_V]; int top = -1; bool visited[MAX_V] = {false}; stack[++top] = start; while (top != -1) { int v = stack[top--]; if (!visited[v]) { visited[v] = true; printf("%c ", 'A' + v); // 将所有未访问的邻居入栈 // (反序入栈以保证和递归顺序一致) for (int i = g->vertexNum - 1; i >= 0; i--) { if (g->matrix[v][i] != 0 && !visited[i]) { stack[++top] = i; } } } } }

递归 vs 非递归

实现优点缺点
递归代码简洁深度过大可能栈溢出
非递归(栈)不受递归深度限制代码稍复杂

第四部分:广度优先搜索(BFS)

一、BFS 的核心思想

BFS = 层层扩散,先近后远。从起点开始,先访问所有邻居,再访问邻居的邻居……逐层向外扩展。

二、BFS 代码实现(邻接矩阵)

void BFS_Matrix(GraphMatrix* g, int start) { int queue[MAX_V]; int front = 0, rear = 0; int visited[MAX_V] = {0}; // 起点入队 queue[rear++] = start; visited[start] = 1; while (front < rear) { int v = queue[front++]; // 出队 printf("%c ", 'A' + v); // 将所有未访问的邻居入队 for (int i = 0; i < g->vertexNum; i++) { if (g->matrix[v][i] != 0 && !visited[i]) { queue[rear++] = i; visited[i] = 1; } } } }

三、BFS 代码实现(邻接表)

void BFS_List(GraphList* g, int start) { int queue[MAX_V]; int front = 0, rear = 0; int visited[MAX_V] = {0}; queue[rear++] = start; visited[start] = 1; while (front < rear) { int v = queue[front++]; printf("%c ", 'A' + v); EdgeNode* p = g->adjList[v]; while (p != NULL) { if (!visited[p->adjVertex]) { queue[rear++] = p->adjVertex; visited[p->adjVertex] = 1; } p = p->next; } } }

第五部分:DFS vs BFS

对比项DFSBFS
数据结构栈(递归/显式栈)队列
遍历顺序纵向深入横向扩散
找到的路径不一定最短一定最短(无权图)
空间O(h),h 为深度O(w),w 为最大宽度
适用场景路径存在性、连通分量、环检测最短路径、层序遍历
实现递归简单队列简单

第六部分:完整测试代码

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #define MAX_V 100 // ==================== 邻接矩阵 ==================== typedef struct { int vertexNum; int edgeNum; int matrix[MAX_V][MAX_V]; } GraphMatrix; void initMatrix(GraphMatrix* g, int n) { g->vertexNum = n; g->edgeNum = 0; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) g->matrix[i][j] = 0; } void addMatrixEdge(GraphMatrix* g, int u, int v) { g->matrix[u][v] = 1; g->matrix[v][u] = 1; g->edgeNum++; } // ==================== 邻接表 ==================== typedef struct EdgeNode { int adjVertex; struct EdgeNode* next; } EdgeNode; typedef struct { EdgeNode* adjList[MAX_V]; int vertexNum; int edgeNum; } GraphList; void initList(GraphList* g, int n) { g->vertexNum = n; g->edgeNum = 0; for (int i = 0; i < n; i++) g->adjList[i] = NULL; } void addListEdge(GraphList* g, int u, int v) { EdgeNode* node; node = (EdgeNode*)malloc(sizeof(EdgeNode)); node->adjVertex = v; node->next = g->adjList[u]; g->adjList[u] = node; node = (EdgeNode*)malloc(sizeof(EdgeNode)); node->adjVertex = u; node->next = g->adjList[v]; g->adjList[v] = node; g->edgeNum++; } // ==================== DFS ==================== int visited[MAX_V]; void DFS_Matrix(GraphMatrix* g, int v) { visited[v] = 1; printf("%c ", 'A' + v); for (int i = 0; i < g->vertexNum; i++) if (g->matrix[v][i] && !visited[i]) DFS_Matrix(g, i); } void DFS_List(GraphList* g, int v) { visited[v] = 1; printf("%c ", 'A' + v); for (EdgeNode* p = g->adjList[v]; p; p = p->next) if (!visited[p->adjVertex]) DFS_List(g, p->adjVertex); } // ==================== BFS ==================== void BFS_Matrix(GraphMatrix* g, int start) { int queue[MAX_V], front = 0, rear = 0; int vis[MAX_V] = {0}; queue[rear++] = start; vis[start] = 1; while (front < rear) { int v = queue[front++]; printf("%c ", 'A' + v); for (int i = 0; i < g->vertexNum; i++) if (g->matrix[v][i] && !vis[i]) { queue[rear++] = i; vis[i] = 1; } } } void BFS_List(GraphList* g, int start) { int queue[MAX_V], front = 0, rear = 0; int vis[MAX_V] = {0}; queue[rear++] = start; vis[start] = 1; while (front < rear) { int v = queue[front++]; printf("%c ", 'A' + v); for (EdgeNode* p = g->adjList[v]; p; p = p->next) if (!vis[p->adjVertex]) { queue[rear++] = p->adjVertex; vis[p->adjVertex] = 1; } } } // ==================== 测试 ==================== int main() { GraphMatrix gm; initMatrix(&gm, 5); addMatrixEdge(&gm, 0, 1); // A-B addMatrixEdge(&gm, 0, 2); // A-C addMatrixEdge(&gm, 1, 3); // B-D addMatrixEdge(&gm, 2, 3); // C-D addMatrixEdge(&gm, 1, 4); // B-E GraphList gl; initList(&gl, 5); addListEdge(&gl, 0, 1); addListEdge(&gl, 0, 2); addListEdge(&gl, 1, 3); addListEdge(&gl, 2, 3); addListEdge(&gl, 1, 4); printf("===== DFS(邻接矩阵)=====\n"); memset(visited, 0, sizeof(visited)); DFS_Matrix(&gm, 0); printf("\n"); printf("===== DFS(邻接表)=====\n"); memset(visited, 0, sizeof(visited)); DFS_List(&gl, 0); printf("\n"); printf("===== BFS(邻接矩阵)=====\n"); BFS_Matrix(&gm, 0); printf("\n"); printf("===== BFS(邻接表)=====\n"); BFS_List(&gl, 0); printf("\n"); return 0; }

运行结果

注意:DFS 的邻接矩阵和邻接表结果可能不同(因为遍历邻居的顺序不同),但这不影响正确性——两者都是合法的 DFS。


总结

一、核心概念速查

概念含义
顶点图中的节点
顶点之间的连接
有向图边有方向
无向图边无方向
带权图边有权重
连通图任意两点可达
顶点连接的边数
稀疏图边数远小于 n²
稠密图边数接近 n²

二、存储方式对比

方式空间判邻边遍历邻居适用
邻接矩阵O(n²)O(1)O(n)稠密图
邻接表O(n+e)O(degree)O(degree)稀疏图

三、DFS vs BFS

对比DFSBFS
结构队列
路径不一定最短最短
场景连通性、环检测最短路径、层级遍历

四、一句话记忆

图是顶点和边的集合,用邻接矩阵(稠密)或邻接表(稀疏)存储。DFS 用栈纵向深入,BFS 用队列横向扩散。BFS 能找到无权图的最短路径。

http://www.jsqmd.com/news/944578/

相关文章:

  • macOS第三方鼠标体验差?Mac Mouse Fix如何解决滚动卡顿与按键失灵问题
  • 高级java每日一道面试题-2026年01月18日-实战篇[Docker]-如何清理仓库中的旧镜像?
  • kkfile安全预览minio的文件
  • 住建部2026城市体检全面启动 ——“一网统管”平台将成为核心载体
  • 免费高效的跨语言语义工具:cross-en-de-fr-roberta-sentence-transformer安装与配置指南
  • 智能反馈不是“加个评分按钮”!深度解析Transformer-based Feedback Encoder在低信噪比场景下的F1提升23.6%实证
  • ProteinMPNN:当AI学会“设计“蛋白质,生物医药的未来会怎样?
  • Python中模块导入方式
  • AI 不听话?7 步排查清单,从「它又犯病了」到「我懂了」
  • 智能拼团合规红线预警(GDPR+《生成式AI服务管理暂行办法》双框架适配方案),法务+技术联合签发
  • Laravel 5 角色权限管理终极指南:从 is() 到 allowed() 的完整 API 解析
  • 小型运油船价格多少 - 舒雯文化
  • Logback 1.5.34 发布:修复反序列化漏洞,增强异常处理能力
  • DIY无绳工具电池适配器:跨品牌电池兼容改造实战指南
  • 2026婚纱摄影行业白皮书:丽江影楼合规标杆与市场真相 - GrowthUME
  • 终极音频编辑指南:如何用Audacity制作专业级音效
  • Haon-Chen/e5-omni-7B完全安装指南:从Sentence Transformers到多模态环境配置
  • 多语言文本嵌入终极指南:paraphrase-multilingual-MiniLM-L12-v2实战部署与优化
  • 如何优雅地在 Laravel 视图中控制权限:gh_mirrors/role/roles Blade 指令完全指南 [特殊字符]
  • indonesian-roberta-base-posp-tagger实战教程:10个印尼语句子词性标注示例详解
  • 2026 文旅游乐商户开店优选!景区电玩乐园智慧票务核销系统全解析 - 新闻快传
  • 5分钟快速上手:Windows平台最强大的开源按键映射工具QKeyMapper终极指南
  • 11 ELMo 论文精读:上下文词向量为什么重要?
  • Linux 内核中的 epoll:从 syscall 底层原理到高并发架构启示
  • NuExtract-1.5未来路线图:AI信息提取技术的发展趋势与创新方向
  • Adobe-GenP 3.0终极指南:免费激活Adobe CC全系列软件
  • 【电赛终极杀器】别再只会写裸机主循环了!STM32进阶修仙指南:双缓冲DMA、FreeRTOS避坑与HardFault死机抢救
  • ETCHR-FLUX.2-klein-9B核心架构解析:深入理解Edit-Verify-Reason推理机制
  • 2026-2027年度在线浊度计十大国产品牌综合实力排行榜与技术选型白皮书 - 水质仪表品牌排行榜
  • 如何利用YOLOv8深度学习实现FPS游戏AI瞄准辅助?完整实战指南