ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C语言实现有向图邻接矩阵与邻接表双向转换实验

C语言实现有向图邻接矩阵与邻接表双向转换实验 简介这份资源面向正在学习数据结构中图结构的学生与开发者聚焦图的两种核心存储方式——邻接矩阵与邻接表帮助解决二者之间相互转换与手动输入建图的编程实践问题。资源包内含1个docx文档压缩包约55KB文档以C语言代码为主体完整给出AdjMatrix、AdjList、ArcNode等结构定义以及DispMat、MatToList、DispAdj、ListToMat等关键函数的实现并配有实验目的、问题描述与具体要求说明。目前已有8611人学习下载适合作为课程实验参考或自学练习材料。读者可从中掌握邻接矩阵与邻接表的存储原理、转换算法、动态内存分配与链表操作技巧理解稀疏图与稠密图在不同存储方式下的效率差异并借助示例有向图与手动输入方案加深对图结构在网络路由、社交网络分析等场景中应用的认识。1. 从一张 6×6 矩阵说起这份图存储实验到底能解决什么很多人学数据结构卡在“图”这一章不是因为算法难而是因为存储结构没吃透。邻接矩阵和邻接表书上讲得清楚但真让你写代码把两者互相转换很多人第一反应是“矩阵我会链表我也懂合在一起就懵”。这份实验资源就是冲着这个痛点来的它给了一套完整的 C 语言实现把有向图的邻接矩阵和邻接表两种存储方式都写出来并且实现了双向转换——矩阵转邻接表、邻接表转矩阵还支持手动输入任意图。它适合谁正在做数据结构实验的学生、准备 408 考研需要手写图存储代码的人、以及工作中偶尔要用 C 处理图结构但不想从零搭轮子的工程师。核心价值在于它不是一个孤立的 demo而是一个可以跑通、可以改参数、可以扩展成手动输入版本的完整骨架。你拿到手就能编译运行看到矩阵和邻接表互相转换的实际输出而不是对着伪代码发呆。2. 邻接矩阵与邻接表的结构定义为什么这样设计2.1 邻接矩阵的结构体与二维数组类型邻接矩阵的本质是一个二维数组arcs[i][j]表示从顶点 i 到顶点 j 的边。对于带权图这个值就是权值对于无权图通常用 1 表示有边、0 表示无边。这份代码里用#define MAX_VERTEX_NUM 20限定了最大顶点数然后用typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]定义了一个二维数组类型。#define MAX_VERTEX_NUM 20 typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; typedef struct { VertexType vexs[MAX_VERTEX_NUM]; // 顶点表存顶点数据 AdjMatrix arcs; // 邻接矩阵存边和权值 int vexnum, arcnum; // 当前顶点数和弧数 } MGraph;这里有几个参数值得注意。MAX_VERTEX_NUM设为 20 是教学场景的常见选择实际工程中如果顶点数可能超过这个值要么改大要么改用动态分配。vexnum和arcnum记录的是当前图的实际规模遍历时只遍历0到vexnum-1不会碰数组里多余的空间。VertexType被定义为int意味着顶点用整数编号表示如果你想让顶点存字符串名字需要把VertexType改成char*或固定长度字符数组同时调整输入输出逻辑。矩阵初始化时对角线通常为 0顶点到自身没有边无边的地方也是 0。但这份代码里用了一个INF宏定义为 32767在DispMat函数中如果arcs[i][j]等于INF就打印---否则打印数值。这说明它预留了“无穷大表示不可达”的语义适合带权图场景。不过在主函数的测试数据里矩阵用的是 0 表示无边所以打印出来是 0 而不是---。这个细节后面在避坑章节会展开。2.2 邻接表的边节点与顶点节点邻接表的核心是“每个顶点挂一条链表”链表里每个节点代表一条从该顶点出发的边。这份代码定义了ArcNode作为边节点VNode作为顶点节点AdjList作为顶点数组类型。typedef struct ArcNode { int adjvex; // 该边指向的顶点在顶点表中的下标 struct ArcNode *nextarc; // 指向下一条边的指针 int info; // 边的权值或其他信息 } ArcNode; typedef struct VNode { VertexType data; // 顶点数据 ArcNode *firstarc; // 指向第一条依附该顶点的边的指针 } VNode, AdjList[MAX_VERTEX_NUM]; typedef struct { AdjList vertices; // 顶点数组每个元素是一条链表的头 int vexnum, arcnum; // 顶点数和弧数 } ALGraph;adjvex存的是目标顶点在vertices数组里的下标不是顶点本身的值。这一点很关键如果你把顶点数据改成字符串adjvex依然存下标通过G-vertices[p-adjvex].data才能拿到顶点名字。info存权值对于无权图可以忽略或置 0。firstarc是链表头指针初始化为NULL表示该顶点没有出边。头插法是这份代码构建邻接表时用的方式每读到一条边就新建一个ArcNode把它插到当前顶点链表的头部。头插法的好处是代码简单不需要维护尾指针代价是链表中边的顺序和矩阵中列遍历的顺序相反。如果你希望邻接表中边的顺序和输入顺序一致需要改成尾插法多维护一个尾指针即可。3. 矩阵转邻接表MatToList 的遍历逻辑与头插法3.1 转换的核心思路矩阵转邻接表本质是遍历矩阵的每一行对于第 i 行找出所有arcs[i][j] ! 0的 j这些 j 就是从顶点 i 出发的边的终点。每找到一个就创建一个边节点插入到顶点 i 的链表中。这份代码的MatToList函数用了倒序遍历列的方式for(jn-1; j0; j--)。为什么要倒着来因为头插法会把后插入的节点放在链表前面。如果正序遍历列先插入的边会被后插入的边挤到后面最终链表顺序和列顺序相反。倒序遍历列再头插链表顺序就正好和列顺序一致。这是一个很实用的技巧很多人第一次写的时候不会注意到。void MatToList(MGraph *g, ALGraph *G) { int i, j, n g-vexnum; ArcNode *p; for (i 0; i n; i) G-vertices[i].firstarc NULL; // 初始化每个顶点的链表为空 for (i 0; i n; i) { for (j n - 1; j 0; j--) { // 倒序遍历配合头插法保持顺序 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; } } } G-vexnum n; G-arcnum g-arcnum; }参数说明g是源邻接矩阵图G是目标邻接表图。注意G在调用前已经分配了内存主函数里G(ALGraph *)malloc(sizeof(ALGraph))函数内部不再分配ALGraph本身只分配边节点。G-vexnum和G-arcnum直接从g复制。这里有一个容易翻车的点如果矩阵中权值为 0 表示“有边但权值为 0”那么arcs[i][j] ! 0这个判断就会漏掉这条边。常见做法是用INF表示无边判断条件改成arcs[i][j] ! INF。这份代码的测试数据里没有权值为 0 的边所以没暴露这个问题但你自己改数据时要注意。3.2 打印邻接表的 DispAdj 函数转换完成后需要验证结果DispAdj负责打印邻接表。它遍历每个顶点打印顶点下标然后沿着链表打印所有邻接顶点下标。void DispAdj(ALGraph *G) { int i; ArcNode *p; for (i 0; i G-vexnum; i) { p G-vertices[i].firstarc; if (p ! NULL) printf(%3d:, i); // 有边才打印顶点编号 while (p ! NULL) { printf(%3d, p-adjvex); // 打印邻接顶点下标 p p-nextarc; } printf(\n); } }输出格式是顶点下标: 邻接点1 邻接点2 ...。比如顶点 0 有边到 1 和 3输出就是0: 1 3。这个格式和很多教材一致方便对照。注意它只打印了adjvex没有打印权值info。如果你需要看权值把printf(%3d, p-adjvex)改成printf(%3d(%d), p-adjvex, p-info)即可。4. 邻接表转矩阵ListToMat 的初始化与回填4.1 先清零再回填邻接表转矩阵比反向转换更直接遍历每个顶点的链表把链表节点的adjvex和info填到矩阵对应位置。但有一个前提矩阵必须先全部清零否则残留的旧值会干扰结果。void ListToMat(ALGraph *G, MGraph *g) { int i, j, n G-vexnum; ArcNode *p; for (i 0; i n; i) for (j 0; j n; j) g-arcs[i][j] 0; // 矩阵清零 for (i 0; i n; i) { p G-vertices[i].firstarc; while (p ! NULL) { g-arcs[i][p-adjvex] p-info; // 回填权值 p p-nextarc; } } g-vexnum n; g-arcnum G-arcnum; }清零这一步不能省。如果g是栈上声明的局部变量里面的值是随机的不清零就会把随机值当成边。如果g是全局变量或静态变量默认是 0但显式清零仍然是好习惯因为同一个MGraph可能被多次复用。回填时只设置了arcs[i][p-adjvex]没有设置对称位置。这是有向图的正确做法有向图的邻接矩阵本来就不对称。如果你要处理无向图需要在回填时同时设置arcs[p-adjvex][i] p-info或者在构建邻接表时就保证每条无向边在两个顶点的链表中都出现。4.2 主函数的测试数据与调用流程主函数里定义了一个 6×6 的矩阵A然后把它复制到MGraph g中接着依次调用DispMat、MatToList、DispAdj、ListToMat、DispMat完成一次完整的双向转换验证。int A[][6] { {0,5,0,7,0,0}, {0,0,4,0,0,0}, {8,0,0,0,0,9}, {0,0,5,0,0,6}, {0,0,0,5,0,0}, {3,0,0,0,1,0} }; g.vexnum 6; g.arcnum 10; for (i 0; i g.vexnum; i) for (j 0; j g.vexnum; j) g.arcs[i][j] A[i][j];这个测试图有 6 个顶点、10 条弧。比如第 0 行有A[0][1]5和A[0][3]7表示从顶点 0 到顶点 1 有一条权值 5 的边到顶点 3 有一条权值 7 的边。第 2 行有A[2][0]8和A[2][5]9表示从顶点 2 出发的两条边。整个图是有向的因为矩阵不对称比如A[0][1]5但A[1][0]0。调用流程是先打印原始矩阵然后矩阵转邻接表并打印再邻接表转回矩阵并打印。如果两次打印的矩阵一致说明转换逻辑正确。这个验证方法很朴素但有效适合作为单元测试的雏形。5. 避坑与排查这份代码跑起来容易翻车的几个地方5.1 现象编译报错ListToMat缺少返回类型原因代码里ListToMat(ALGraph *G, MGraph *g)没有写void在 C89 标准下可能只是警告但在 C99 及更严格的编译器下会报错或警告。main函数也写成了void main(void)标准写法应该是int main(void)。解决把ListToMat的返回类型补成void把void main(void)改成int main(void)并在末尾加return 0;。如果你用的编译器默认是 C89可能不报错但换成 GCC 的-stdc99或-Wall就会暴露。5.2 现象邻接表打印出来顺序和预期相反原因MatToList用了头插法如果你把列遍历改成正序for(j0; jn; j)链表顺序就会和矩阵列顺序相反。解决要么保持倒序遍历列要么改成尾插法。尾插法需要为每个顶点维护一个尾指针代码稍复杂但顺序更直观。常见做法是倒序遍历列配合头插代码最少。5.3 现象手动输入图信息时程序崩溃或读入错误原因原代码只支持硬编码的测试数据没有手动输入功能。如果你直接加scanf循环可能因为缓冲区残留、输入格式不匹配、顶点数超过MAX_VERTEX_NUM等原因崩溃。解决手动输入时先读顶点数和弧数校验不超过MAX_VERTEX_NUM然后循环读每条边的起点、终点、权值校验下标在0到vexnum-1之间每次scanf后检查返回值输入失败时清空缓冲区。常见做法是用fgets读整行再解析比裸scanf更稳。5.4 现象矩阵转邻接表后权值为 0 的边丢失原因判断条件g-arcs[i][j] ! 0把权值为 0 的边当成了无边。解决如果图允许权值为 0需要用INF表示无边判断条件改成g-arcs[i][j] ! INF初始化矩阵时把所有元素设为INF对角线设为 0。这样权值为 0 的边就能被正确识别。5.5 现象内存泄漏多次转换后程序占用内存持续增长原因MatToList里用malloc分配了边节点但没有对应的释放函数。如果在一个循环里反复调用转换旧链表的内存不会被回收。解决写一个FreeAdjList(ALGraph *G)函数遍历每个顶点的链表逐个free边节点然后把firstarc置NULL。在每次重新转换前调用一次或者在程序结束前统一释放。教学代码通常不强调释放但实际工程中这是必须的。6. 手动输入任意图与进阶验证把实验代码变成可复用工具原代码的测试数据是硬编码的只能验证固定图。要把它变成能处理任意图的工具需要加一个手动输入函数。我一般会这样写void InputGraph(MGraph *g) { int i, j, u, v, w; printf(输入顶点数和弧数: ); scanf(%d %d, g-vexnum, g-arcnum); if (g-vexnum MAX_VERTEX_NUM) { printf(顶点数超过上限\n); return; } for (i 0; i g-vexnum; i) for (j 0; j g-vexnum; j) g-arcs[i][j] 0; // 初始化矩阵 for (i 0; i g-arcnum; i) { printf(输入第 %d 条边 (起点 终点 权值): , i 1); scanf(%d %d %d, u, v, w); if (u 0 u g-vexnum v 0 v g-vexnum) g-arcs[u][v] w; // 有向图只设一个方向 else printf(顶点下标越界忽略这条边\n); } }这个函数先读顶点数和弧数校验顶点数不超过MAX_VERTEX_NUM然后清零矩阵再逐条读边。每条边读起点、终点、权值校验下标范围后写入arcs[u][v]。有向图只写一个方向无向图需要同时写arcs[u][v]和arcs[v][u]。输入完成后调用MatToList转成邻接表再调用ListToMat转回矩阵对比两次矩阵是否一致。这个“转换-逆转换-对比”的验证方法很实用能覆盖大部分逻辑错误。如果两次矩阵不一致问题通常出在清零不彻底、权值判断条件不对、或者头插法顺序导致链表遍历遗漏。还有一个进阶技巧把邻接矩阵和邻接表的打印函数改成输出到文件然后用diff对比两次输出。这样比肉眼比对更可靠尤其当图有几十个顶点时。我习惯在调试转换逻辑时先跑小图3 到 5 个顶点确认无误后再上大图。小图上能一眼看出哪条边丢了、哪个权值错了大图上肉眼根本看不过来。从那以后我每次写图存储相关的代码都会先用一个 3 顶点的最小有向图跑通转换再逐步加顶点和边。这个习惯帮我省了很多调试时间也让我对邻接矩阵和邻接表的边界条件有了肌肉记忆。希望帮到你。本文还有配套的精品资源点击获取
返回列表