☰
C/C++数据结构实战代码包:28个可调试可修改的算法实现
2026/10/6 8:17:12 网站建设 项目流程

简介:本资源是一套面向计算机专业本科生及数据结构初学者的C/C++代码实现合集,聚焦算法与核心数据结构的手动编码实践,有效解决课程设计、实验课作业及考研上机题中的常见实现难点。压缩包共35个文件,含34个.cpp源码文件与1个.md说明文档,总大小仅28KB,轻量易用;其中BFS、DFS、Dijkstra、Floyd等图算法,Kruskal与Prim最小生成树,哈夫曼树、线索二叉树、广义表、十字链表等进阶结构均有完整可运行代码,顺序表、链表、栈、队列、串、矩阵等基础模块亦全覆盖。已有369人学习下载,代码风格统一、注释清晰,配套md文档梳理了各算法原理与使用要点,所有文件均经实际编译验证,可直接用于调试、教学演示或课设参考,是夯实数据结构动手能力的实用型代码基座。

1. 这不是“抄作业”的代码包,而是一套能跑通、能调试、能改出自己逻辑的数据结构C/C++实战实现

你是不是也经历过:教材上讲栈是“后进先出”,但写完push()和pop()一运行就段错误;看懂了Kruskal算法的贪心思路,可union-find并查集里路径压缩到底该放find()里还是union()里,一改就崩;拓扑排序手动画图没问题,代码里indegree[]数组初始化漏了个0,整个循环卡死在第一个节点——不是不会,是缺一个能真实编译、单步调试、对照输出反推逻辑的最小可运行载体。这个.rar包里没有PPT、不讲时间复杂度推导、不塞满注释说教,它只做一件事:把《数据结构(C语言版)》严蔚敏那套经典体系,用纯C和少量C++特性(如new/delete替代malloc/free)落地成28个独立.cpp文件 + 1份数据结构.md说明文档。每个文件对应一个核心结构或算法,从最基础的顺序表、单链表,到图论里的Dijkstra、Floyd、Prim、Kruskal,再到树相关的哈夫曼编码、线索二叉树、关键路径,甚至冷门但考试常考的广义表、十字链表、邻接多重表——全都有。它适合两类人:一是正在啃《数据结构408》或校内期末复习、需要快速验证自己手写代码逻辑是否正确的同学;二是刚学完指针和结构体、想用真实项目练手、拒绝“Hello World”式玩具代码的C/C++初学者。它不承诺“一键运行”,但保证每个.cpp文件都自带main()函数、输入样例和清晰输出格式,你只需要一个支持C++11的编译器(g++或MSVC),就能立刻看到结果、打断点、改参数、加printf——这才是数据结构学习该有的手感。


2. 从编译环境到代码组织:为什么这28个文件能真正“跑起来”,而不是一堆静态文本

2.1 编译环境选择与最小依赖确认:g++ 7.5+ 或 Visual Studio 2019 是黄金组合

这个包里的所有.cpp文件,默认按C++11标准编写,核心依赖仅限于标准库头文件:<iostream>、<vector>、<stack>、<queue>、<algorithm>、<climits>等。没有使用Boost、STL以外的第三方库,也没有调用Windows API或POSIX系统调用。这意味着你不需要安装任何额外SDK,只要满足以下任一条件即可编译:

  • Linux/macOS终端:g++ -std=c++11 -o xxx xxx.cpp(推荐g++ 7.5以上,避免std::to_string等兼容性问题)
  • Windows命令行:安装Visual Studio 2019或更高版本,直接用cl.exe(VS自带)编译,或使用MinGW-w64(需确保-std=c++11生效)

注意:不要用Turbo C++或老版TC 2.0——这些环境不支持std::vector、nullptr、范围for循环等现代C++语法,强行编译会报大量语法错误。如果你还在用TC,请先切换到VS Code + MinGW或VS Community,这是2024年数据结构实操的底线配置。

