
简介这份文档面向正在学习数据结构、需要攻克图存储难点的学生与编程初学者聚焦图的邻接矩阵与邻接表两种存储结构及其相互转换。内容围绕有向图展开先给出邻接矩阵与邻接表的定义与结构体设计再通过DispMat、MatToList、DispAdj、ListToMat等函数实现矩阵转邻接表、邻接表转矩阵的双向转换并支持手动输入任意图信息进行验证同时涉及动态内存分配与释放等易错细节。资源包共1个docx文件约55KB以实验目的、问题描述、程序要求与完整C语言源码的形式组织便于对照阅读与上机调试。已有8614人学习适合作为课程实验参考或考前复习材料帮助读者理解两种存储方式的适用场景、掌握转换思路并提升图算法的编程能力。1. 从一份“能跑但不敢改”的图存储代码说起很多同学学数据结构图的邻接矩阵和邻接表是绕不过去的一关。课本上讲得清楚可一到自己动手往往卡在“矩阵怎么变成链表”“链表怎么变回矩阵”这两步。我手头这份 C 语言实验代码就是专门解决这个问题的它把一张有向图分别用邻接矩阵和邻接表存下来再实现两种结构之间的双向转换最后打印出来对照。代码不长但把MGraph、ALGraph、ArcNode这几个核心结构体串起来了适合刚学完图、想找个能跑通的例子练手的开发者。它不追求工程化胜在链路完整——输入、存储、转换、输出一条线走完拿来当二次开发的起点正合适。2. 结构体与转换链路两种存储到底怎么对应2.1 邻接矩阵MGraph的字段设计先看矩阵这边。代码里用typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]把二维数组包了一层再塞进MGraph#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;vexs是顶点数组arcs[i][j]存的是从顶点 i 到顶点 j 的边权。有向图里arcs[i][j]和arcs[j][i]一般不相等这点和摘要里说的一致。INF被定义成 32767用来表示“没有边”打印时显示成---。这里有个细节MatToList判断的是g-arcs[i][j] ! 0而不是! INF说明这份代码默认用 0 表示无边、非零表示有边权。两种约定混着用是后面容易翻车的地方第 5 章会细说。MAX_VERTEX_NUM定成 20意味着顶点数上限写死在编译期。想支持更大的图要么改宏要么把数组换成动态分配。实验场景够用但二次开发时这是第一个要动的地方。2.2 邻接表ALGraph与边结点ArcNode邻接表这边是典型的“数组 链表”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;vertices[i].firstarc指向顶点 i 的第一条出边顺着nextarc能遍历完所有出边。adjvex是目标顶点下标info是权值。这套结构和课本上的定义基本一致好处是稀疏图省空间——只存实际存在的边不像矩阵那样不管有没有边都占一个格子。两种结构的对应关系可以这样理解矩阵的每一行 i对应邻接表里vertices[i]挂的那条链表矩阵里arcs[i][j]非零链表里就有一个adjvex j的结点。转换的本质就是在“按行扫描二维数组”和“按顶点遍历单链表”之间来回搬数据。2.3 转换链路MatToList与ListToMat的分工整个程序的调用链在main里很清楚先构造MGraph g打印矩阵再MatToList(g, G)转成邻接表并打印最后ListToMat(G, g1)转回矩阵再打印。两个转换函数互为逆操作验证方式就是看第一次打印的矩阵和最后一次打印的矩阵是否一致。MatToList的核心是双重循环加头插法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; }这里j从n-1往0扫是因为用了头插法——每次新结点插到链表头部倒着扫能让最终链表里adjvex从小到大排列打印出来好看。如果正着扫再头插链表顺序会反过来虽然不影响正确性但对照输出时容易看花眼。malloc出来的结点没有统一释放实验代码跑完就退出问题不大但二次开发里这是内存泄漏的隐患。ListToMat则是先清零再填值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; // 矩阵先全部置 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; }注意ListToMat没有返回值声明默认int这是老式 C 的写法现代编译器会警告。逻辑本身没问题矩阵清零后遍历每个顶点的边链表把info填到arcs[i][adjvex]。因为邻接表只存了实际存在的边没填到的位置保持 0正好对应“无边”。3. 跑起来编译、输入与输出对照3.1 编译环境与命令这份代码是纯 C没有第三方依赖gcc直接能编。把源码存成graph_convert.c在终端里执行gcc graph_convert.c -o graph_convert ./graph_convert如果用的是较新的 GCC可能会对ListToMat缺返回类型、main写成void main这类老写法报警告加-w可以压掉但建议留着警告看正好借机把类型补全。Windows 下用 MinGW 或 Dev-C 同理注意源文件编码用 UTF-8否则中文提示可能乱码。3.2 内置测试数据长什么样main里硬编码了一个 6 顶点的邻接矩阵Aint 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} };这是一张有向带权图arcnum手动写成 10。跑起来后先打印矩阵0的位置显示---非零位置显示权值然后打印邻接表格式是顶点下标: 邻接点1 邻接点2 ...最后打印转回来的矩阵。对照第一次和第三次输出如果完全一致说明两个转换函数逻辑自洽。3.3 手动输入任意图的改造点题目要求里有一条“手动输入任意图形”但原代码只做了硬编码。要补上这个能力得在main开头加一段输入逻辑把g.vexnum、g.arcnum和g.arcs从键盘读进来。常见做法是先读顶点数和弧数再循环读每条边的起点、终点、权值往矩阵里填printf(输入顶点数和弧数: ); scanf(%d %d, g.vexnum, g.arcnum); for (i 0; i g.vexnum; i) for (j 0; j g.vexnum; j) g.arcs[i][j] 0; // 初始化全 0 for (k 0; k g.arcnum; k) { int u, v, w; scanf(%d %d %d, u, v, w); // 起点、终点、权值 g.arcs[u][v] w; // 有向图只填一个方向 }这段替换掉原来的A数组赋值即可后面的转换和打印不用动。输入时顶点下标从 0 开始和代码里的数组索引对齐省得再做减一换算。如果要做无向图填完arcs[u][v]再补一句arcs[v][u] w就行。4. 避坑与排查这份代码最容易翻车的五个地方4.1 打印矩阵时INF和0混用现象矩阵里明明没有边却打印出0而不是---或者反过来有权值为 0 的边被当成无边。原因DispMat判断的是g-arcs[i][j] INF才打---但MatToList判断的是! 0才建边。两套约定不一致——矩阵初始化用 0 表示无边打印却指望 INF 表示无边。解决统一约定。要么全用 0 表示无边把DispMat的条件改成 0要么全用 INF初始化时填 INF、MatToList判断! INF。改的时候两处一起改别只动一头。4.2ListToMat缺返回类型导致编译报错现象GCC 报return type defaults to int严格模式下直接编译失败。原因函数定义写成ListToMat(ALGraph *G, MGraph *g)没写void。老编译器默认按int处理新标准不再容忍。解决在函数名前补void同时在文件开头或main之前加函数声明避免隐式声明警告。顺手把void main改成int main并在末尾return 0养成习惯。4.3 邻接表结点只分配不释放现象反复调用转换函数时内存占用持续上涨长时间运行的程序会越来越吃内存。原因MatToList里每个边结点都malloc但全篇没有对应的free。实验代码跑一次就退出操作系统兜底回收看不出问题一旦嵌进循环或服务里就是实打实的泄漏。解决写一个DestroyAdj(ALGraph *G)遍历每个顶点的链表逐个free在转换前或程序结束前调用。二次开发时这是必补的一环。4.4 顶点数超过MAX_VERTEX_NUM直接越界现象输入 25 个顶点程序不报错但打印乱码或崩溃。原因MAX_VERTEX_NUM写死 20vexs、arcs、vertices都是定长数组超了就是越界写C 不会拦你。解决输入前校验vexnum MAX_VERTEX_NUM超了给提示并退出或者把定长数组改成malloc动态分配按实际顶点数申请空间。前者改动小后者更彻底。4.5 头插法顺序反了导致对照困难现象邻接表打印出来每个顶点的邻接点是从大到小排的和矩阵从左到右的顺序对不上肉眼核对费劲。原因MatToList里j从n-1扫到0配合头插本来是为了让链表升序如果改成从0扫到n-1还用头插顺序就反了。解决要么保持倒序扫描要么改用尾插法多维护一个尾指针。顺序不影响转换正确性但影响可读性和调试效率建议固定一种写法并在注释里写清楚。5. 二次开发把实验代码改成能复用的图工具5.1 抽出初始化与销毁让转换函数可反复调用原代码把矩阵构造写在main里转换函数又依赖外部传进来的MGraph已经填好。想做成工具函数第一步是把“建图”和“转换”解耦。我一般会加两个辅助函数void InitMGraph(MGraph *g, int n) { int i, j; g-vexnum n; g-arcnum 0; for (i 0; i n; i) { g-vexs[i] i; // 顶点数据先用下标占位 for (j 0; j n; j) g-arcs[i][j] 0; // 统一用 0 表示无边 } } void DestroyAdj(ALGraph *G) { int i; ArcNode *p, *q; for (i 0; i G-vexnum; i) { p G-vertices[i].firstarc; while (p ! NULL) { q p-nextarc; free(p); p q; } G-vertices[i].firstarc NULL; } }InitMGraph把顶点数、矩阵清零、顶点表初始化一次做完调用方只管往里填边。DestroyAdj配合MatToList使用转换前先销毁旧链表避免重复转换时旧结点泄漏。这两个函数一加MatToList和ListToMat就能在循环里安全地反复调用不再是一次性脚本。5.2 用一张对照表验证转换正确性改完代码怎么确认没改坏最直接的办法是准备几组输入跑完对比矩阵和邻接表的输出。我习惯列一张小表把预期结果写死每次改动后手动或脚本核对测试场景输入特征预期矩阵输出预期邻接表输出空图顶点 3弧 0全---每行只有顶点下标无邻接点单边图顶点 2弧 10→1 权 5arcs[0][1]5其余 0顶点 0 挂一个adjvex1原实验图6 顶点 10 弧与A数组一致每行邻接点升序含权 0 边某条边权值为 0需明确 0 是边还是无边取决于约定必须与矩阵一致最后一行是重点如果业务里真的出现权值为 0 的边用 0 表示无边的约定就失效了得换成 INF 或额外加一个存在标记数组。这个坑在实验里碰不到真做项目时迟早遇到提前想清楚比事后改省事。5.3 从“能跑”到“能改”的一个习惯这份代码最值得带走的东西不是某一行具体写法而是“转换前后必须能对上”的验证思路。我早年改这类代码图省事只跑一遍内置数据看着输出正常就提交结果换一组输入就露馅——头插顺序、0 和 INF 的约定、越界检查全是那时候踩出来的。后来我给自己定了个规矩凡是涉及两种结构互转的代码改完必须至少跑三组输入一组空、一组单边、一组原数据三组都对上才算完。这个习惯不花什么时间但能挡掉大部分低级错误。这份图存储转换的实验代码结构清晰、链路完整拿它当起点把动态内存、输入校验、约定统一这几块补上就是一个能塞进小工具里的图结构模块。希望帮到你。本文还有配套的精品资源点击获取