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

邻接表转逆邻接表:C语言实现与图解教程(附完整代码)

邻接表转逆邻接表:C语言实现与图解教程(附完整代码)

在数据结构与算法领域,图的存储方式直接影响着算法的效率与实现复杂度。邻接表作为最常用的图存储结构之一,以其空间效率高、易于遍历邻接节点的特点广受欢迎。然而在某些场景下,我们需要快速获取"哪些节点指向当前节点"而非"当前节点指向哪些节点",这时逆邻接表就派上了用场。本文将手把手带你用C语言实现邻接表到逆邻接表的转换,通过图解+代码的双重解析,让初学者也能彻底掌握这一核心技能。

1. 图的基础:邻接表与逆邻接表对比

1.1 邻接表的结构解析

邻接表采用数组+链表的方式存储图:

  • 顶点表:用数组存储各顶点信息,每个数组元素包含:
    typedef struct VNode { int data; // 顶点数据 ArcNode *first; // 指向第一条边 } VNode;
  • 边表:链表节点表示从该顶点出发的边:
    typedef struct ArcNode { int adjvex; // 指向的顶点位置 ArcNode *next; // 下一条边指针 } ArcNode;

示例:对于边A→B,A的边表中会有一个节点指向B。

1.2 逆邻接表的本质

逆邻接表是邻接表的"反向版本",其核心差异在于:

  • 边表存储的是指向该顶点的边而非从该顶点出发的边
  • 边A→B在逆邻接表中表现为:B的边表包含指向A的节点

对比表格

特性邻接表逆邻接表
边表含义出边入边
空间复杂度O(V
查询效率快速获取出边快速获取入边
典型应用DFS/BFS遍历拓扑排序、关键路径

提示:逆邻接表特别适合需要频繁查询节点入度的场景,比如拓扑排序中需要不断查找入度为0的节点。

2. 转换算法的核心思路

2.1 算法流程图解

转换过程可分为三个关键步骤:

  1. 初始化逆邻接表的顶点数组
  2. 遍历原邻接表的每个顶点
  3. 对每个顶点的每条边,在逆邻接表中创建反向边
graph TD A[开始] --> B[初始化逆邻接表顶点数组] B --> C[遍历邻接表顶点i] C --> D[遍历顶点i的所有边i→j] D --> E[在逆邻接表中创建边j→i] E --> F{是否遍历完所有边?} F -- 否 --> D F -- 是 --> G{是否遍历完所有顶点?} G -- 否 --> C G -- 是 --> H[结束]

2.2 时间复杂度分析

  • 需要遍历所有顶点:O(|V|)
  • 对每个顶点遍历其所有边:O(|E|)
  • 总时间复杂度:O(|V|+|E|)

3. C语言完整实现

3.1 数据结构定义

首先定义图的最大顶点数和基本结构体:

#include <stdio.h> #include <stdlib.h> #define MAX_VERTEX_NUM 100 // 最大顶点数 // 边表节点 typedef struct ArcNode { int adjvex; // 指向的顶点位置 struct ArcNode *next; // 下一条边指针 } ArcNode; // 顶点表节点 typedef struct VNode { int data; // 顶点信息 ArcNode *first; // 第一条边指针 } VNode, AdjList[MAX_VERTEX_NUM]; // 图结构 typedef struct { AdjList vertices; // 顶点数组 int vexnum, arcnum; // 顶点数和边数 } ALGraph;

3.2 邻接表创建函数

void CreateALGraph(ALGraph *G) { printf("输入顶点数和边数: "); scanf("%d %d", &G->vexnum, &G->arcnum); // 初始化顶点表 for (int i = 1; i <= G->vexnum; i++) { G->vertices[i].data = i; G->vertices[i].first = NULL; } // 建立边表 printf("输入%d条边(格式: 起点 终点):\n", G->arcnum); for (int k = 0; k < G->arcnum; k++) { int u, v; scanf("%d %d", &u, &v); ArcNode *p = (ArcNode*)malloc(sizeof(ArcNode)); p->adjvex = v; p->next = G->vertices[u].first; // 头插法 G->vertices[u].first = p; } }

3.3 核心转换函数

void ConvertToInverse(ALGraph *G, ALGraph *GInverse) { GInverse->vexnum = G->vexnum; GInverse->arcnum = G->arcnum; // 初始化逆邻接表顶点 for (int i = 1; i <= GInverse->vexnum; i++) { GInverse->vertices[i].data = G->vertices[i].data; GInverse->vertices[i].first = NULL; } // 转换核心逻辑 for (int u = 1; u <= G->vexnum; u++) { ArcNode *p = G->vertices[u].first; while (p != NULL) { int v = p->adjvex; // 创建反向边 ArcNode *s = (ArcNode*)malloc(sizeof(ArcNode)); s->adjvex = u; s->next = GInverse->vertices[v].first; GInverse->vertices[v].first = s; p = p->next; } } }

4. 实例演示与测试

4.1 测试用例图解

以6个顶点8条边的有向图为例:

输入: 6 8 1 2 1 4 4 2 2 5 5 4 3 5 3 6 6 6

邻接表结构

1 -> 4 -> 2 2 -> 5 3 -> 6 -> 5 4 -> 2 5 -> 4 6 -> 6

转换后的逆邻接表

1 -> 2 -> 4 -> 1 3 -> 4 -> 5 -> 1 5 -> 3 -> 2 6 -> 6 -> 3

4.2 打印函数实现

为验证结果,实现打印函数:

void PrintGraph(ALGraph *G) { printf("顶点数: %d, 边数: %d\n", G->vexnum, G->arcnum); for (int i = 1; i <= G->vexnum; i++) { printf("[%d]", G->vertices[i].data); ArcNode *p = G->vertices[i].first; while (p != NULL) { printf(" -> %d", p->adjvex); p = p->next; } printf("\n"); } }

4.3 主函数测试

int main() { ALGraph G, GInverse; // 创建邻接表 CreateALGraph(&G); printf("\n原始邻接表:\n"); PrintGraph(&G); // 转换为逆邻接表 ConvertToInverse(&G, &GInverse); printf("\n逆邻接表:\n"); PrintGraph(&GInverse); return 0; }

5. 常见问题与优化技巧

5.1 内存管理注意事项

  • 每次malloc后应在程序结束时free
  • 推荐添加销毁函数:
    void DestroyGraph(ALGraph *G) { for (int i = 1; i <= G->vexnum; i++) { ArcNode *p = G->vertices[i].first; while (p != NULL) { ArcNode *temp = p; p = p->next; free(temp); } } }

5.2 性能优化方向

  1. 空间优化:对于稀疏图,邻接表/逆邻接表比邻接矩阵更省空间
  2. 时间优化:如果需要同时频繁访问出边和入边,可同时维护两种结构
  3. 并行处理:大规模图处理时,可对不同的顶点分区并行转换

5.3 调试技巧

  • 打印中间结果验证每个顶点的转换
  • 使用小型测试用例(如3个顶点的完全图)人工验证
  • 检查内存泄漏工具如Valgrind

在实际项目中遇到过一个典型错误:忘记初始化逆邻接表顶点的first指针为NULL,导致边表连接异常。这个bug让我花了两个小时才定位到,所以特别提醒初学者注意指针初始化的细节。

http://www.cnnetsun.cn/news/1834417.html

相关文章:

  • AI+Dify实战:零代码构建新闻聚合与智能分析全栈应用
  • LeetCode--459.重复的子字符串(字符串/KMP算法)
  • 智能密码解锁实战:ArchivePasswordTestTool高效恢复加密压缩包全攻略
  • 每日热门Skill研究报告:Browser-Use 深度研究报告
  • Rust网络编程Tokio运行时原理
  • 技术团队管理
  • LaTeX2Word-Equation:3秒搞定公式迁移,学术写作效率提升90%
  • 【人生底稿 13】2020 年 11 月部门调整:从人脸业务到政务行业信息化,我的第二次职场转型,从组长到项目经理
  • 没有界面的 SaaS,反而更值钱?|企服“虾“想
  • 如何用Markdown颠覆传统PPT制作:一站式演示文稿解决方案
  • 桌面卡牌批量生成器终极指南:3步完成100张专业卡牌设计
  • 喔去,litellm 竟然被投毒了,赶紧检查你的机器中招了没有斯
  • RDF 规则:构建语义网的标准指南
  • 阿里“欢乐马”登顶AI视频生成榜单,剑指字节跳动
  • MKVToolNix 98.0 发布:功能升级与问题修复并进
  • OpenClaw 主程序 2026.4.5 升到 2026.4.9
  • 自然语言处理趋势
  • 智能体学习16——学习与适应(Learning-and-Adaptation)-深入解读
  • 软件可访问性中的包容性设计原则
  • 如何5分钟免费激活Windows和Office:KMS_VL_ALL_AIO智能脚本终极指南
  • Windows PDF处理终极方案:5分钟部署Poppler完整工具包
  • [ICLR23]LightGCL揭秘:如何通过SVD增强图对比学习在推荐系统中的表现
  • crossoverJie把
  • 构建毫秒级响应、TB级吞吐、零人工干预的数据Pipeline:揭秘某千亿参数模型背后的12个原子化算子设计
  • 【奇点密档·RAG架构白皮书】:基于2026大会实测数据的向量库选型决策树(Milvus/Weaviate/Qdrant终极对比)
  • Dell r730xd服务器阵列卡实战:系统盘RAID 1配置详解与避坑指南
  • STM32实战:打造物联网智能充电桩安全监控系统
  • 5步掌握SGP4卫星轨道计算:从理论到实战部署指南
  • 深入浅出Virtio:从半虚拟化原理到现代硬件加速演进史
  • 打字不如说话,说话不如截图——AI 代码助手的多模态输入实践晌