2.2 文件命名与功能映射:28个文件不是随机堆砌,而是按“结构→操作→算法”三级分层

作者将28个文件按教学逻辑分组,而非按字母序排列。我重新梳理了它们的内在层级关系,方便你按需定位:

类别文件名(节选)核心作用典型输入/输出特征
基础线性结构顺序表.cpp,单链表.cpp,双向链表.cpp,栈.cpp,链栈.cpp,队列.cpp,链队.cpp,串.cpp实现ADT(抽象数据类型)的物理存储与基本操作输入多为数字序列或字符序列;输出含Length: 5,Top: 10,Front: 3等状态快照
树与二叉树二叉树.cpp,线索二叉树.cpp,哈夫曼树.cpp,哈夫曼树编码.cpp构建、遍历、线索化、最优编码生成二叉树.cpp输出前中后序遍历序列;哈夫曼树编码.cpp输出字符编码表如a: 00, b: 01, c: 1
图及其算法邻接矩阵创建图.cpp,邻接表创建图.cpp,邻接多重表.cpp,十字链表.cpp,BFS.cpp,DFS.cpp,Dijkstra.cpp,Floyd.cpp,Prim.cpp,Kruskal.cpp,拓扑排序.cpp,关键路径.cpp图的四种存储结构 + 六大经典算法输入含顶点数、边数、权值矩阵;输出如Shortest Path: 0->2->4, Cost=15或Topological Order: 0 1 3 2 4
特殊结构与应用矩阵.cpp,广义表.cpp,Hanoi.cpp,舞伴问题.cpp,表达式求值.cpp,括号的匹配.cpp,数制的转换.cpp,表合并.cpp解决特定场景问题,强化递归与栈应用表达式求值.cpp支持3+5*2-8/4;舞伴问题.cpp模拟队列配对逻辑

这份结构不是作者随手写的,而是严格遵循《数据结构(C语言版)》第2章到第7章的知识脉络。比如邻接矩阵创建图.cpp必然在Dijkstra.cpp之前——因为后者直接复用前者构建的Graph结构体。这种强耦合性意味着:你不能孤立地只编译Dijkstra.cpp,必须先确认邻接矩阵创建图.cpp已成功运行并理解其MGraph定义。

2.3数据结构.md:不是README,而是28个文件的“接口说明书”与调试指南

这个Markdown文件是整包的灵魂。它不罗列代码,而是用表格形式明确每个.cpp文件的:

  • 输入格式规范:例如Dijkstra.cpp要求第一行输入顶点数n,第二行输入源点v0,随后n行每行n个整数构成邻接矩阵(∞用-1表示);
  • 输出字段定义:Prim.cpp输出Edge: (0,1) Weight: 5表示边0→1权值为5,Total Cost: 23为最小生成树总权;
  • 关键变量说明:线索二叉树.cpp中ltag/rtag取值含义(0=指针,1=线索)、ThBiTree结构体内存布局;
  • 调试断点建议:在Kruskal.cpp的sort(edges, edges+e, cmp)后加printf("Sorted edges:\n"),验证边排序是否正确;在unionSet()函数入口打印parent[i]数组,观察并查集状态变化。

提示:数据结构.md里有一句被加粗的话:“所有图算法文件均假设图已通过邻接矩阵创建图.cpp或邻接表创建图.cpp构建完成,勿直接修改图结构体定义”。这意味着如果你要改Dijkstra.cpp的邻接表版本,必须同步修改邻接表创建图.cpp中的ALGraph定义,并确保Dijkstra_AL.cpp(包里没提供,需你自建)与之匹配——这是作者埋下的第一个协作契约。

2.4 一个典型工作流:以单链表.cpp为例,走通从编译到调试的完整闭环

我们拿最基础的单链表.cpp实操一遍,验证这套代码的真实可用性:

