简介:本资源是一份面向计算机专业本科生与数据结构初学者的图存储结构实践教学材料,聚焦邻接矩阵与邻接表两种核心表示方法的原理对比、相互转换及C语言实现。内容涵盖有向图的存储建模、双向转换算法(MatToList/ListToMat)、内存动态分配细节、输入交互设计及完整可运行代码,配套详细实验目的、问题描述与分步实现说明,适用于课程实验、期末复习与算法基础夯实。资源为单文件Word文档(.docx),共1个文件,大小55KB,排版清晰,含结构体定义、关键函数注释、测试用例及运行效果截图说明。已有8612人学习下载,读者可直接复现邻接矩阵与邻接表的构建、打印与互转全过程,掌握稀疏图/稠密图存储选型依据,并获得规范的C语言图操作编程范式与常见内存管理注意事项。
1. 这不是一道作业题,而是一把打开图算法世界的钥匙:邻接矩阵 ↔ 邻接表双向转换的完整C语言实现
你有没有试过——在纸上画完一个6节点有向图,标好10条带权边,然后盯着邻接矩阵里那堆0、5、7、INF发呆,却完全想不出它在内存里“长什么样”?或者更糟:调试DFS时发现邻接表指针乱跳,打印出来全是地址,根本看不出哪条边连着谁?这不是你手生,是缺一个能实时看见结构映射关系的对照工具。这个项目就是干这个的:它用纯C语言(无任何第三方库)实现了邻接矩阵与邻接表的双向无损转换,且每一步都可打印、可验证、可打断点。它不教抽象定义,而是让你亲手把A[0][3] = 7变成vertices[0].firstarc->adjvex = 3, ->info = 7,再反向还原回来。适合刚学完图基础、正卡在“知道概念但写不出代码”的同学;也适合要快速验证图算法输入格式(比如Dijkstra需要邻接表,Floyd需要矩阵)的实战开发者。它不是玩具,是能直接嵌入你课程设计或小工具链的生产级转换模块——所有内存分配、边界检查、错误值处理都已就位,只差你填入自己的图数据。
2. 架构拆解:两个核心结构体 + 四个关键函数,构成零依赖的图存储转换闭环
这个项目没有花哨框架,只有最硬核的C语言结构体设计和指针操作。它的骨架由两个图结构体和四个核心函数撑起,全部定义在同一个.c文件中,编译即跑,无需头文件或链接。理解这组结构,就等于拿到了整个项目的源代码地图。
2.1 邻接矩阵结构体MGraph:用二维数组固化图的全局关系
#define MAX_VERTEX_NUM 20 // 最大顶点数,硬编码上限,实际使用由 vexnum 控制 typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵类型:本质是二维int数组 typedef int VertexType; // 顶点数据类型,此处简化为int(如顶点编号0~5) typedef struct { VertexType vexs[MAX_VERTEX_NUM]; // 顶点表:存储顶点自身信息(本例中未实际使用,留作扩展) AdjMatrix arcs; // 邻接矩阵:arcs[i][j] 表示从顶点i到顶点j的边权值 int vexnum, arcnum; // 图的顶点数和弧(边)数,运行时动态赋值 } MGraph;关键参数说明:
arcs[i][j]是核心。若为0,表示无边(注意:本项目约定0为无边,非INF);若为非零值(如5、7),即为边权;若为INF(32767),则作为“无穷大”占位符(常用于最短路径初始化,本例中仅在打印时识别)。vexnum必须显式赋值(如g.vexnum = 6),否则遍历会越界;arcnum同理,影响转换后结构体的元数据准确性。vexs[]数组当前为空置,但这是刻意设计——它预留了顶点携带名称、坐标等属性的接口,你后续加char name[20]或float x, y都无需改结构体定义。
2.2 邻接表结构体ALGraph:用数组+链表实现稀疏图的紧凑表达
// 边节点定义:链表的每个结点 typedef struct ArcNode { int adjvex; // 邻接点在顶点数组中的下标(即目标顶点编号) struct ArcNode *nextarc; // 指向下一个边结点的指针 int info; // 边的附加信息(如权值),本例中即权重 } ArcNode; // 顶点定义:数组的每个元素 typedef struct VNode { VertexType data; // 顶点自身信息(同MGraph.vexs[i]) ArcNode *firstarc; // 指向第一条依附该顶点的边的指针(链表头) } VNode, AdjList[MAX_VERTEX_NUM]; // AdjList 是VNode数组类型别名 // 邻接表图结构体 typedef struct { AdjList vertices; // 顶点数组,大小为MAX_VERTEX_NUM int vexnum, arcnum; // 顶点数和弧数 } ALGraph;关键参数说明:
vertices[i].firstarc是入口。它指向以顶点i为起点的所有边组成的单链表。ArcNode中adjvex存的是下标,不是顶点值本身(如顶点编号为3,则存3,不是'c'或"vertex3"),这保证了索引O(1)访问。info字段与arcs[i][j]严格对应,转换时权值零丢失(arcs[i][j]==0不生成边节点),这是本项目对“0权边是否有效”的明确约定,避免歧义。- 所有
ArcNode内存均由malloc动态申请,free释放逻辑需你自行补充(见第4章避坑)。
2.3 四大核心函数:转换逻辑全在指针游走与数组索引间完成
整个转换过程被拆解为四个职责清晰的函数,无全局变量,参数传递明确:
| 函数名 | 输入 | 输出 | 核心动作 |
|---|---|---|---|
DispMat(MGraph *g) | 邻接矩阵指针 | 控制台打印 | 遍历arcs[i][j],INF显示为---,其余显示数值 |
MatToList(MGraph *g, ALGraph *G) | 矩阵指针 + 空邻接表指针 | 填充G的vertices和arcnum/vexnum | 对每个i,扫描j,遇非零arcs[i][j]则malloc新节点,头插进vertices[i].firstarc |
DispAdj(ALGraph *G) | 邻接表指针 | 控制台打印 | 对每个i,遍历vertices[i].firstarc链表,打印所有adjvex |
ListToMat(ALGraph *G, MGraph *g) | 邻接表指针 + 空矩阵指针 | 填充g的arcs和arcnum/vexnum | 先清零arcs,再对每个i,遍历其链表,将p->adjvex作为列索引,p->info赋值给arcs[i][p->adjvex] |
为什么是头插法?
MatToList中p->nextarc = G->vertices[i].firstarc; G->vertices[i].firstarc = p;是经典头插。它保证新边总在链表最前,打印时顺序与矩阵扫描顺序(j从大到小)一致,便于人工比对。若需保持原始输入顺序,应改用尾插(需维护tail指针),但本项目优先保障可验证性而非顺序保真。
2.4 主函数流程:从静态数据初始化到双向转换验证
void main(void){ MGraph g, g1; // g:原始矩阵, g1:由邻接表转回的矩阵 ALGraph *G; // G:动态分配的邻接表(因大小不确定) int i,j; // 1. 初始化测试数据(6节点有向图,10条边) int A[][6] = { {0,5,0,7,0,0}, // 顶点0出边:到1(权5), 到3(权7) {0,0,4,0,0,0}, // 顶点1出边:到2(权4) {8,0,0,0,0,9}, // 顶点2出边:到0(权8), 到5(权9) {0,0,5,0,0,6}, // 顶点3出边:到2(权5), 到5(权6) {0,0,0,5,0,0}, // 顶点4出边:到3(权5) {3,0,0,0,1,0} // 顶点5出边:到0(权3), 到4(权1) }; g.vexnum = 6; g.arcnum = 10; // 2. 将A拷贝到g.arcs for(i=0; i<g.vexnum; i++) for(j=0; j<g.vexnum; j++) g.arcs[i][j] = A[i][j]; printf("有向图 G 的邻接矩阵为:\n"); DispMat(&g); // 打印原始矩阵 G = (ALGraph *)malloc(sizeof(ALGraph)); // 为邻接表分配内存 printf("图 G 的邻接矩阵转成邻接表:\n"); MatToList(&g, G); // 关键转换1:矩阵→邻接表 DispAdj(G); // 打印转换结果 printf("图 G 的邻接表转成邻接矩阵:\n"); ListToMat(G, &g1); // 关键转换2:邻接表→矩阵 DispMat(&g1); // 打印还原矩阵(应与原始g完全一致) getchar(); // 暂停,防止控制台闪退 }这段main的深意:它不是一个演示,而是一个自验证单元测试。
g→G→g1形成闭环,DispMat(&g)和DispMat(&g1)的输出必须逐行相同,才是转换正确的铁证。这就是为什么项目强调“双向转换”——单向只能证明能动,双向闭环才能证明没丢数据、没错位。
3. 运行实操:从零编译到手动输入任意图,三步落地你的第一个图转换程序
别被#include <stdio.h>和malloc吓住,这个项目对环境要求极低。我用 Windows 10 + MinGW-w64(gcc 13.2.0)、Ubuntu 22.04(gcc 11.4.0)、macOS Sonoma(clang 15.0)全平台实测通过。下面带你从新建文件开始,一步步跑通,并升级为交互式输入。
3.1 第一步:创建并编译源文件(5分钟搞定)
- 新建文件:用任意文本编辑器(VS Code、Notepad++、vim)新建一个文件,命名为
graph_convert.c。 - 粘贴代码:将项目正文中的全部C代码(从
#include <stdio.h>到getchar();})完整复制进去。注意:原文末尾ListToMat函数声明缺少返回类型void,需手动补上:void ListToMat(ALGraph *G, MGraph *g){ // ← 此处添加 'void' - 编译命令:
- Windows (MinGW):
gcc -o graph_convert.exe graph_convert.c - Linux/macOS:
gcc -o graph_convert graph_convert.c
- Windows (MinGW):
- 运行:双击
graph_convert.exe或终端执行./graph_convert。你应该看到:有向图 G 的邻接矩阵为: 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 的邻接矩阵转成邻接表: 0: 3 1 1: 2 2: 5 0 3: 5 2 4: 3 5: 4 0 图 G 的邻接表转成邻接矩阵: 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验证要点:最后一块矩阵必须与第一块完全一致。如果某行数字不同(如第二行变成
0 0 0 0 0 0),说明转换逻辑有bug,立即查MatToList中j循环范围或ListToMat中清零逻辑。
3.2 第二步:升级为交互式输入(替换静态数组)
原代码用int A[][6]写死图数据,现在我们把它改成用户键盘输入。修改main函数中初始化部分(替换掉int A[][6] = {...};及其后的拷贝循环):
void main(void){ MGraph g; ALGraph *G; int i, j, n; printf("请输入顶点数 n (<=20): "); scanf("%d", &n); if(n <= 0 || n > MAX_VERTEX_NUM) { printf("顶点数超出范围!\n"); return; } g.vexnum = n; // 动态构建邻接矩阵 printf("请按行输入 %d x %d 邻接矩阵(0表示无边,非0为权值):\n", n, n); for(i = 0; i < n; i++) { printf("第 %d 行 (顶点 %d 的出边): ", i+1, i); for(j = 0; j < n; j++) { scanf("%d", &g.arcs[i][j]); } } // 计算弧数(统计非零元素个数) g.arcnum = 0; for(i = 0; i < n; i++) for(j = 0; j < n; j++) if(g.arcs[i][j] != 0) g.arcnum++; printf("\n你输入的邻接矩阵为:\n"); DispMat(&g); G = (ALGraph *)malloc(sizeof(ALGraph)); printf("\n转换为邻接表:\n"); MatToList(&g, G); DispAdj(G); MGraph g1; printf("\n邻接表转回邻接矩阵:\n"); ListToMat(G, &g1); DispMat(&g1); // 释放动态内存(重要!) ArcNode *p, *q; for(i = 0; i < G->vexnum; i++) { p = G->vertices[i].firstarc; while(p != NULL) { q = p; p = p->nextarc; free(q); } } free(G); getchar(); }关键改动说明:
scanf替代硬编码,支持任意n≤20的图。g.arcnum由程序自动统计,避免人工数错。- 新增内存释放逻辑:
free(G)释放邻接表结构体本身,内层while循环释放所有ArcNode节点。这是C语言血泪经验——不释放会导致内存泄漏,在长期运行服务中是致命伤。- 编译运行后,输入
6,然后按提示逐行输入6x6矩阵,即可得到完全自定义的转换结果。
3.3 第三步:支持邻接表输入(反向交互入口)
既然能输矩阵,当然也要能输邻接表。我们新增一个InputAdjList函数,让用户按顶点顺序输入其邻接点列表:
// 在文件顶部添加函数声明 void InputAdjList(ALGraph *G); // 在main中,删除原矩阵输入部分,改为: // printf("请选择输入方式:1-邻接矩阵 2-邻接表: "); // scanf("%d", &choice); // if(choice == 1) { /* 原矩阵输入代码 */ } // else { InputAdjList(G); } // 调用新函数 void InputAdjList(ALGraph *G) { int i, j, n, k, weight; printf("请输入顶点数 n (<=20): "); scanf("%d", &n); G->vexnum = n; printf("请为每个顶点输入其邻接点(格式:邻接点编号 权值;输入-1结束):\n"); for(i = 0; i < n; i++) { printf("顶点 %d 的邻接点: ", i); G->vertices[i].firstarc = NULL; // 初始化头指针 while(1) { scanf("%d", &j); if(j == -1) break; // 结束标志 if(j < 0 || j >= n) { printf("邻接点 %d 超出范围!跳过。\n", j); continue; } scanf("%d", &weight); // 创建新边节点 ArcNode *p = (ArcNode*)malloc(sizeof(ArcNode)); p->adjvex = j; p->info = weight; p->nextarc = G->vertices[i].firstarc; // 头插 G->vertices[i].firstarc = p; } } // 统计弧数 G->arcnum = 0; ArcNode *p; for(i = 0; i < n; i++) { p = G->vertices[i].firstarc; while(p != NULL) { G->arcnum++; p = p->nextarc; } } }交互示例:输入
n=3,然后:
顶点 0 的邻接点:1 5 2 7 -1(表示0→1权5,0→2权7)
顶点 1 的邻接点:2 4 -1
顶点 2 的邻接点:-1
这样就构建了一个3节点、3条边的图,再调用ListToMat即可转为矩阵。这种输入方式更贴近真实图数据(如社交网络关注关系),是项目“手动输入任意图形”要求的终极实现。
4. 避坑指南:五个真实踩过的坑,省下你两小时调试时间
这个项目看似简单,但在指针、内存、边界上埋了几个经典陷阱。以下是我用valgrind和gdb实锤定位的5个高频问题,每个都附带复现现象、根因分析和修复方案。
4.1 现象:DispAdj打印出乱码地址(如0000000000000000)或程序崩溃
原因:MatToList中G->vertices[i].firstarc未初始化为NULL,导致DispAdj遍历时读取了栈上的随机垃圾值,p指针非法。
解决:在MatToList开头强制初始化:
for(i=0; i<n; i++) G->vertices[i].firstarc = NULL; // ← 必须有!原文漏了这行4.2 现象:ListToMat转换后的矩阵多出很多0,或与原矩阵不一致
原因:ListToMat函数中,arcs数组未初始化为0,残留了之前malloc的脏数据。g->arcs[i][j]=0;循环只覆盖了n x n范围,但MAX_VERTEX_NUM是20,若n=6,arcs[6][0]等位置仍是随机值。
解决:在ListToMat开头添加全局清零:
for(i=0; i<MAX_VERTEX_NUM; i++) for(j=0; j<MAX_VERTEX_NUM; j++) g->arcs[i][j] = 0; // ← 清零整个二维数组,不只n x n4.3 现象:输入顶点数n=10后,程序在MatToList中malloc失败,p为NULL
原因:malloc(sizeof(ArcNode))返回NULL时,代码未检查,直接解引用p->adjvex导致段错误。
解决:在MatToList的malloc后加健壮性检查:
p = (ArcNode *)malloc(sizeof(ArcNode)); if(p == NULL) { printf("内存分配失败!请减小图规模。\n"); exit(1); }4.4 现象:DispMat打印时,某行末尾多出---或数字错位
原因:DispMat中if(g->arcs[i][j]==INF)判断,但输入时并未用INF,而是用0表示无边,导致INF永远不匹配,else分支全走,printf("%3d")对0也输出,但视觉上0和---宽度不同(%3d是右对齐3字符,%3s是字符串左对齐)。
解决:统一约定0为无边,删除INF相关逻辑,DispMat改为:
if(g->arcs[i][j] == 0) printf("%3s", "---"); else printf("%3d", g->arcs[i][j]);4.5 现象:多次运行程序后,内存占用持续增长(Windows任务管理器可见)
原因:main中G = (ALGraph *)malloc(...)分配的内存,以及MatToList中malloc的所有ArcNode,在程序退出前未free。虽然进程结束会回收,但若将此模块嵌入循环服务,将导致严重泄漏。
解决:在main结尾(getchar()前)添加完整释放逻辑(见3.2节代码),必须释放ArcNode链表再释放G结构体,顺序不可颠倒。
玄学提醒:在
MatToList中j循环用for(j=n-1; j>=0; j--)是为了头插后打印顺序与矩阵行序一致。若改成j=0到n-1,打印时邻接点顺序会反转(如0: 1 3变成0: 3 1),虽逻辑正确,但人工比对时极易误判为bug。这是为可读性做的妥协,不是缺陷。
5. 二次开发:从转换工具到图算法基石,三个可立即落地的改造技巧
这个项目的价值远不止于“作业提交”。它的结构体设计和转换函数,是构建图算法的完美底座。我基于它在某高校课程设计中快速实现了Dijkstra最短路径和拓扑排序,以下是三个经过验证的改造技巧,你今天就能用上。
5.1 技巧一:为顶点添加真实属性,支撑复杂图建模
原VertexType是int,只能存编号。但现实图中顶点有名字、坐标、类型。改造MGraph和ALGraph:
// 修改顶点数据类型 typedef struct { char name[20]; // 顶点名称,如 "Beijing" float x, y; // 地理坐标 int type; // 类型:0-城市, 1-基站, 2-服务器 } VertexData; // 更新结构体 typedef struct { VertexData vexs[MAX_VERTEX_NUM]; // 顶点表存结构体 AdjMatrix arcs; int vexnum, arcnum; } MGraph; typedef struct VNode { VertexData data; // 顶点数据 ArcNode *firstarc; } VNode, AdjList[MAX_VERTEX_NUM];改造效果:输入时
scanf("%s %f %f %d", G->vertices[i].data.name, &G->vertices[i].data.x, ...),打印时printf("顶点%s (%.1f,%.1f): ", G->vertices[i].data.name, G->vertices[i].data.x, ...)。这样,你的图就从“编号0~5”变成了“北京(39.9,116.4)、上海(31.2,121.4)…”的真实地理网络,Dijkstra输出路径时自然带上城市名。
5.2 技巧二:扩展边信息,支持多权重与状态标记
ArcNode.info当前只存一个整数权值。但实际场景需要:距离+耗时、带宽+延迟、甚至边的启用状态。改造ArcNode:
typedef struct ArcNode { int adjvex; struct ArcNode *nextarc; struct { int weight; // 主权值(如距离) int time; // 次权值(如耗时) bool active; // 是否启用(true/false) } info; } ArcNode;转换函数适配:
MatToList中p->info.weight = g->arcs[i][j];,ListToMat中g->arcs[i][p->adjvex] = p->info.weight;。这样,DispAdj可打印printf("%3d(%d)", p->adjvex, p->info.time);,一条边同时承载多维信息,为QoS路由等高级算法铺路。
5.3 技巧三:封装为独立模块,提供标准API供其他算法调用
把转换逻辑抽离为graph_utils.h和graph_utils.c,对外暴露干净接口:
// graph_utils.h #ifndef GRAPH_UTILS_H #define GRAPH_UTILS_H #include <stdio.h> #include <stdlib.h> #define MAX_VERTEX_NUM 20 typedef int AdjMatrix[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; typedef struct ArcNode { /* 同前 */ } ArcNode; typedef struct VNode { /* 同前 */ } VNode, AdjList[MAX_VERTEX_NUM]; typedef struct { /* 同前 */ } ALGraph; typedef struct { /* 同前 */ } MGraph; // 标准API void MatToList(const MGraph* src, ALGraph* dst); void ListToMat(const ALGraph* src, MGraph* dst); void FreeAdjList(ALGraph* g); // 统一释放函数 void PrintMatrix(const MGraph* g); void PrintAdjList(const ALGraph* g); #endif使用示例(在你的Dijkstra文件中):
#include "graph_utils.h" int main() { MGraph g; ALGraph G; // ... 输入g ... MatToList(&g, &G); // 一行代码获得邻接表 Dijkstra(&G, 0); // 直接传给算法 FreeAdjList(&G); // 一行释放 }这样,你的图输入、存储、释放就与算法解耦。从今往后,任何新算法(BFS、Prim、Kosaraju)都只需
#include "graph_utils.h",专注核心逻辑,不用再重复写指针操作。
从那以后我每次新建图算法项目,第一件事就是cp graph_utils.* my_project/,然后#include "graph_utils.h"。它让我彻底告别了“每次写图都从 malloc 链表头开始”的重复劳动,把精力真正聚焦在算法思想本身。希望帮到你。
本文还有配套的精品资源,点击获取