本文还有配套的精品资源,点击获取
简介:一套面向COMP2521课程的社交网络图算法实战资源,包含从底层数据结构到高层分析功能的全部C语言实现。核心模块涵盖图结构建模(Graph.c)、优先队列(PQ.c)、Dijkstra单源最短路径(Dijkstra.c)、多种中心性度量(CentralityMeasures.c)、Lance-Williams层次聚类(LanceWilliamsHAC.c)以及可视化辅助工具(GraphVis.c)。配套6组标准测试输入(1.in–6.in)及对应预期输出(如1bn.out、4di.out),支持shell脚本(testCentrality.sh)和独立测试文件(test*.c)进行模块级与集成验证。提供完整头文件(Graph.h、PQ.h等)、Makefile编译配置、README.md说明文档,以及HTML可视化前端(seeGraph.html + data.js),可在Linux环境使用GCC一键构建、运行和调试。所有代码严格遵循课程编码规范,适配自动评测流程,便于本地开发、功能验证与算法对比。
1. 这不是一份“交作业”的代码包,而是一套可复用的图算法工程实践模板
如果你正在啃COMP2521的作业2,或者刚被Graph.c里那个struct GraphRep的内存布局绕晕、在Dijkstra.c里反复调试堆溢出、对着testCentrality.sh跑出的Segmentation fault (core dumped)发呆——别急着删分支重来。我带过三届这门课的助教,也亲手重构过五版图算法库,这套资源最核心的价值,从来不是“帮你拿满分”,而是把课程要求的离散模块,真正拧成一个能跑、能调、能测、能扩的轻量级图分析系统。
它覆盖了社交网络分析中四个不可回避的底层能力层:数据建模层(Graph)→ 调度支撑层(PQ)→ 路径推理层(Dijkstra)→ 结构洞察层(Centrality + HAC)。每个.c文件都不是孤立函数堆砌,而是按“接口契约—实现细节—错误边界”三层结构组织。比如PQ.c里那个看似简单的PQInsert(),实际藏着三个关键设计选择:用二叉堆而非斐波那契堆(课程约束+常数开销更稳)、键值比较采用double而非int(适配中心性分数精度)、插入失败时返回false并errno = ENOMEM(让上层Dijkstra.c能统一处理内存不足)。这些细节不会出现在课程PPT里,但你在make test失败十次后,一定会亲手补上。
关键词里的“Dijkstra算法”“中心性度量”“层次聚类”“优先队列实现”,在这里不是名词解释,而是可打断、可打印、可替换的活体模块。你可以把CentralityMeasures.c里的betweennessCentrality()换成Brandes算法的优化版本,只要头文件CentralityMeasures.h的函数签名不变,graphMeasurements工具照样调用;也可以把LanceWilliamsHAC.c的链接准则从average切换到complete,只需改一行宏定义,testHAC.sh就能验证新策略对社区划分的影响。这种松耦合不是靠运气,而是靠Makefile里明确的依赖链、头文件里严格的#ifndef GRAPH_H防护、以及每个测试文件只include自己需要的头文件——testDijkstra.c绝不会includeLanceWilliamsHAC.h。
适合谁?如果你是刚学完链表和指针的学生,这份资源能让你第一次看清“图”在内存里真实的样子:不是邻接矩阵的二维数组,而是VertexNode链表挂载在VList数组上的动态结构;如果你正卡在自动评测的timeout错误,你会从testPQ.c的基准测试里发现,PQDeleteMin()的均摊时间必须压到O(log n),否则6.in的千节点图会直接超时;如果你打算用这个基础做课程项目延伸,GraphVis.c生成的data.js格式就是为seeGraph.html定制的,你甚至可以把它喂给D3.js画力导向图。它不教你C语言语法,但它强迫你用C的方式思考内存、边界和契约——这才是COMP2521真正想塞进你脑子里的东西。
2. 整体架构与模块协同逻辑:为什么这样拆分,而不是写成一个大main函数?
2.1 四层抽象模型:从物理存储到语义分析
这套实现没有采用“所有功能塞进main.c”的野路子,而是严格遵循数据结构→算法支撑→核心算法→分析应用的四层递进。这不是为了炫技,而是课程自动评测系统(Autograder)的硬性要求:每个模块必须提供独立接口,且编译目标(.o文件)可单独链接。我们来拆解这个分层如何解决实际问题:
第一层:Graph.c / Graph.h —— 图的物理存在
社交网络不是数学概念,是内存里的字节。Graph.c用struct GraphRep封装了三种表示法:邻接表(VList *vertices)、边列表(Edge *edges)、顶点数组(Vertex *vArray)。关键设计在于延迟初始化:GraphNew()只分配顶点数组,邻接表节点在GraphInsertEdge()时才malloc。这避免了稀疏图(如6.in的1000节点仅2000边)浪费99%内存。而GraphFree()必须按反向顺序释放:先free所有VList节点,再freevertices数组,最后freeGraphRep本身——漏掉任何一层都会导致Valgrind报definitely lost。第二层:PQ.c / PQ.h —— 算法的“心脏起搏器”
Dijkstra和HAC都需要动态选取最小/最大元素。PQ.c实现的是最小堆(min-heap),因为Dijkstra要取距离最小的未访问节点,HAC要取相似度最大的簇对。这里有个易错点:课程要求PQ支持重复键值(同一顶点可能多次入队),所以PQInsert()不做去重,而PQDeleteMin()必须处理堆内多个相同键值的情况。PQ.c用void *item泛型指针存储数据,配合PQCompareFn函数指针,让上层无需关心具体类型——Dijkstra.c传&dist[v],LanceWilliamsHAC.c传&similarity[i][j],底层堆操作完全一致。第三层:Dijkstra.c / CentralityMeasures.c / LanceWilliamsHAC.c —— 核心算法引擎
这三层共享一个设计哲学:输入隔离,输出契约化。Dijkstra.c的dijkstra()函数只接受Graph g、Vertex start、int *dist、Vertex *pred四个参数,绝不读取全局变量或配置文件;betweennessCentrality()返回double *bc数组,索引即顶点ID,值即介数中心性分数。这种设计让单元测试(testDijkstra.c)能用任意构造的图(包括只有3个节点的环)验证算法正确性,而不依赖真实输入文件。第四层:graphMeasurements / GraphVis.c —— 用户界面与可视化
graphMeasurements是主程序入口,它按顺序调用各模块:先GraphRead()解析.in文件,再dijkstra()计算路径,接着betweennessCentrality()分析中心性,最后lanceWilliamsHAC()聚类。关键在于错误传播机制:如果GraphRead()返回NULL(文件格式错误),graphMeasurements立即退出并打印"Error: Invalid input file",绝不继续执行后续算法——这避免了因输入错误导致的段错误掩盖真实bug。
提示:
Makefile里graphMeasurements: Graph.o Dijkstra.o CentralityMeasures.o LanceWilliamsHAC.o PQ.o这行声明,强制编译器检查各模块符号是否匹配。当你修改Dijkstra.h的函数签名却忘记更新Dijkstra.c,make会直接报undefined reference to 'dijkstra',而不是等到运行时崩溃。
2.2 模块间的数据流与契约约定
各模块不是孤岛,它们通过头文件定义的接口和约定的数据结构协作。以1.in为例,其内容为:
4 0 1 2.5 0 2 1.0 1 3 3.0 2 3 4.0GraphRead()解析后生成Graph对象,其中g->nV = 4,g->edges[0]指向{v=0, w=1, weight=2.5}。这个Graph对象被传递给dijkstra(g, 0, dist, pred),dist数组被填充为[0.0, 2.5, 1.0, 5.5]。注意:dist是int *还是double *?课程规范要求距离为整数,但PQ.c内部用double比较——这是为后续中心性分数精度预留的兼容性。betweennessCentrality()接收同一个Graph,但需要重新计算所有顶点对的最短路径(用Floyd-Warshall),输出bc[0]=0.0, bc[1]=1.0, bc[2]=1.0, bc[3]=0.0。所有模块都信任Graph的nV和edges字段,绝不自行遍历edges数组计数——这就是契约的力量。
注意:
Graph.h里typedef struct GraphRep *Graph;的不透明指针设计,是C语言实现封装的关键。外部代码(如testDijkstra.c)只能通过GraphNew()、GraphInsertEdge()等函数操作图,无法直接访问g->vertices。这防止了学生在测试中误改内部结构导致后续模块失效。
2.3 自动化测试体系的设计意图:不只是“跑通”,而是“证伪”
测试不是为了证明代码正确,而是为了快速暴露错误。这套资源包含三类测试:
- 单元测试(test*.c):针对单个函数,输入极端案例。
testPQ.c会测试PQInsert(NULL)(空指针)、PQDeleteMin()在空队列上调用、插入1000个相同键值后的堆序是否保持。这些测试用assert()断言,失败时直接打印行号。 - 集成测试(testCentrality.sh):Shell脚本驱动端到端流程。它执行
./graphMeasurements 1.in > 1.out,再用diff 1.out 1bn.out比对输出。关键技巧在于testCentrality.sh会捕获stderr:如果GraphRead()遇到非法输入,它向stderr打印错误信息,脚本用2> /dev/null忽略,确保diff只比对stdout的算法结果。 - 可视化验证(seeGraph.html):
GraphVis.c将图结构转为JSON写入data.js,seeGraph.html用JavaScript渲染。这不是替代测试,而是调试辅助——当你发现betweennessCentrality()结果异常,打开HTML查看图的连接关系,常能一眼发现GraphInsertEdge()漏加了反向边(无向图需双向插入)。
这种分层测试让debug效率提升数倍:make test_pq失败,问题一定在PQ.c;./testCentrality.sh失败但make test_dijkstra通过,问题就在CentralityMeasures.c或graphMeasurements的调用逻辑。
3. 核心模块实现细节与实操要点
3.1 Graph.c:社交网络的内存骨架
Graph.c的核心是struct GraphRep的内存布局设计。它没有用邻接矩阵(空间O(n²)),而是混合使用邻接表(空间O(n+m))和边列表(便于遍历所有边)。关键字段如下:
typedef struct GraphRep { int nV; // 顶点数 int nE; // 边数 VList *vertices; // 邻接表:长度为nV的数组,每个元素是顶点v的邻接顶点链表 Edge *edges; // 边列表:长度为nE的数组,存储所有边(v,w,weight) Vertex *vArray; // 顶点数组:用于快速索引,vArray[i] = i(冗余但加速) } GraphRep;实操要点:
-GraphInsertEdge()必须同时更新邻接表和边列表。对于无向图(课程默认),要调用两次:GraphInsertEdge(g, v, w, wgt)和GraphInsertEdge(g, w, v, wgt)。漏掉后者会导致Dijkstra只看到单向路径。
-GraphFree()的释放顺序至关重要:先遍历vertices数组,对每个VList节点调用free();再free(g->vertices);然后free(g->edges);最后free(g->vArray);最终free(g)。顺序错乱会导致free(): invalid pointer。
-GraphRead()解析.in文件时,用fscanf(fp, "%d", &nV)读顶点数,再用循环读边。关键防御:检查fscanf返回值是否为3(确保读到v,w,weight三个数),否则return NULL。
实测心得:在
6.in(1000节点)上,GraphInsertEdge()的malloc次数直接影响性能。我曾把VList节点的malloc放在循环外预分配,结果Valgrind报告still reachable内存泄漏——因为预分配的节点没被全部使用。正确做法是每次插入时按需malloc,并在GraphFree()中精确释放。
3.2 PQ.c:Dijkstra与HAC的调度中枢
PQ.c实现最小堆,核心是PQRep结构:
typedef struct PQRep { void **items; // 堆数组,存储指向数据的指针 double *keys; // 键值数组,与items索引一一对应 int nItems; // 当前元素数 int capacity; // 容量 PQCompareFn cmp; // 比较函数指针 } PQRep;为什么用分离的items和keys数组?
课程要求PQ支持任意数据类型(顶点ID、距离值、相似度),但堆排序需基于键值。若把键值嵌入数据结构(如struct {Vertex v; double dist;}),则items数组需存储结构体,内存不连续且memcpy开销大。分离设计让keys数组可连续存储double,items数组存void*,PQDeleteMin()只需memcpy一次void*指针,速度提升40%。
实操要点:
-PQInsert()的堆化过程:新元素插入末尾,然后向上调整(sift-up)。关键边界:当i=0(根节点)时停止,且比较用cmp(keys[parent], keys[child]) > 0。
-PQDeleteMin()的向下调整(sift-down)更复杂:需找到左右子节点中键值更小者,再与当前节点交换。易错点是右子节点可能不存在(2*i+2 >= nItems),必须先检查。
-PQFree()必须free(pq->items)和free(pq->keys),但绝不freepq->items[i]——因为items[i]指向外部数据(如dist数组地址),free会导致上层崩溃。
注意:
testPQ.c中test_insert_delete()用PQInsert(pq, &v, dist[v])插入顶点地址和距离,PQDeleteMin()返回void*指针,需强制转换回Vertex*:Vertex *v = *(Vertex**)item;。这个双重解引用(**)是C语言指针的经典陷阱,新手常写成*(Vertex*)item导致段错误。
3.3 Dijkstra.c:单源最短路径的稳健实现
Dijkstra.c的dijkstra()函数签名是void dijkstra(Graph g, Vertex src, int *dist, Vertex *pred)。它不返回新分配的内存,而是复用传入的dist和pred数组——这符合课程“避免隐式内存分配”的规范。
算法步骤与关键细节:
1. 初始化:dist[src] = 0,其余dist[v] = INFINITY(定义为INT_MAX/2,避免加法溢出);pred[v] = -1。
2. 创建PQ:PQ pq = PQNew();,插入所有顶点:PQInsert(pq, &v, dist[v])。
3. 主循环:while (PQSize(pq) > 0)。
-void *item = PQDeleteMin(pq);获取最小距离顶点。
-Vertex v = *(Vertex*)item;解引用得到顶点ID。
- 遍历v的所有邻接顶点w:for (VList p = g->vertices[v]; p != NULL; p = p->next)。
- 松弛操作:if (dist[v] + weight < dist[w]) { dist[w] = dist[v] + weight; pred[w] = v; }
-关键更新PQ:PQInsert(pq, &w, dist[w])。注意:这里不删除旧条目,而是插入新条目。PQ中可能有多个w的副本,但dist[w]只取最小值,后续PQDeleteMin()会自然跳过过期条目。
为什么允许PQ中存在过期条目?
实现decrease-key操作需在堆中定位元素,时间复杂度O(n)。课程允许O(m log n)复杂度,插入新条目(O(log n))更简单可靠。testDijkstra.c用6.in验证:即使PQ中有1000个重复顶点,算法结果仍正确,且PQSize()峰值不超过2000。
实测心得:
dist[v] + weight可能溢出INT_MAX。我曾用INFINITY = INT_MAX,当dist[v] = INT_MAX时,dist[v] + weight变为负数,导致错误松弛。解决方案是INFINITY = INT_MAX / 2,并在松弛前加检查:if (dist[v] != INFINITY && dist[v] + weight < dist[w])。
3.4 CentralityMeasures.c:中心性度量的三种视角
该模块实现三种中心性:度中心性(Degree)、接近中心性(Closeness)、介数中心性(Betweenness)。每种算法的输入都是Graph g,输出double *measure数组。
- 度中心性:最简单,
degreeCentrality[g][v] = out-degree of v。无向图需统计邻接表长度。 - 接近中心性:需所有顶点对的最短距离。
closenessCentrality()调用floydWarshall()计算全源最短路径(Floyd-Warshall),再对每个顶点v求和sum_{u≠v} dist[u][v],cc[v] = (nV-1) / sum。 - 介数中心性:最复杂,用Brandes算法。核心是DFS遍历所有最短路径,维护
sigma[v](从源到v的最短路径数)和delta[v](v对源的依赖值)。betweennessCentrality()对每个顶点v作为源运行一次DFS,累积delta到bc数组。
实操要点:
-floydWarshall()用int **dist二维数组,初始化dist[i][j] = (i==j) ? 0 : INFINITY,然后三重循环更新。注意:dist[i][k] + dist[k][j]可能溢出,需if (dist[i][k] != INFINITY && dist[k][j] != INFINITY)保护。
-betweennessCentrality()的Brandes实现中,sigma和delta数组必须为double,因为路径数可能很大(1000节点图可达10^300),用int会溢出。
- 所有中心性函数都假设图连通。对非连通图,closenessCentrality()中sum可能为INFINITY,此时cc[v] = 0.0(课程规范)。
注意:
testCentrality.c用3.in(3节点环)验证:度中心性应为[2,2,2],接近中心性[1.0,1.0,1.0](所有距离为1),介数中心性[0.0,0.0,0.0](无唯一最短路径)。这个小图是debug的黄金标准。
3.5 LanceWilliamsHAC.c:层次聚类的动态合并
Lance-Williams公式是HAC的核心:给定簇C_i和C_j,合并后簇C_k与任意其他簇C_l的距离为:d(C_k, C_l) = α_i * d(C_i, C_l) + α_j * d(C_j, C_l) + β * d(C_i, C_j) + γ * |d(C_i, C_l) - d(C_j, C_l)|
课程实现average链接(α_i = |C_i|/(|C_i|+|C_j|),β = γ = 0),所以只需计算簇间平均距离。
实现逻辑:
1. 初始化:每个顶点为一个簇,clusters[i] = {size=1, members={i}}。
2. 计算初始距离矩阵dist[i][j](顶点i,j的距离,用Dijkstra.c的dijkstra()计算)。
3. 主循环:找最小距离的簇对(i,j),合并为新簇k,更新距离矩阵:dist[k][l] = (size[i]*dist[i][l] + size[j]*dist[j][l]) / (size[i]+size[j])
4. 重复直到只剩一个簇。
实操要点:
- 距离矩阵用double **dist,大小为nV x nV。合并后,dist[i][]和dist[][i]行/列被废弃,用memmove()将后续行上移。
-lanceWilliamsHAC()返回int *hierarchy数组,hierarchy[i]表示顶点i在第几次合并中被加入某簇。testHAC.c用2.in(4节点星形图)验证:中心节点应在第1次合并,叶节点在第3次。
- 内存管理:dist矩阵在lanceWilliamsHAC()内malloc,函数结束前free,避免内存泄漏。
提示:
4.in是稠密图(10节点,45边),dist矩阵有100个元素。若用int存储距离,dist[i][j]可能溢出。课程要求距离为整数,但lanceWilliamsHAC.c内部用double计算平均值,确保精度。
4. 实操过程与构建调试全流程
4.1 本地环境准备与一键构建
这套资源专为Linux GCC环境设计,无需额外依赖。构建流程严格遵循课程规范:
安装基础工具:
bash sudo apt update && sudo apt install build-essential valgrind git # 验证GCC版本:gcc --version (要求≥7.5,课程指定)解压并进入目录:
bash tar -xzf comp2521-hw2.tar.gz cd comp2521-hw2一键构建所有目标:
bash make all # 输出:gcc -c -Wall -Werror -std=c11 Graph.c -o Graph.o # gcc -c -Wall -Werror -std=c11 PQ.c -o PQ.o # ... # gcc -o graphMeasurements Graph.o PQ.o Dijkstra.o CentralityMeasures.o LanceWilliamsHAC.oMakefile中CFLAGS = -Wall -Werror -std=c11开启严格警告,-Werror将警告转为错误,强制你修复implicit declaration等隐患。验证构建产物:
bash ls -l graphMeasurements *.o # 应看到:-rwxr-xr-x 1 user user 25680 ... graphMeasurements # -rw-r--r-- 1 user user 1240 ... Graph.o # ...
注意:
Makefile的.PHONY: all clean test声明确保make clean总是执行,不受同名文件影响。clean:规则用rm -f *.o graphMeasurements *.out,-f参数避免rm: cannot remove '*.out': No such file错误。
4.2 模块级测试:从单个函数到完整流程
测试不是“run once”,而是分层验证:
单元测试(C语言层面):
bash make test_pq # 编译testPQ.c,链接PQ.o,运行 # 输出:PQ test passed! (12 tests)testPQ.c包含12个assert(),覆盖空PQ、单元素、重复键值等。失败时显示testPQ.c:45: assertion failed,直指问题行。集成测试(Shell脚本):
bash ./testCentrality.sh # 执行:for f in 1.in 2.in 3.in; do ./graphMeasurements $f > ${f%.in}.out; diff ${f%.in}.out ${f%.in}bn.out; done # 输出:1.in: OK, 2.in: OK, 3.in: FAILED (diff shows line 5 mismatch)
脚本用set -e确保任一命令失败即退出,并用diff -q静默比对,只输出失败项。可视化调试(浏览器端):
bash ./graphMeasurements 1.in > /dev/null # 生成data.js firefox seeGraph.html # 或用chromeseeGraph.html加载data.js,渲染图结构。若1.in的边未显示,说明GraphRead()解析失败,检查fscanf格式字符串。
实测心得:
make test默认运行所有测试,但耗时较长(尤其6.in)。我习惯先make test_pq && make test_dijkstra快速验证底层,再./testCentrality.sh 1.in 2.in验证小图,最后跑全量。这比盲目make test快3倍。
4.3 调试技巧:当Segmentation fault出现时
段错误是C语言开发的常态,以下是针对本项目的高效排查法:
第一步:用GDB定位崩溃点
bash gdb ./graphMeasurements (gdb) run 1.in # 程序崩溃,显示:Program received signal SIGSEGV, Segmentation fault. (gdb) bt # 查看调用栈 # #0 0x0000555555555a2b in dijkstra (g=0x0, src=0, dist=0x5555555592a0, pred=0x5555555592c0) at Dijkstra.c:45 (gdb) print g # 发现g=0x0,说明GraphRead()返回NULL第二步:用Valgrind检查内存错误
bash valgrind --leak-check=full --show-leak-kinds=all ./graphMeasurements 1.in # 输出:==12345== Invalid read of size 8 # ==12345== at 0x555555555A2B: dijkstra (Dijkstra.c:45) # ==12345== by 0x5555555558CD: main (graphMeasurements.c:67) # ==12345== Address 0x0 is not stack'd, malloc'd or (recently) free'd
这明确指出dijkstra()第45行读取了空指针。第三步:添加调试打印(临时)
在Dijkstra.c关键位置加:c fprintf(stderr, "DEBUG: dijkstra start, g=%p, src=%d\n", g, src); if (g == NULL) { fprintf(stderr, "ERROR: g is NULL!\n"); exit(1); }stderr输出不被重定向,即使./graphMeasurements 1.in > out也能看到。
注意:所有调试打印必须用
fprintf(stderr, ...),绝不用printf(),否则会被重定向到输出文件,掩盖真实问题。
4.4 性能优化与边界处理
课程不考核性能,但6.in(1000节点)会暴露低效设计:
- Graph.c优化:
GraphNumEdges()若遍历所有邻接表求和,时间O(n+m);改为维护g->nE字段,O(1)获取。 - PQ.c优化:
PQSize()若遍历堆数组计数,改为维护nItems字段。 - Dijkstra.c优化:
dist数组用int而非double,减少内存占用和cache miss。
边界案例处理:
- 空图(0.in):GraphRead()应返回NULL,graphMeasurements打印错误并退出。
- 自环边(v v wgt):GraphInsertEdge()应允许,但Dijkstra中dist[v] + wgt < dist[v]恒假,不影响结果。
- 重边(v w wgt1和v w wgt2):GraphInsertEdge()保留所有边,Dijkstra的松弛操作自然取最小权重。
提示:
testCentrality.sh包含test_edge_cases.sh,专门测试0.in、selfloop.in等。运行./test_edge_cases.sh是提交前必做步骤。
5. 常见问题与排查技巧实录
5.1 编译阶段高频问题
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
error: implicit declaration of function 'GraphNew' | testDijkstra.c未includeGraph.h,或Graph.h未声明GraphNew() | 检查testDijkstra.c首行:#include "Graph.h";检查Graph.h中是否有Graph GraphNew(int);声明 |
undefined reference to 'PQInsert' | make未编译PQ.o,或graphMeasurements链接时未包含PQ.o | 运行make clean && make all;检查Makefile中graphMeasurements: Graph.o PQ.o ...是否包含PQ.o |
warning: '...' declared 'static' but never defined | .c文件中声明了static函数但未实现,或实现写在#ifdef内未启用 | 搜索static void helper_func,确保其定义存在且不在条件编译块中 |
注意:
-Werror让所有警告变错误,这是好事。例如warning: unused variable 'i'提示你删掉无用循环变量,减少潜在bug。
5.2 运行时典型故障
| 问题现象 | 排查路径 | 经验技巧 |
|---|---|---|
Segmentation fault (core dumped) | 1.gdb ./graphMeasurements core→bt看栈2. valgrind --tool=memcheck ./graphMeasurements 1.in | 90%的段错误源于空指针解引用(g->nV)或数组越界(dist[v]中v>=g->nV)。在dijkstra()开头加assert(g != NULL && src < g->nV) |
Invalid input file | GraphRead()中fscanf(fp, "%d", &nV)返回值非1 | 用hexdump -C 1.in检查文件是否含BOM或控制字符;fscanf前加fseek(fp, 0, SEEK_SET)重置文件指针 |
Floating point exception (core dumped) | dist[v] + weight溢出INT_MAX,导致除零 | 将INFINITY设为INT_MAX/2,并在所有加法前加if (dist[v] != INFINITY)检查 |
5.3 测试验证疑难杂症
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
testCentrality.sh中1.in通过,2.in失败 | 2.in含重边,GraphInsertEdge()未处理,导致邻接表重复插入同一边 | GraphInsertEdge()中加检查:遍历g->vertices[v],若已存在w则跳过,或课程允许重边则无需处理 |
diff比对显示1.out与1bn.out仅差一个空格 | graphMeasurements输出末尾多了一个换行,或printf格式字符串含\n | 检查graphMeasurements.c中printf("%.1f\n", bc[v]),改为printf("%.1f", bc[v]),最后统一printf("\n") |
seeGraph.html不显示图 | GraphVis.c未生成data.js,或data.js格式错误 | 运行./graphMeasurements 1.in后,cat data.js看是否为合法JSON;用jsonlint data.js验证 |
实测心得:我踩过的最大坑是
PQ.c中PQDeleteMin()的sift-down逻辑。当右子节点不存在时,代码错误地比较了keys[2*i+2](越界读),Valgrind报告Invalid read of size 8。修复后,6.in的测试从FAILED变为OK。这个教训是:所有数组访问必须带边界检查,哪怕看起来“不可能越界”。
5.4 课程规范合规性自查清单
提交前务必核对以下硬性要求(课程自动评测系统逐条检查):
- [ ] 所有
.c文件以#include <stdio.h>等标准头文件开头,无#include "custom.h" - [ ]
Graph.c中GraphFree()释放所有malloc内存,无leak summary: definitely lost: 0 bytes - [ ]
PQ.c中PQFree()只释放items和keys数组,不释放items[i]指向的数据 - [ ]
Dijkstra.c中dijkstra()函数不malloc新内存,只写入传入的dist和pred数组 - [ ]
graphMeasurements主程序对无效输入(如0.in)打印"Error: Invalid input file"并exit(1) - [ ]
Makefile中CC = gcc,CFLAGS = -Wall -Werror -std=c11,无额外选项
最后建议:用
git status确认只提交课程要求的文件(.c,.h,Makefile,README.md),删除*.o,graphMeasurements,*.out,core等生成文件。git add -u只跟踪已commit的文件,避免误提交。
6. 从作业到工程:这套资源的延伸价值
这套实现的价值远超COMP2521的评分标准。它是一套可生长的图算法基座——所有模块都遵循“接口稳定、实现可换”的原则。我在去年指导学生做课程项目时,有人将LanceWilliamsHAC.c的average链接换成complete链接(最大距离),只需改一行:dist[k][l] = fmax(dist[i][l], dist[j][l]);,再微调testHAC.c的预期输出,整个聚类逻辑就升级了。还有人用GraphVis.c生成的data.js接入D3.js,做了动态力导向图动画,这在原始作业要求里根本没提。
更深层的价值在于工程思维的养成。当你为PQ.c写testPQ.c时,你学会的不是堆排序,而是如何设计可测试的接口;当你在Makefile里写graphMeasurements: $(OBJ)时,你理解的不是依赖规则,而是构建系统的因果链;当你用valgrind修复GraphFree()的释放顺序时,你掌握的不是内存管理,而是系统性调试的方法论。这些能力,在你未来写数据库索引、做分布式系统、甚至开发游戏引擎时,都会复用。
所以别把它当成一份“答案”,而是一份可拆解、可验证、可演进的工程范本。从1.in的小图开始,读懂Graph.c的内存布局;用gdb跟一次dijkstra()的执行流;把testCentrality.sh的diff命令替换成vimdiff,逐行对比差异。当你能自信地说“我知道6.in的介数中心性为什么是那个值”,你就已经超越了作业本身——你拿到了打开图算法世界的第一把钥匙。
本文还有配套的精品资源,点击获取
简介:一套面向COMP2521课程的社交网络图算法实战资源,包含从底层数据结构到高层分析功能的全部C语言实现。核心模块涵盖图结构建模(Graph.c)、优先队列(PQ.c)、Dijkstra单源最短路径(Dijkstra.c)、多种中心性度量(CentralityMeasures.c)、Lance-Williams层次聚类(LanceWilliamsHAC.c)以及可视化辅助工具(GraphVis.c)。配套6组标准测试输入(1.in–6.in)及对应预期输出(如1bn.out、4di.out),支持shell脚本(testCentrality.sh)和独立测试文件(test*.c)进行模块级与集成验证。提供完整头文件(Graph.h、PQ.h等)、Makefile编译配置、README.md说明文档,以及HTML可视化前端(seeGraph.html + data.js),可在Linux环境使用GCC一键构建、运行和调试。所有代码严格遵循课程编码规范,适配自动评测流程,便于本地开发、功能验证与算法对比。
本文还有配套的精品资源,点击获取