资讯动态

C语言图存储双模转换实战:邻接矩阵与邻接表互转

发布时间:2026/10/18 2:17:36 来源:尧图企业网站定制
简介本资源是一份面向计算机专业本科生及数据结构初学者的图存储结构实践教学材料聚焦邻接矩阵与邻接表两种核心表示方法的原理对比、相互转换及C语言实现。内容涵盖有向图的存储建模、双向转换算法MatToList/ListToMat、内存动态分配细节、输入交互扩展设计并附完整可运行代码与详细注释适用于课程实验、课程设计及算法基础巩固。资源为1个55KB的Word文档.docx内含实验目的、问题描述、实现要求、完整C源码含MGraph/ALGraph结构体定义、DispMat/DispAdj等关键函数及运行示例输出结构清晰、即学即用。已有8612人学习下载读者可直接复现邻接矩阵与邻接表的构建、转换与打印全过程深入理解稀疏图/稠密图下的空间与时间效率权衡夯实图论算法的底层实现基础。1. 这不是“背概念”作业而是一套能跑通、能改、能 debug 的图存储双模转换实战代码你有没有试过在数据结构课上把邻接矩阵和邻接表的定义抄了三遍考试也能画对但一到写代码——输入一个带权有向图想让它在内存里既存成矩阵又转成链表立刻卡在 malloc 崩溃、指针野指、打印全乱码这不是你基础差是教材给的伪代码没告诉你p-nextarc G-vertices[i].firstarc这一行背后藏着多少空指针陷阱也没说为什么for(jn-1;j0;j--)要倒着遍历才能头插法不丢边。这份 C 语言源码包就是为这种“纸上懂、手上废”的瞬间写的——它不是一个演示程序而是一个可调试、可断点、可手动输任意图、可验证转换结果是否等价的完整工程。6 个顶点、10 条带权有向边的测试用例已内置但更关键的是它把MatToList和ListToMat两个核心函数拆成了带注释的原子操作每一步 malloc 后都隐含内存泄漏风险提示每个DispAdj输出都对齐了顶点编号便于肉眼校验。适合刚学完图的逻辑结构、正要动手实现存储转换的本科生也适合需要快速验证算法中间态比如 Dijkstra 前先确认邻接表建对了的刷题党。别再对着课本空想“链表怎么连”现在就把它编译运行起来亲眼看到矩阵第 2 行第 3 列的4如何变成邻接表中顶点 2 的第一个ArcNode-adjvex3。2. 项目结构与技术选型为什么用纯 C 写为什么顶点数硬编码 MAX_VERTEX_NUM2.1 整体目录与文件构成单文件即工程但结构清晰可扩展这个项目本质是一个单 C 源文件工程graph_convert.c没有 Makefile、没有头文件分离、没有构建脚本——这恰恰是它的优势。对于数据结构初学者最怕“先配环境再写逻辑”而这份代码开箱即用gcc graph_convert.c -o graph_convert ./graph_convert两步就能看到输出。但它的内部结构却非常工整按标准数据结构实验范式组织类型定义区#define MAX_VERTEX_NUM 20起集中声明AdjMatrix二维数组类型、MGraph邻接矩阵图结构、ArcNode边节点、VNode顶点节点、ALGraph邻接表图结构。所有typedef都加了注释说明用途比如//邻接矩阵类型直接点明语义。工具函数区DispMat,DispAdj纯输出函数不修改数据只做格式化打印。DispMat用---替代INF32767提升可读性DispAdj每行以顶点编号开头如2:后跟所有邻接点符合教科书图示习惯。核心转换区MatToList,ListToMat真正的“心脏”。两个函数严格遵循“输入图结构 → 遍历原结构 → 构造新结构 → 赋值元数据顶点数/边数”四步法且变量命名直白p是当前边节点n是顶点总数避免抽象符号干扰理解。主流程区main先静态初始化一个 6×6 测试矩阵A再调用转换链MatToList→DispAdj→ListToMat→DispMat形成闭环验证。最后getchar()防止窗口闪退细节到位。提示虽然当前是单文件但若你要升级为多图管理或加入 BFS/DFS只需将MGraph和ALGraph定义移到graph.h函数声明放头文件实现放graph.cmain.c只负责交互——这是 C 项目最自然的演进路径。2.2 关键技术决策解析硬编码 MAX_VERTEX_NUM 真的是偷懒吗看到#define MAX_VERTEX_NUM 20新手常疑惑“为什么不用动态分配顶点数组”——这恰恰是本项目最务实的设计。在教学场景下动态顶点数会把复杂度从“图存储”引向“内存管理”。学生真正要掌握的是邻接矩阵如何用arcs[i][j]映射边邻接表如何用firstarc链起ArcNode。如果此时还要处理realloc失败、sizeof(VNode)计算、顶点数组指针双重间接就偏离了核心目标。但“硬编码”不等于“写死”。MAX_VERTEX_NUM是一个可安全修改的编译期常量若测试图顶点超 20只需改#define MAX_VERTEX_NUM 50重新编译即可所有循环如for(i0;ig-vexnum;i)都用运行时g-vexnum控制而非直接写20保证逻辑与容量解耦malloc分配ALGraph时只申请结构体本身sizeof(ALGraph)其内部vertices数组大小由MAX_VERTEX_NUM决定这是 C99 之前的经典柔性数组替代方案。对比常见误用有人把vexnum当作malloc尺寸传入sizeof(AdjList) * vexnum这是错的——AdjList本身已是VNode[MAX_VERTEX_NUM]类型sizeof(AdjList)恒为sizeof(VNode)*20与vexnum无关。本项目G(ALGraph *)malloc(sizeof(ALGraph))一行精准踩中正确姿势。2.3 存储结构内存布局图解为什么邻接表的 firstarc 必须初始化为 NULL理解MatToList前必须看清ALGraph在内存中的真实样子。假设MAX_VERTEX_NUM6ALGraph结构体布局如下ALGraph { vertices[0]: VNode { data?, firstarc? } // 顶点0 vertices[1]: VNode { data?, firstarc? } // 顶点1 ... vertices[5]: VNode { data?, firstarc? } // 顶点5 vexnum ? // 运行时填入实际顶点数 arcnum ? // 运行时填入实际边数 }关键点在于vertices[i].firstarc是一个指针未初始化时是随机值野指针。若跳过初始化直接插入边节点p-nextarc G-vertices[i].firstarc会把垃圾地址当链表头后续遍历必然崩溃。因此MatToList开头必须显式清零for(i0; in; i) G-vertices[i].firstarc NULL; // 必须否则后续头插法访问野指针这个NULL初始化不是“多此一举”而是链表安全的基石。很多同学调试时发现DispAdj输出乱码或段错误90% 源于漏了这行。它确保了每次while(p!NULL)判断都有确定起点让整个链表操作从“玄学”回归“可预测”。3. 核心模块逐行拆解MatToList 如何把矩阵的每一行“翻译”成链表3.1 MatToList 函数逻辑链从矩阵坐标到链表节点的映射关系MatToList的本质是将邻接矩阵的二维索引空间i, j映射到邻接表的一维链表空间顶点 i 的链表中插入节点指向 j。我们以测试矩阵A[6][6]第 0 行{0,5,0,7,0,0}为例逐步跟踪i0处理顶点 0 的出边j0:A[0][0]0→ 无边跳过j1:A[0][1]5→ 存在边 0→1权值 5p malloc(sizeof(ArcNode))→ 分配新节点p-adjvex 1→ 记录邻接顶点是 1p-info 5→ 记录边权值是 5p-nextarc G-vertices[0].firstarc→ 此时firstarc是NULL已初始化所以p-nextarc NULLG-vertices[0].firstarc p→ 顶点 0 的链表头指向pj2:A[0][2]0→ 无边跳过j3:A[0][3]7→ 存在边 0→3权值 7再次malloc新节点p2p2-adjvex 3,p2-info 7p2-nextarc G-vertices[0].firstarc→ 此时firstarc指向刚才的p0→1所以p2-nextarc pG-vertices[0].firstarc p2→ 链表头更新为p20→3形成p2 → p → NULL最终顶点 0 的链表是0→3→0→1→NULL注意因倒序遍历jn-1...0实际插入顺序是 3 先于 1故 3 在链表头。这就是头插法的典型效果后遍历到的边出现在链表前面。若需保持输入顺序如 j 从小到大应改为尾插法但头插法代码更简洁且不影响图的数学等价性。3.2 关键代码块精讲倒序遍历 j 的深层原因与头插法实现看这段核心循环for(i0; in; i){ for(jn-1; j0; j--){ // 注意j 从 n-1 递减到 0 if(g-arcs[i][j] ! 0){ p (ArcNode *)malloc(sizeof(ArcNode)); p-adjvex j; p-info g-arcs[i][j]; p-nextarc G-vertices[i].firstarc; // 关键接在当前链表头后 G-vertices[i].firstarc p; // 关键新节点成为新链表头 } } }为什么j要倒着走表面看是为头插法服务但根本原因是避免重复遍历已插入的节点。假设正序j0 to n-1j0: 插入边i→0firstarc指向它j1:p-nextarc firstarc→ 指向i→0节点firstarc更新为i→1链表为i→1 → i→0j2:p-nextarc firstarc→ 指向i→1链表变为i→2 → i→1 → i→0这完全正确那为何作者用倒序答案是测试矩阵A的初始化方式决定了倒序更自然。观察A[0] {0,5,0,7,0,0}非零元素在j1和j3倒序j5,4,3,2,1,0会先遇到j3值 7再遇到j1值 5插入后链表为0→3→0→1与矩阵中“从左到右”的阅读习惯一致人眼先看到A[0][1]但代码先处理A[0][3]所以倒序让代码处理顺序匹配人眼顺序。这不是必须而是教学友好性设计。注意头插法p-nextarc firstarc; firstarc p;是 C 链表的黄金模板。务必记住赋值nextarc必须在firstarc更新之前否则新节点会丢失与旧链表的连接。3.3 ListToMat 函数的“反向映射”如何从链表节点还原矩阵坐标ListToMat是MatToList的逆过程但实现更简单——因为邻接表天然携带(i, j)信息。看关键段for(i0; in; i){ p G-vertices[i].firstarc; // 获取顶点 i 的链表头 while(p ! NULL){ // 遍历顶点 i 的所有出边 g-arcs[i][p-adjvex] p-info; // 矩阵 [i][j] 边权值 p p-nextarc; // 移动到下一条边 } }这里没有嵌套循环时间复杂度是O(arcnum)而非O(vexnum²)体现了邻接表在稀疏图中的优势。以顶点 0 的链表0→3→0→1→NULL为例p指向0→3节点 →g-arcs[0][3] 7p移动到0→1节点 →g-arcs[0][1] 5p为NULL退出循环完美还原矩阵第 0 行的非零项。注意ListToMat不主动清零矩阵它只设置有边的位置其余位置保持0因main中g1是栈变量未初始化的arcs数组值是随机的。所以必须在ListToMat开头加初始化for(i0; in; i) for(j0; jn; j) g-arcs[i][j] 0; // 必须否则随机值污染结果原文代码缺失此行ListToMat函数内注释//for(i0;in;i)for(j0;jn;j)g-arcs[i][j]0;被注释掉了这是重大隐患运行时可能输出---或巨大负数。我们在修复版中已补上。4. 运行与调试从编译报错到输出验证的完整链路4.1 编译与基础运行gcc 命令与常见报错速查在 Linux/macOS 终端进入源码目录执行gcc -Wall -Wextra graph_convert.c -o graph_convert ./graph_convert-Wall -Wextra启用全部警告能捕获潜在问题。常见报错及解决error: ‘for’ loop initial declarations are only allowed in C99 mode原因代码中for(int i0; in; i)是 C99 语法老版本 gcc 默认 C89。解决加-stdc99参数gcc -stdc99 -Wall graph_convert.c -o graph_convertwarning: implicit declaration of function ‘getchar’原因缺少#include stdio.h但原文已有此警告多因头文件顺序错乱。解决确认#include stdio.h在所有#include的第一行。Segmentation fault (core dumped)最可能原因MatToList中漏了G-vertices[i].firstarc NULL初始化或ListToMat未清零矩阵。用gdb调试gcc -g graph_convert.c -o graph_convert # 加 -g 生成调试信息 gdb ./graph_convert (gdb) run (gdb) bt # 查看崩溃栈定位到哪一行4.2 输出结果人工验证三步法确认转换正确性程序输出分三块需逐块验证原始邻接矩阵有向图 G 的邻接矩阵为对照A[6][6]初始化数组检查是否完全一致。特别注意INF32767是否显示为---。例如A[0][1]5应输出5A[0][0]0应输出0。邻接表输出图 G 的邻接矩阵转成邻接表每行格式i: j1 j2 ...表示顶点i有出边到j1,j2...。对照矩阵第i行所有arcs[i][j]!0的j必须全部出现。例如矩阵第 0 行非零列是j1,3则输出应有0: 3 1因头插法顺序。转换回矩阵图 G 的邻接表转成邻接矩阵此矩阵必须与第 1 步完全相同。这是验证转换等价性的黄金标准。若不同说明ListToMat有 bug如未清零矩阵或MatToList漏边。提示用文本对比工具如diff比对两次矩阵输出最可靠./graph_convert | grep -E ^[0-9]|^$ output.txt # 提取数字行 # 手动分割 output.txt 为 matrix1 和 matrix2然后 diff matrix1 matrix24.3 手动输入功能扩展如何把“静态测试”升级为“交互式输入”原文要求“手动输入任意图形”但main中是静态数组。我们来补全交互逻辑。核心是替换A[6][6]初始化部分// 在 main 函数开头添加 int n, e, i, j, w; printf(请输入顶点数 n 和边数 e: ); scanf(%d %d, n, e); if(n MAX_VERTEX_NUM) { printf(顶点数超过最大限制 %d!\n, MAX_VERTEX_NUM); return 1; } g.vexnum n; g.arcnum e; // 初始化矩阵为 0 for(i0; in; i) for(j0; jn; j) g.arcs[i][j] 0; // 输入每条边起点、终点、权值 printf(请依次输入 %d 条边格式i j w:\n, e); for(i0; ie; i) { scanf(%d %d %d, j, w, w); // 注意scanf 顺序是 i j w if(j0 jn w0 wn) { g.arcs[j][w] w; // arcs[i][j] weight } else { printf(顶点编号 %d 或 %d 超出范围 [0,%d)跳过。\n, j, w, n-1); i--; // 重输这条 } }这样用户就能输入任意规模的图。注意scanf读入的是i j w但矩阵索引是arcs[i][j]别写反。此扩展让项目真正满足“任意图”要求且代码改动仅 15 行符合教学项目渐进式演进原则。5. 避坑指南5 个血泪经验总结的高频翻车点5.1 现象DispAdj输出全是0:或1:后面跟着乱码数字原因MatToList中G-vertices[i].firstarc NULL初始化被注释或遗漏导致while(p!NULL)判断失效p指向随机内存p-adjvex读出垃圾值。解决严格检查MatToList开头是否有该初始化循环并确认未被注释。用printf(init %d\n, i)打印初始化过程确认执行。5.2 现象转换后的矩阵中出现---或极大负数如-12345原因ListToMat函数未清零g-arcs数组。栈上分配的MGraph g1其arcs成员是未初始化的随机内存ListToMat只设置了有边的位置其余位置保留垃圾值。解决在ListToMat开头添加双重循环清零for(i0; iG-vexnum; i) for(j0; jG-vexnum; j) g-arcs[i][j] 0;5.3 现象程序运行后直接退出看不到输出原因main结尾的getchar()等待用户按键但若终端输入缓冲区有残留如之前输入的回车getchar()立即返回窗口关闭。解决在getchar()前加fflush(stdin)清空缓冲区Windows或__fpurge(stdin)Linux或更稳妥地用循环printf(按任意键继续...); while(getchar() ! \n); // 吃掉所有残留字符 getchar(); // 等待新输入5.4 现象malloc失败p为NULL后续解引用崩溃原因MatToList中p (ArcNode*)malloc(...)后未检查p是否为NULL直接使用p-adjvex。解决添加防御性检查p (ArcNode*)malloc(sizeof(ArcNode)); if(p NULL) { printf(内存分配失败顶点 %d 的边无法创建。\n, i); exit(1); }5.5 现象邻接表输出中同一顶点的邻接点重复出现如2: 3 3 1原因MatToList的j循环边界错误如写成for(j0; jn; j)导致越界访问g-arcs[i][n]不存在的列该位置若非零则被误认为有效边。解决严格核对循环条件jn不是jn且ng-vexnum是实际顶点数非MAX_VERTEX_NUM。6. 二次开发实战从“跑通”到“能改”的三个改造技巧6.1 技巧一支持无向图转换——只需修改两处逻辑当前代码针对有向图A[i][j]和A[j][i]独立。要支持无向图需保证矩阵对称且邻接表中i→j和j→i同时存在。改造点输入阶段当用户输入边i-j时同时设置arcs[i][j] w和arcs[j][i] w无向图权值相同。MatToList中保持原逻辑不变因为对称矩阵的每一行都会自然产生双向边。但要注意DispAdj输出会显示i: j ...和j: i ...符合无向图定义。无需修改转换函数这是邻接矩阵/表模型的天然优势——存储结构不区分有向无向区别只在数据填充逻辑。6.2 技巧二添加边权为 0 的容错——避免与“无边”混淆当前代码用g-arcs[i][j] ! 0判断是否存在边这导致权值为 0 的边被忽略。实际应用中0 权边有意义如网络延迟为 0。解决方案是引入专用“无边标记”#define NO_EDGE -1 // 替换原来的 0 判断 // 初始化矩阵时g-arcs[i][j] NO_EDGE; // MatToList 中if(g-arcs[i][j] ! NO_EDGE) // ListToMat 中g-arcs[i][p-adjvex] p-info; // info 可为 0这样0成为合法权值NO_EDGE-1才表示无边。只需全局替换0为NO_EDGE并初始化矩阵5 分钟完成升级。6.3 技巧三导出为 DOT 文件——用 Graphviz 可视化验证转换结果肉眼难验加一个SaveToDot(ALGraph *G, char* filename)函数生成 Graphviz DOT 格式void SaveToDot(ALGraph *G, char* filename) { FILE *f fopen(filename, w); fprintf(f, digraph G {\n); for(int i0; iG-vexnum; i) { ArcNode *p G-vertices[i].firstarc; while(p ! NULL) { fprintf(f, %d - %d [label\%d\];\n, i, p-adjvex, p-info); p p-nextarc; } } fprintf(f, }\n); fclose(f); printf(DOT 文件已保存为 %s可用 Graphviz 渲染。\n, filename); }调用SaveToDot(G, graph.dot)然后dot -Tpng graph.dot -o graph.png即得可视化图。这是验证邻接表是否正确的“后悔药”——如果 DOT 图与你脑中的图不一致一定是MatToList逻辑错了。从那以后我每次写图算法只要涉及存储转换都强制走一遍MatToListListToMatSaveToDot三连再对比 PNG 和原始矩阵。这招帮我揪出过三次j循环边界错误和一次malloc未检查。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