# 步骤1:进入解压目录,确认文件存在 ls -l *.cpp | head -5 # 输出应含:单链表.cpp 顺序表.cpp 栈.cpp 队列.cpp ... # 步骤2:编译(以g++为例) g++ -std=c++11 -o singlelist 单链表.cpp # 步骤3:运行,观察交互式输入提示 ./singlelist # 控制台输出: # === 单链表基本操作演示 === # 请输入链表长度: # 请输入5个元素(空格分隔): # 1 2 3 4 5 # 创建成功!当前链表: 1 -> 2 -> 3 -> 4 -> 5 -> NULL # 请选择操作:1.插入 2.删除 3.查找 4.遍历 0.退出

此时你输入1,再输入位置3和值99,程序会输出插入成功!新链表: 1 -> 2 -> 99 -> 3 -> 4 -> 5 -> NULL。关键在于,这个输出不是硬编码的字符串,而是由LinkList类的Insert()成员函数实时计算并打印的。你可以用VS Code打开单链表.cpp,在Insert()函数第一行加printf("DEBUG: Insert pos=%d, val=%d\n", i, e);,重新编译运行,就能看到调试日志——这证明代码是活的,不是截图。


3. 为什么BFS.cpp和DFS.cpp必须配对使用?图算法的三大隐性依赖与初始化陷阱

3.1 图结构体的“三重身份”:同一个MGraph,在不同算法里承担不同角色

BFS.cpp和DFS.cpp表面看都是遍历算法,但它们对图结构体的依赖方式截然不同。包里提供的邻接矩阵创建图.cpp定义了全局结构体:

#define MAX_VERTEX_NUM 20 typedef struct { char vexs[MAX_VERTEX_NUM]; // 顶点信息(本包中多数未使用,留作扩展) int arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵,arcs[i][j] = 权值 int vexnum, arcnum; // 顶点数、边数 } MGraph;

这个MGraph在BFS.cpp中仅被当作静态数据容器:BFS()函数只读取arcs[][]判断连通性,不修改任何字段;但在DFS.cpp中,它却成了状态记录器:DFS()内部会动态维护一个visited[]数组(局部变量),而DFS_Traverse()主函数则依赖MGraph的vexnum来初始化该数组。更隐蔽的是,关键路径.cpp和拓扑排序.cpp会复用同一份MGraph实例,但要求arcs[i][j]存储的是活动持续时间而非简单连通标志——这意味着:你不能把BFS.cpp的测试数据直接喂给关键路径.cpp,必须先按AOE网语义重填arcs[][]。

3.2 初始化的“静默失败”:arcs[][]未清零导致Floyd.cpp输出全为0

Floyd.cpp实现弗洛伊德算法求任意两点最短路径,其核心是三重循环:

for(k = 0; k < G.vexnum; k++) for(i = 0; i < G.vexnum; i++) for(j = 0; j < G.vexnum; j++) if(G.arcs[i][k] != INF && G.arcs[k][j] != INF && G.arcs[i][k] + G.arcs[k][j] < G.arcs[i][j]) G.arcs[i][j] = G.arcs[i][k] + G.arcs[k][j];

这里INF定义为INT_MAX/2(防止溢出)。但如果邻接矩阵创建图.cpp在读入数据后,没有显式将arcs[i][j]初始化为INF(当i≠j且无边时),那么arcs[i][j]将保持内存垃圾值。Floyd.cpp的if条件G.arcs[i][k] != INF永远为假,最终G.arcs[i][j]不变,输出全是0。这个Bug不会报错,只会让你以为算法失效。解决方案是在邻接矩阵创建图.cpp的CreateGraph()函数开头加:

// 初始化邻接矩阵为INF for(i = 0; i < G.vexnum; i++) for(j = 0; j < G.vexnum; j++) G.arcs[i][j] = (i==j) ? 0 : INF; // 对角线为0,其余为INF

3.3visited[]数组的生命周期陷阱:DFS.cpp递归调用中栈溢出的根源

DFS.cpp采用递归实现深度优先搜索,其DFS()函数签名是:

