严蔚敏数据结构第七章图算法C语言实战指南
发布时间:2026/10/1 17:47:24来源:尧图网络
1. 这不是“抄答案”而是用C语言重走严蔚敏数据结构的第七章实战路径你搜到这个标题时大概率正卡在第七章——图Graph的课后习题上手边摊着《数据结构C语言版 第2版》那本深蓝色封面的教材第203页开始的12道题像一堵砖墙。不是不会写是写了跑不通不是看不懂算法是调试时指针乱飞、邻接表越界、DFS递归栈溢出……我带过三届计算机专业本科生做数据结构实验几乎所有人在这章都摔过跟头。第七章之所以难不在于概念多玄奥而在于它把前六章所有底层能力全拉出来考指针的嵌套操作、动态内存的生命周期管理、递归与栈的隐式配合、结构体嵌套定义的内存对齐、文件I/O与图的持久化存储——它是一次综合压力测试。严蔚敏老师这本教材的第七章核心其实是两个骨架邻接矩阵和邻接表两种图的物理存储模型以及基于它们的四大基础操作图的创建、遍历DFS/BFS、最小生成树Prim/Kruskal、最短路径Dijkstra/Floyd。但课本里给的代码框架极简比如邻接表只定义了ArcNode和VNode结构体连CreateALGraph()函数体都是空的Dijkstra算法只画了流程图没一行可运行的C代码。学生照着抄等于在没图纸的情况下组装发动机——零件都在但不知道哪个螺丝该拧几圈、油路怎么接、点火时机怎么调。所以这份“最详细答案”本质是一份可编译、可调试、可单步跟踪的C语言工程级实现手册。它不回避malloc失败时的错误处理不跳过free释放的边界条件不省略scanf读入顶点名时的缓冲区清理甚至把printf输出格式都按考试要求对齐。我用Dev-C 5.11和VS Code MinGW实测过全部12题每道题都附带标准输入样例、预期输出截图、关键变量内存快照GDB调试截取。比如第7题“判断无向图是否连通”课本只说“用DFS遍历一次”但实际要解决如何初始化visited[]数组遍历完后怎么判断是否所有顶点都被访问如果图有多个连通分量count变量该怎么设计这些细节才是你调试两小时却找不到bug的真正原因。适合谁看不是只想交作业的同学——那抄网上零散代码更快而是准备考研王道408、备战蓝桥杯算法组、或正在写课程设计需要图算法模块的开发者。因为这里每个函数都预留了扩展接口Dijkstra()返回的不只是最短距离还有path[]路径数组方便你后续实现路径还原Kruskal()用的是并查集优化版FindRoot()函数里做了路径压缩时间复杂度严格控制在O(E log E)。你拿到的不是答案是第七章所有算法的C语言工业级参考实现代码风格贴近Linux内核链表操作习惯注释密度达到每3行代码就有1行说明连// 防止头节点指针悬空这种细节都不放过。2. 图的物理存储为什么邻接表比邻接矩阵更适合严蔚敏第七章的习题场景2.1 两种存储模型的本质差异与选择逻辑严蔚敏教材第七章课后习题的题目设置其实暗含了对存储模型的强引导。比如第1题要求“以邻接表为存储结构”第3题明确指定“邻接矩阵”而第5题“实现图的深度优先遍历”则未限定——这时你的选择就决定了后续90%的编码工作量。我做过对比测试用邻接矩阵实现一个含50个顶点、200条边的稀疏图int edges[50][50]会占用10KB内存其中96%是0而邻接表只需分配200个ArcNode节点50个VNode头结点总内存约3.2KB且遍历时跳过大量0值判断。第七章习题中90%的图都是稀疏图边数远小于顶点数的平方邻接表是更符合工程实际的选择。邻接矩阵的核心是二维数组edges[MAXVEX][MAXVEX]逻辑简单edges[i][j] 1表示Vi到Vj有边。但它有三个硬伤第一空间复杂度O(n²)当n1000时需4MB内存而多数习题图顶点数在10~50之间纯属浪费第二插入/删除边是O(1)但遍历所有邻接点却是O(n)对稀疏图效率极低第三无法直接存储边权以外的属性如边的类型、颜色、容量。而邻接表用链表模拟“每个顶点的邻居列表”VNode结构体中的firstarc指针指向第一条边ArcNode里存adjvex邻接点下标、weight权值、nextarc下一条边天然支持动态增删、权值扩展、多属性附加。提示严蔚敏教材中邻接表定义常被初学者忽略一个关键点——VNode结构体里的data字段类型。课本写的是char data但实际编程中必须改为char data[MAX_NAME_LEN]如char data[20]。否则当你输入顶点名Beijing时单个char只能存B后续字符全丢。我在第2题“创建有向网”调试时发现scanf(%s, G-vertices[i].data)崩溃根源就是G-vertices[i].data取的是单字节地址而%s要写入字符串。正确写法是scanf(%s, G-vertices[i].data)去掉取地址符。2.2 邻接表的C语言实现从结构体定义到内存分配的完整链条严蔚敏教材给出的邻接表结构体如下typedef struct ArcNode { int adjvex; struct ArcNode *nextarc; } ArcNode; typedef struct VNode { char data; ArcNode *firstarc; } VNode, AdjList[MAX_VERTEX_NUM]; typedef struct { AdjList vertices; int vexnum, arcnum; } ALGraph;这个定义在教学上简洁但直接用于工程编码会引发至少5个致命问题ArcNode缺少权值字段第七章习题第4题“求最小生成树”、第6题“求最短路径”都需边权必须添加int weight;VNode.data类型错误如前所述char data无法存字符串应改为char data[MAX_NAME_LEN]AdjList定义歧义typedef struct VNode AdjList[MAX_VERTEX_NUM]让初学者误以为AdjList是类型名实际它是数组类型别名声明图变量时应写ALGraph G;而非AdjList G;ALGraph缺少图类型标识无向图/有向图/有向网的遍历逻辑不同需添加int kind;0无向图1有向图2有向网动态内存未考虑失败处理malloc可能返回NULL但课本代码全假设成功修正后的工业级定义如下#define MAX_VERTEX_NUM 50 #define MAX_NAME_LEN 20 typedef struct ArcNode { int adjvex; // 邻接点下标 int weight; // 边权值无权图设为1 struct ArcNode *nextarc; } ArcNode; typedef struct VNode { char data[MAX_NAME_LEN]; // 顶点名称如V1,A ArcNode *firstarc; // 指向第一条边 } VNode; typedef struct { VNode vertices[MAX_VERTEX_NUM]; // 顶点数组 int vexnum, arcnum; // 顶点数、边数 int kind; // 图的类型0-无向图1-有向图2-有向网 } ALGraph;创建邻接表的关键步骤是CreateALGraph()函数。课本只给框架实际需处理输入顶点信息用for(i0; iG-vexnum; i) { scanf(%s, G-vertices[i].data); }输入边信息对每条边需malloc一个ArcNode填adjvex和weight然后头插法插入到起点顶点的链表G-vertices[v].firstarc p;无向图需插入两条边v→w和w→v有向图只插v→w注意头插法导致邻接点顺序与输入顺序相反但不影响遍历正确性。若需保持输入顺序改用尾插法需维护tail指针代码量翻倍。第七章习题不要求顺序头插法更简洁。2.3 邻接矩阵的适用场景与避坑指南虽然邻接表更常用但第七章第3题明确要求“用邻接矩阵实现图的广度优先遍历”此时必须用矩阵。其定义更简单typedef struct { char vexs[MAX_VERTEX_NUM][MAX_NAME_LEN]; // 顶点数组 int arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵 int vexnum, arcnum; int kind; // 同上 } MGraph;关键陷阱在于矩阵初始化。课本常写for(i0;in;i) for(j0;jn;j) G.arcs[i][j]0;但这是危险的——若图有权值0可能被误认为有效边权如边权为0的合法边。正确做法是用INFINITY如#define INFINITY 32767初始化无边时为无穷大有边时赋具体权值。另一个坑是输入边时的下标转换。习题常给顶点名如A,B需先用LocateVex()函数查下标int LocateVex(MGraph G, char* u) { for(int i0; iG.vexnum; i) { if(strcmp(G.vexs[i], u) 0) return i; } return -1; // 未找到 }然后G.arcs[i][j] weight;。若忘记strcmp而用比较字符串地址永远返回-1。3. 图的遍历DFS与BFS的C语言实现细节与调试技巧3.1 深度优先遍历DFS递归与非递归版本的取舍第七章第5题要求“实现图的深度优先遍历”课本只给递归伪代码。但实际考试和课程设计中必须掌握非递归版本因为递归深度受限于栈空间。一个含1000个顶点的图递归DFS可能触发栈溢出。我对比过两种实现特性递归DFS非递归DFS代码长度短20行长40行理解难度低符合数学定义高需模拟栈内存占用隐式栈不可控显式栈可监控调试便利性断点难设变量作用域窄所有变量全局可见易观察递归版本核心逻辑void DFS(ALGraph G, int v, bool visited[]) { printf(%s , G.vertices[v].data); // 访问顶点 visited[v] true; ArcNode *p G.vertices[v].firstarc; while(p ! NULL) { if(!visited[p-adjvex]) { DFS(G, p-adjvex, visited); // 递归调用 } p p-nextarc; } }看似简单但调试时常见问题visited[]数组未初始化为false导致首次调用就跳过所有顶点p-adjvex越界邻接点下标超出vexnum因ArcNode中adjvex未校验。非递归版本用栈模拟void DFS_Iterative(ALGraph G, int v) { bool visited[MAX_VERTEX_NUM] {false}; int stack[MAX_VERTEX_NUM], top -1; stack[top] v; while(top ! -1) { int cur stack[top--]; if(!visited[cur]) { printf(%s , G.vertices[cur].data); visited[cur] true; // 将所有未访问邻接点压栈逆序压入以保证输出顺序 ArcNode *p G.vertices[cur].firstarc; while(p ! NULL) { if(!visited[p-adjvex]) { stack[top] p-adjvex; } p p-nextarc; } } } }关键技巧邻接点压栈顺序决定输出顺序。若按链表顺序压入最后访问的是最后一个邻接点若逆序压入先存尾部则输出顺序与递归一致。第七章习题不要求特定顺序但考试常考“按邻接表顺序输出”此时需逆序压栈。3.2 广度优先遍历BFS队列实现与环检测逻辑第3题要求邻接矩阵的BFS第7题要求邻接表的BFS。两者核心差异仅在获取邻接点的方式矩阵用for(j0;jn;j) if(G.arcs[i][j]!0)邻接表用while(p!NULL)。但BFS有一个隐藏考点——如何判断图是否连通。课本BFS只输出遍历序列但第7题“判断无向图是否连通”需统计访问顶点数。实现逻辑int BFS_Count(ALGraph G, int start) { bool visited[MAX_VERTEX_NUM] {false}; int queue[MAX_VERTEX_NUM], front 0, rear 0; int count 0; visited[start] true; queue[rear] start; count; while(front rear) { int v queue[front]; ArcNode *p G.vertices[v].firstarc; while(p ! NULL) { if(!visited[p-adjvex]) { visited[p-adjvex] true; queue[rear] p-adjvex; count; } p p-nextarc; } } return count; } // 主函数调用if(BFS_Count(G, 0) G.vexnum) printf(连通); else printf(不连通);这里count变量是关键。初学者常犯错在while(p!NULL)循环内count导致重复计数或忘记初始化count0。更隐蔽的bug是queue数组大小——若图有50个顶点queue[MAX_VERTEX_NUM]足够但若rear超过MAX_VERTEX_NUM会越界。安全做法是加判断if(rear MAX_VERTEX_NUM) { printf(队列溢出); return -1; }3.3 遍历算法的统一接口设计避免重复造轮子第七章12道题中遍历相关占4道5、6、7、8题。若每道题都重写DFS/BFS代码冗余且易出错。我的解决方案是设计通用遍历引擎typedef void (*VisitFunc)(ALGraph*, int); void Traverse(ALGraph G, VisitFunc visit, int start, bool isDFS) { bool visited[MAX_VERTEX_NUM] {false}; if(isDFS) { DFS_Core(G, start, visited, visit); } else { BFS_Core(G, start, visited, visit); } } void DFS_Core(ALGraph G, int v, bool visited[], VisitFunc visit) { visit(G, v); visited[v] true; ArcNode *p G.vertices[v].firstarc; while(p ! NULL) { if(!visited[p-adjvex]) { DFS_Core(G, p-adjvex, visited, visit); } p p-nextarc; } }这样第5题只需传PrintVertex函数第7题传CountVertex函数第8题“求连通分量个数”传ComponentCounter函数。用函数指针解耦算法与业务逻辑是C语言高级用法的典型体现也是王道考研常考点。4. 最小生成树与最短路径Prim、Kruskal、Dijkstra的C语言落地难点4.1 Prim算法邻接矩阵下的贪心实现与边界处理第9题“用Prim算法求最小生成树”课本描述为“选一个顶点逐步加入最近的顶点”。但C语言实现需解决三个实操问题距离数组lowcost[]的初始化以v0为起点lowcost[i]存v0到vi的边权。若v0与vi无边lowcost[i]应为INFINITY不能为0。已选顶点集合adjvex[]的维护adjvex[i]记录vi在MST中邻接的顶点下标。初始时adjvex[v0]-1自身其余为v0。寻找最小lowcost的顶点时需跳过已加入MST的顶点用final[i]布尔数组标记。完整代码框架void Prim(MGraph G, int start) { int lowcost[MAX_VERTEX_NUM], adjvex[MAX_VERTEX_NUM]; bool final[MAX_VERTEX_NUM] {false}; // 初始化 for(int i0; iG.vexnum; i) { lowcost[i] G.arcs[start][i]; adjvex[i] start; final[i] false; } final[start] true; lowcost[start] 0; // 自身距离为0 // 主循环加入vexnum-1条边 for(int i1; iG.vexnum; i) { int min INFINITY, k -1; // 找最小lowcost且未加入的顶点 for(int j0; jG.vexnum; j) { if(!final[j] lowcost[j] min) { min lowcost[j]; k j; } } if(k -1) break; // 图不连通 printf(边 %s-%s 权值:%d\n, G.vexs[adjvex[k]], G.vexs[k], lowcost[k]); final[k] true; // 更新lowcost和adjvex for(int j0; jG.vexnum; j) { if(!final[j] G.arcs[k][j] lowcost[j]) { lowcost[j] G.arcs[k][j]; adjvex[j] k; } } } }常见错误lowcost[j] G.arcs[k][j]未判断G.arcs[k][j]是否为INFINITY导致min永远为INFINITY或final[k] true放在循环外导致死循环。4.2 Kruskal算法并查集的C语言实现与路径压缩第10题“用Kruskal算法求最小生成树”核心是并查集Union-Find。课本只提概念但C语言需自己实现FindRoot()和Union()。并查集数组parent[]parent[i] j表示i的父节点是jparent[i] -1表示i是根。int FindRoot(int parent[], int x) { if(parent[x] 0) return x; // 根节点 return parent[x] FindRoot(parent, parent[x]); // 路径压缩 } void Union(int parent[], int x, int y) { int rootX FindRoot(parent, x); int rootY FindRoot(parent, y); if(rootX rootY) return; // 已连通 // 按秩合并小树挂大树 if(parent[rootX] parent[rootY]) { parent[rootY] rootX; } else { if(parent[rootX] parent[rootY]) parent[rootY]--; parent[rootX] rootY; } }FindRoot的路径压缩是性能关键parent[x] FindRoot(...)将x直接连到根使后续查找接近O(1)。若不用压缩最坏情况退化为O(n)。Kruskal主流程将所有边按权值升序排序用qsort比较函数需传Edge结构体初始化parent[]为-1遍历排序后的边对每条边(u,v)若FindRoot(u) ! FindRoot(v)则加入MST并Union(u,v)实操心得边结构体定义要包含u,v,weight排序时qsort(edges, e, sizeof(Edge), cmp)cmp函数中return a.weight - b.weight。注意qsort要求比较函数返回int不能直接return a.weight b.weight。4.3 Dijkstra算法单源最短路径的数组实现与负权边陷阱第11题“用Dijkstra算法求最短路径”课本强调“不能处理负权边”但C语言实现时初学者常误以为只要边权非负就安全忽略了初始化和松弛操作的精度。Dijkstra核心是dist[]最短距离、path[]前驱顶点、final[]是否已确定最短路径三个数组。void Dijkstra(ALGraph G, int start) { int dist[MAX_VERTEX_NUM], path[MAX_VERTEX_NUM]; bool final[MAX_VERTEX_NUM] {false}; // 初始化 for(int i0; iG.vexnum; i) { dist[i] (istart) ? 0 : INFINITY; path[i] -1; final[i] false; } // 主循环 for(int i0; iG.vexnum; i) { int min INFINITY, v -1; for(int j0; jG.vexnum; j) { if(!final[j] dist[j] min) { min dist[j]; v j; } } if(v -1) break; // 不连通 final[v] true; // 松弛操作 ArcNode *p G.vertices[v].firstarc; while(p ! NULL) { int w p-adjvex; if(!final[w] dist[v] p-weight dist[w]) { dist[w] dist[v] p-weight; path[w] v; } p p-nextarc; } } // 输出结果 for(int i0; iG.vexnum; i) { if(i ! start) { printf(到%s的最短距离:%d, 路径:, G.vertices[i].data, dist[i]); PrintPath(G, path, start, i); // 递归打印路径 } } }PrintPath函数需递归回溯path[]void PrintPath(ALGraph G, int path[], int start, int end) { if(end start) { printf(%s, G.vertices[start].data); return; } PrintPath(G, path, start, path[end]); printf(-%s, G.vertices[end].data); }关键陷阱dist[v] p-weight dist[w]中的加法可能溢出。若dist[v]为INFINITY32767加任何正数都会溢出。安全写法if(!final[w] dist[v] ! INFINITY dist[v] p-weight dist[w])5. 课后习题的完整实现与调试实录从输入到输出的全流程拆解5.1 第1题邻接表创建有向图的完整输入协议题目要求“以邻接表为存储结构编写算法创建有向图”。严蔚敏教材未规定输入格式但实际编程必须定义清晰协议。我采用以下标准输入格式 第一行顶点数n边数e 第二行n个顶点名空格分隔如 A B C D 后e行每行三个值——起点名 终点名 权值如 A B 5对应代码void CreateALGraph(ALGraph *G) { printf(请输入顶点数和边数: ); scanf(%d %d, G-vexnum, G-arcnum); printf(请输入%d个顶点名: , G-vexnum); for(int i0; iG-vexnum; i) { scanf(%s, G-vertices[i].data); G-vertices[i].firstarc NULL; // 初始化头指针 } printf(请输入%d条边(起点 终点 权值): \n, G-arcnum); for(int k0; kG-arcnum; k) { char u[MAX_NAME_LEN], v[MAX_NAME_LEN]; int w; scanf(%s %s %d, u, v, w); int i LocateVex(*G, u); int j LocateVex(*G, v); if(i-1 || j-1) { printf(顶点%s或%s不存在!\n, u, v); continue; } // 头插法插入边 ArcNode *p (ArcNode*)malloc(sizeof(ArcNode)); if(p NULL) { printf(内存分配失败\n); return; } p-adjvex j; p-weight w; p-nextarc G-vertices[i].firstarc; G-vertices[i].firstarc p; } }LocateVex函数必须健壮int LocateVex(ALGraph G, char* u) { for(int i0; iG.vexnum; i) { if(strcmp(G.vertices[i].data, u) 0) return i; } return -1; }调试实录曾因scanf(%s %s %d, u, v, w)中u,v未初始化导致strcmp崩溃。解决定义char u[MAX_NAME_LEN] {0};显式初始化。5.2 第4题邻接矩阵实现无向图的最小生成树Prim输入样例6 10 v1 v2 v3 v4 v5 v6 v1 v2 6 v1 v3 1 v1 v4 5 v2 v3 5 v2 v5 3 v3 v4 5 v3 v5 6 v4 v6 4 v5 v6 6 v3 v6 2预期输出Prim边 v1-v3 权值:1 边 v3-v6 权值:2 边 v2-v5 权值:3 边 v4-v6 权值:4 边 v1-v2 权值:6总权值16关键验证点lowcost数组在每次加入新顶点后是否正确更新。例如加入v3后v3到v6的边权2应覆盖原v1到v6的INFINITY使下次选中v6。5.3 第12题综合应用——校园导航系统的核心图算法模块最后一题是开放题“设计一个校园导航系统支持查询两点间最短路径”。这要求整合前述所有算法。我的实现方案存储用邻接表校园地图稀疏输入从文件map.txt读取格式同第1题功能LoadMap()解析文件创建图FindShortestPath(char* start, char* end)调用Dijkstra返回路径字符串GetAllPaths()用DFS枚举所有路径供教学演示核心函数char* FindShortestPath(ALGraph G, char* start, char* end) { int s LocateVex(G, start); int e LocateVex(G, end); if(s-1 || e-1) return 起点或终点不存在; // 运行Dijkstra int dist[MAX_VERTEX_NUM], path[MAX_VERTEX_NUM]; bool final[MAX_VERTEX_NUM] {false}; // ... 同4.3节Dijkstra实现 ... // 构建路径字符串 static char result[1000]; result[0] \0; BuildPathString(G, path, s, e, result); return result; }BuildPathString用栈避免递归void BuildPathString(ALGraph G, int path[], int start, int end, char* res) { char stack[MAX_VERTEX_NUM][MAX_NAME_LEN]; int top -1; int cur end; while(cur ! start cur ! -1) { strcpy(stack[top], G.vertices[cur].data); cur path[cur]; } strcpy(stack[top], G.vertices[start].data); strcpy(res, stack[top--]); while(top 0) { strcat(res, -); strcat(res, stack[top--]); } }此模块已集成到某高校课程设计中支持200顶点的校园地图响应时间50ms。6. 常见问题与独家调试技巧那些课本不会告诉你的坑6.1 内存泄漏与野指针图结构体释放的完整链式清理严蔚敏教材从不提free但实际编程中CreateALGraph()分配的ArcNode内存必须释放否则程序退出时泄漏。邻接表释放是经典链表销毁问题void DestroyALGraph(ALGraph *G) { for(int i0; iG-vexnum; i) { ArcNode *p G-vertices[i].firstarc; while(p ! NULL) { ArcNode *q p; p p-nextarc; free(q); // 释放边节点 } G-vertices[i].firstarc NULL; // 头指针置空 } // 顶点数组是静态分配无需free }常见错误只释放p未保存p-nextarc导致链表断裂后无法继续释放或释放后未置firstarcNULL后续再调用Destroy时free(NULL)虽安全但逻辑混乱。实操心得在CreateALGraph末尾加printf(图创建完成共%d个顶点,%d条边\n, G-vexnum, G-arcnum);在DestroyALGraph开头加printf(开始销毁图...\n);。用GDB调试时在free(q)处设断点观察q的地址和内容确认释放的是正确节点。6.2 输入缓冲区残留scanf与gets混用的灾难第七章习题常需先输入数字再输入字符串如scanf(%d, n); for(i0; in; i) scanf(%s, name[i]);问题scanf(%d)后回车符留在缓冲区下一个scanf(%s)会读到空字符串。解决方案方法1scanf(%d%*c, n)%*c读取并丢弃一个字符回车方法2fflush(stdin)但POSIX不推荐方法3用fgets替代scanf更安全char line[100]; fgets(line, sizeof(line), stdin); sscanf(line, %d, n);6.3 GDB调试图算法的黄金三步法面对DFS无限递归或BFS队列不空我用GDB的固定流程断点设在循环入口b DFS_Core或b BFS_Corer运行n单步监控关键变量display visiteddisplay curdisplay p-adjvex让GDB自动打印检查内存状态x/10xw G.vertices[0]查看顶点数组前10个字x/5xw G.vertices[0].firstarc查看链表前5个节点曾定位到一个经典bugp p-nextarc在while(p!NULL)循环末尾但p已被free导致p-nextarc访问非法内存。修复在free(q)前先保存q-nextarc。6.4 习题答案的验证方法论不止看输出要看过程很多同学对照网上答案输出一样就认为正确。但第七章算法的正确性需多维验证验证维度方法示例功能正确性
网站建设高端定制企业官网