void DFS(MGraph G, int v, bool visited[]) { ... }

注意visited[]是传入的数组指针,而非函数内部分配。如果在main()中这样写:

bool visited[MAX_VERTEX_NUM]; DFS(G, 0, visited);

一切正常。但若误写为:

bool *visited = new bool[G.vexnum]; // 动态分配 DFS(G, 0, visited); delete[] visited; // 错!DFS递归中可能多次访问visited,delete过早释放

程序会在第二次递归调用时访问已释放内存,导致未定义行为(常见表现:输出乱码、程序崩溃、或看似正常但结果错误)。血泪经验:所有图遍历算法的visited[]必须在main()作用域内静态声明,或用std::vector<bool>管理生命周期。

3.4 避坑:图算法四大高频翻车点与现场排查法

现象原因解决方案
Dijkstra.cpp输出路径为空,或dist[]全为INF源点v0输入超出[0, vexnum-1]范围,或邻接矩阵中源点所在行全为INF(无出边)在Dijkstra()函数开头加assert(v0 >= 0 && v0 < G.vexnum);检查输入矩阵第v0行是否有非INF值
Kruskal.cpp生成的最小生成树边数少于vexnum-1并查集unionSet()函数中,parent[root1] = root2写反为parent[root2] = root1,导致集合合并失败在unionSet()内加printf("Union %d->%d\n", root1, root2),观察合并顺序是否符合预期
拓扑排序.cpp输出"有环"但手动验图无环indegree[]数组未在每次TopoSort()调用前重置为0,残留上次计算值将indegree[]声明为局部数组(int indegree[MAX_VERTEX_NUM] = {0}),或在函数开头显式memset(indegree, 0, sizeof(indegree))
关键路径.cpp中ve[](最早发生时间)全为0TopoSort()返回的拓扑序列为空(即图有环),但代码未检查返回值直接进入ve[]计算循环在CriticalPath()中,if(!TopoSort(G, topOrder)) { printf("Graph has cycle!\n"); return; }

注意:所有图算法文件中,INF的定义必须统一。包里数据结构.md指定为#define INF 32767,但Floyd.cpp用了INT_MAX/2。实际使用时,请统一在common.h(需你新建)中定义#define INF 0x3f3f3f3f,并在所有.cpp文件顶部#include "common.h"——这是避免跨文件数值不一致的后悔药。


4. 从哈夫曼树.cpp到哈夫曼树编码.cpp:如何把一棵树变成可执行的压缩逻辑

4.1 哈夫曼树构建的“贪心本质”与SelectMin()函数的不可替代性

哈夫曼树.cpp的核心是HuffmanTree结构体和HuffmanCoding()主函数。它不直接操作字符,而是处理一组权值数组(如{5,29,7,8,14,23,3,11})。构建过程严格遵循贪心策略:

  1. 创建n个叶子节点,权值为输入数组;
  2. 循环n-1次,每次选出两个权值最小且未被选中的节点,合并为新节点,新节点权值=两子节点权值和;
  3. 将新节点加入候选集,重复步骤2。

关键函数SelectMin()负责第2步的筛选。它的实现不是简单min_element(),而是双重遍历:第一次找最小,第二次找次小(排除第一次找到的索引)。包里代码是:

void SelectMin(HTNode ht[], int end, int *s1, int *s2) { int i, min1, min2; min1 = min2 = 32767; // INF *s1 = *s2 = 0; for(i = 1; i <= end; i++) { if(ht[i].weight < min1 && ht[i].parent == 0) { min2 = min1; *s2 = *s1; min1 = ht[i].weight; *s1 = i; } else if(ht[i].weight < min2 && ht[i].parent == 0) { min2 = ht[i].weight; *s2 = i; } } }

这个函数的精妙在于:ht[i].parent == 0确保只选未合并的节点;min2 = min1; *s2 = *s1在更新最小值时同步更新次小值,避免二次遍历。如果你用std::priority_queue重写,必须保证每次pop()后,新top()确实是剩余最小值——而原生priority_queue不支持随机访问,无法高效剔除已用节点,反而增加复杂度。

4.2 编码生成的“路径回溯”:为什么哈夫曼树编码.cpp必须从叶子向上走到根

哈夫曼树编码.cpp的任务是:给定字符集{'a','b','c','d'}和对应权值{5,29,7,8},输出每个字符的二进制编码。它不重新建树,而是复用哈夫曼树.cpp生成的HT数组(HTNode ht[MAX_TREE_SIZE])。编码逻辑是典型的“自底向上”:

// 对第i个字符(对应ht[i]叶子节点),从该节点向上走到根 int start = n; // 编码数组code从末尾开始存 int c = i, p = ht[i].parent; while(p != 0) { if(ht[p].lchild == c) code[--start] = '0'; // 左孩子标0 else code[--start] = '1'; // 右孩子标1 c = p; p = ht[p].parent; } // code[start..n-1]即为字符i的编码

这里start初始为n(编码数组长度),每次--start将编码字符存入前面位置,最后printf("%s", &code[start])输出。这个设计避免了字符串拼接的内存开销,是C风格编码的经典手法。如果你尝试改成std::string code = ""; code = '0' + code;,在权值较多时会触发多次内存重分配,性能暴跌。

4.3 实战:用哈夫曼树编码.cpp压缩一段文本的完整流程

假设你要压缩字符串"aabbccdd"(4个a、4个b、4个c、4个d),权值相同,均为4。步骤如下:

  1. 准备输入文件:新建input.txt,内容为:

    4 a b c d 4 4 4 4

    第一行字符数,第二行字符,第三行权值。

  2. 编译并运行:

    g++ -std=c++11 -o huffcode 哈夫曼树编码.cpp ./huffcode < input.txt
  3. 观察输出:

    a: 00 b: 01 c: 10 d: 11 Original bits: 32 (8 chars * 4 bits) Compressed bits: 32 (8 chars * 4 bits, 因权值相等,无压缩增益)
  4. 验证压缩效果:改为权值{10,2,3,5},输出变为:

    a: 0 b: 110 c: 111 d: 10 Original bits: 32 Compressed bits: 10*1 + 2*3 + 3*3 + 5*2 = 10+6+9+10 = 35? 等等,这比原文还大!

    玄学时刻来了:哈夫曼编码只对频率差异大的字符集有效。此处a频次最高(10),但b,c,d频次接近,导致平均码长接近2.5,而原文用2位编码(4字符需2位)已是最优。真正的压缩收益体现在"aaaaabbbbbccccdddddeeeee"这类偏态分布上——这正是作者在数据结构.md里强调“权值需反映真实频次”的原因。

4.4 进阶:把哈夫曼编码集成到文件压缩工具中(伪代码框架)

虽然包里没提供完整压缩器,但你可以基于这两个文件快速搭建:

// step1: 统计文件字符频次(用map<char, int>) ifstream fin("test.txt"); map<char, int> freq; char c; while(fin.get(c)) freq[c]++; // step2: 构建权值数组和字符数组 vector<int> weights; vector<char> chars; for(auto& p : freq) { weights.push_back(p.second); chars.push_back(p.first); } // step3: 调用哈夫曼树构建(需改造哈夫曼树.cpp为函数) HTNode* ht; int n = weights.size(); CreateHuffmanTree(weights.data(), n, &ht); // step4: 生成编码表(复用哈夫曼树编码.cpp逻辑) map<char, string> codeTable; GenerateCodeTable(ht, n, chars, codeTable); // step5: 编码文件(位操作,非字符串拼接!) ofstream fout("test.huf", ios::binary); BitWriter bw(fout); // 自定义位写入器 for(char c : fileContent) bw.write(codeTable[c]); bw.flush();

提示:BitWriter类需自己实现,核心是unsigned char buffer和int bitCount,每写8位buffer才fout.write()一次。这是哈夫曼压缩从理论到落地的最后一公里——包里代码教你建树和编码,而工程化必须补上字节级I/O。


5.表达式求值.cpp与括号的匹配.cpp:栈的两种灵魂用法与运算符优先级黑匣子

5.1括号的匹配.cpp:最简栈应用,却是理解“状态机”的起点

这个文件只有30行,却浓缩了栈的本质:用后进先出的存储特性,模拟嵌套结构的“撤销”逻辑。其核心算法是:

stack<char> s; string exp; cin >> exp; for(char c : exp) { if(c == '(' || c == '[' || c == '{') s.push(c); else if(c == ')' || c == ']' || c == '}') { if(s.empty()) { cout << "NO"; return; } char top = s.top(); s.pop(); if((c == ')' && top != '(') || (c == ']' && top != '[') || (c == '}' && top != '{')) { cout << "NO"; return; } } } cout << (s.empty() ? "YES" : "NO");

这里s栈不存数值,只存“期待被关闭的左符号”。每一次push()是开启一个新作用域,每一次pop()是退出当前作用域。这个模型可直接迁移到XML解析、JSON校验、甚至IDE的括号高亮——它们底层都是同一个栈状态机。如果你发现{[()]}判为NO,一定是if条件中c == '}' && top != '{'的单引号写成中文全角,这是新手最常见的翻车点。

5.2表达式求值.cpp:双栈协同的“运算符优先级”实现,不是简单后缀转换

这个文件实现中缀表达式求值(如3+5*2-8/4),但它没有先转后缀再计算,而是用双栈实时处理:

  • OPTR栈存运算符(char)
  • OPND栈存操作数(int)

关键逻辑在GetTop()和Precede()函数:

char Precede(char op1, char op2) { // 返回op1与op2的优先级关系:'<', '=', '>' if((op1 == '+' || op1 == '-') && (op2 == '*' || op2 == '/' || op2 == '(')) return '<'; if((op1 == '*' || op1 == '/') && (op2 == '+' || op2 == '-' || op2 == ')')) return '>'; if(op1 == '(' && op2 == ')') return '='; if(op1 == '(' && op2 != ')') return '<'; if(op1 != '(' && op2 == ')') return '>'; return '>'; // 默认高优先级 }

当读到新运算符op时:

  • 若op优先级 >OPTR.top(),op入栈;
  • 若op优先级 =OPTR.top()(如遇到)),弹出OPTR栈顶(;
  • 若op优先级 <OPTR.top(),则弹出OPTR栈顶运算符和OPND栈顶两操作数,执行运算,结果压入OPND。

这个机制的精妙在于,它把“运算符优先级”这个抽象概念,转化为栈顶元素与新元素的字符比较。Precede()函数就是这张优先级表的代码化身。如果你把'+'和'-'的优先级设错,1-2+3就会算成1-(2+3)=-4而非2。

5.3 数字解析的边界坑:表达式求值.cpp如何处理多位数与负数

包里代码假设输入为单个数字字符(如"1+2*3"),但真实场景需处理"123+456*78"。原代码的数字解析是:

if(c >= '0' && c <= '9') { int num = 0; while(c >= '0' && c <= '9') { num = num * 10 + (c - '0'); // 但这里c没更新!会无限循环 } OPND.push(num); }

这是一个典型缺陷。修复方案是用stringstream或手动推进指针:

if(isdigit(c)) { int num = 0; while(i < exp.length() && isdigit(exp[i])) { num = num * 10 + (exp[i] - '0'); i++; // 关键!推进索引 } OPND.push(num); i--; // 因为for循环会i++,此处需回退 }

至于负数(如"-5+3"),原包未支持。你需要扩展:当c == '-'且栈空或前一字符是'('或运算符时,将其视为一元负号,压入OPND栈0,再压入'-',后续计算0-5。

5.4 避坑:表达式求值三大隐形雷区

现象原因解决方案
"1+2*3"算出9(先算1+2)Precede('+', '*')返回'>'而非'<',导致+提前弹出计算检查Precede()函数,确保'+'对'*'返回'<'
"(1+2)*3"算出3(忽略括号)Precede('(', '*')返回'>',导致(被错误弹出Precede()中op1=='('时,除op2==')'外一律返回'<'
输入"12+34"时程序崩溃数字解析未推进索引,while循环无限执行,num溢出如前述,添加索引i推进和边界检查i < exp.length()

提示:表达式求值.cpp的OPND栈用int,限制了计算范围。若需大数,应替换为long long或std::string(配合大数加减乘除函数)。这不是bug,而是作者刻意为之的教学取舍——让你先掌握逻辑,再扩展能力。


6. 把28个文件变成你的“数据结构肌肉记忆”:一个真实项目的四步重构法与我的血泪习惯

6.1 第一步:删掉所有main(),封装成可复用的头文件与库

包里每个.cpp都带main(),这是教学友好,但工程上灾难。我的做法是:

  • 新建include/目录,为每个结构创建.h文件,如SeqList.h:
    #ifndef SEQLIST_H #define SEQLIST_H #include <iostream> #define MAXSIZE 100 typedef int ElemType; typedef struct { ElemType data[MAXSIZE]; int length; } SeqList; bool InitList(SeqList &L); bool ListInsert(SeqList &L, int i, ElemType e); bool ListDelete(SeqList &L, int i, ElemType &e); #endif
  • 将顺序表.cpp中函数实现剪切到src/SeqList.cpp,只保留声明在.h中;
  • 用CMakeLists.txt构建静态库:
    add_library(dslib STATIC src/SeqList.cpp src/LinkList.cpp src/Stack.cpp) target_include_directories(dslib PUBLIC include/)

从此,你的新项目只需#include "SeqList.h"和target_link_libraries(your_app dslib),不再复制粘贴28个main()。

6.2 第二步:用Google Test为关键算法写单元测试,让“正确”可验证

Dijkstra.cpp是否真能找出最短路?光看输出不够。我为它写了测试用例:

#include "gtest/gtest.h" #include "Graph.h" // 自定义图结构头文件 TEST(DijkstraTest, SimplePath) { MGraph G; CreateGraphFromMatrix(&G, {{0,1,4},{1,0,2},{4,2,0}}); // 3顶点完全图 int dist[3], path[3]; Dijkstra(G, 0, dist, path); EXPECT_EQ(dist[1], 1); // 0->1距离为1 EXPECT_EQ(dist[2], 3); // 0->1->2距离为1+2=3 }

运行ctest,失败时精准定位到Dijkstra()中dist[j] = G.arcs[v][j]未初始化为INF。测试不是负担,而是把“我以为对”变成“机器验证对”的唯一手段。包里没测试,但你加的每一行EXPECT_EQ,都在加固自己的理解。

6.3 第三步:用Doxygen生成API文档,把28个文件变成可检索的知识图谱

在SeqList.h上加注释:

/** * @brief 顺序表结构体 * @details 支持随机访问,插入删除O(n),查找O(1) * @note length从0开始计数,data[0]为第一个元素 */ typedef struct { ... } SeqList;

运行doxygen Doxyfile,生成HTML文档。点击SeqList,能看到所有相关函数、调用关系图、甚至ListInsert()的调用栈。当线索二叉树.cpp和二叉树.cpp的BiTree定义冲突时,文档能立刻告诉你哪个文件定义了哪个版本。

6.4 第四步:建立“错误模式库”,把踩过的坑变成可复用的检查清单

我维护一个BUG_LOG.md,记录每次翻车:

- [2024-03-15] `Kruskal.cpp`: unionSet()中root1/root2赋值反了 → 导致生成树不连 <p> <a href="https://download.csdn.net/download/qq_53226437/85117420" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询