ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据结构C-C++代码实现:从压缩包到可讲清的考点清单

数据结构C-C++代码实现:从压缩包到可讲清的考点清单 简介这是一份数据结构课程核心内容的C/C实现代码合集面向正在学习数据结构、需要参考经典结构定义与算法编码的本科生及自学者。压缩包共35个文件以34个C/C源文件为主另附1份Markdown说明文档整体仅28KB轻量便捷便于快速下载、查阅和本地编译。代码覆盖线性表、栈与队列、串、广义表、二叉树与线索二叉树、哈夫曼树、矩阵等常用数据结构图的创建则包含邻接矩阵、邻接表、十字链表、邻接多重表等多种存储方式算法方面提供DFS、BFS、Dijkstra、Floyd、Prim、Kruskal、拓扑排序、关键路径等经典实现基本涵盖本科课程实验与常见考试题型。每个源文件按单一知识模块独立组织结构清晰方便按需取用。目前已有369人学习/下载适合用于对照教材理解原理、调试细节、考前突击也可作为课程设计或毕业设计的参考基础。1. 数据结构C-C代码实现这类压缩包拿到手先别急着解压“数据结构C-C代码实现”是很多人在初学阶段绕不开的资源标题但我敢说九成的人拿到它的流程是解压、看一眼目录、关掉、收藏夹吃灰。我见过太多同学电脑里存着十来个这种打包好的源码链表、二叉树、图样样都有真到课程设计答辩或者笔试手写代码的时候一段都写不利索。这压缩包真正的价值其实不在代码本身而在于它天然是一张数据结构考点清单顺序表、单链表、栈、队列、树、图、排序、查找每样都在里面。适合想对照改写、想复习考点、想搞清 C 和 C 写法差异的人前提是把它当草稿纸别当标准答案。2. 解压之后先分清 C 与 C两类实现怎么快速识别和选择2.1 典型压缩包的文件组织按章节分目录但不一定配有文档这类压缩包的目录结构我见到的十有八九是按教材章节来分的打开后大概是这个样子数据结构C-C代码实现/ ├── 第1章 线性表/ │ ├── 顺序表/ │ │ ├── SeqList.c │ │ └── SeqList.h │ ├── 单链表/ │ │ ├── LinkList.c │ │ └── main.c │ └── 双向链表/ │ ├── DuLinkList.c │ └── DuLinkList.h ├── 第2章 栈和队列/ ├── 第3章 树/ ├── 第4章 图/ ├── 第5章 排序/ └── 说明.txt这种按章节组织的还算良心最怕的是按日期或心情命名20230501链表.cpp、test1.cpp、最终版.cpp。后者基本都是历届学生作业攒起来的没有体系、没有文档、也没有统一的接口风格。我一般拿到手的第一件事不是编译而是先盘一遍文件类型判断哪些值得留、哪些可以直接删。文件类型常见命名判断要点纯源文件link.c / link.cpp能不能独立编译取决于里面有没有 main 函数头文件link.h / tree.hC 的模板类实现经常整个塞在 .h 里要连头文件一起拷工程配置.dev / .vcxproj / Makefile年份越老越容易失效建议直接忽略说明文档readme.txt / 实验报告哪怕写得烂也能从中看出这份代码面向的考点这个盘点的过程别跳过它可以帮你判断这份压缩包覆盖了多少个数据结构考点。多数包只覆盖到二叉树或图的前几节排序和哈希反而是最容易缺失的部分。提前知道缺什么等下动手补的时候心里才有数。2.2 C 风格和 C 风格的快速识别看三处就能定压缩包里的“C-C代码实现”往往是两代代码混在一起的低年级阶段写的纯 C 结构体版本和高年级用 class 重写的 C 版本。你不需要逐行读看三处就能定语言风格结构体定义、内存分配方式、输入输出写法。/* C 风格的链表节点 */ typedef struct Node { int data; struct Node *next; } Node; Node *createNode(int val) { Node *p (Node *)malloc(sizeof(Node)); if (p NULL) { return NULL; /* 内存申请失败要处理 */ } p-data val; p-next NULL; return p; }这段代码的特征非常典型用typedef struct把结构体别名成Node用malloc/free管内存用printf/scanf做输入输出。好处是内存管理摊在明面上学指针阶段最好用这种缺点是没有封装代码一长就散得到处都是。// C 风格的链表节点 struct Node { int data; Node *next; Node(int val) : data(val), next(nullptr) {} }; // 使用时 Node *p new Node(5); // ... 用完后 delete p;C 风格的特征是结构体里有构造函数、用new/delete申请内存、头文件写iostream或vector有些还会用 STL 容器把数据结构本身的逻辑藏起来。这里要特别注意如果包里的代码大量使用vector、list、map那它对你复习数据结构本身的帮助很小因为它体现的是“怎么调用现成容器”而不是“怎么实现一个容器”。选哪份看你的阶段。还在学指针和内存管理的优先用 C 版malloc/free逼你把内存的事想清楚做课程设计要交报告、讲代码的优先用 C 版封装性好讲准备笔试机考的两种都行因为绝大多数在线评测系统都同时接受 C 和 C但考试时我建议写 C 风格因为你不需要跟类模板的编译错误纠缠。同一个算法如果包里有 C 和 C 两份以 C 版为准对照着看C 版经常包了一层 STL反而看不到数据结构的核心逻辑。2.3 拿到手先做三件事找 main、查依赖、定编译方式第一件事找main函数。一个文件夹里可能同时有多个带main的文件那是不同作业题的入口把它们单独拎出来剩下的才是被#include依赖的模块文件。第二件事查依赖。点开每个源文件最上面的#include看它引用的是同目录下的.h还是绝对路径。引用绝对路径的代码基本只有原作者电脑上能编译你要么把路径改成相对路径要么放弃这份。第三件事定编译方式。确认文件后缀是.c还是.cpp这决定了你待会用gcc还是g来编。我一般会随手在纸上拉一个这样的清单不花多少时间但能直观看出这份压缩包的可用率文件名语言是否含 main依赖文件能不能单独编译LinkList.cC否LinkList.h能生成库文件main.cC是LinkList.c能BST.cppC否无能final_test.cppC是BST.cpp能否待验证这一步做完你才真正知道包里哪些是活代码、哪些是死代码。很多人在这一步就直接筛掉了三分之一的内容后面省下来大量时间。3. 挑代码要有标准链表、树、图、排序的高质量实现长什么样3.1 单链表能跑不等于正确删除和释放是照妖镜很多压缩包里的链表代码插入能执行遍历也能打印看起来没啥问题。但仔细一看就露馅头指针传进函数后永远改不了要在表头插入就翻车删除节点后不释放内存跑一个循环下来内存涨几十兆。判断一份链表实现能不能留我会先看插入函数有没有用二级指针或者有没有返回值再看释放函数是不是只删了一个节点。// 单链表插入pos 从 0 开始用二级指针更新头指针 int insert(Node **head, int pos, int data) { if (head NULL || pos 0) return -1; Node *newNode (Node *)malloc(sizeof(Node)); if (newNode NULL) return -1; // 内存申请失败 newNode-data data; newNode-next NULL; if (pos 0) { // 插入表头必须改头指针本身 newNode-next *head; *head newNode; return 0; } Node *p *head; int i 0; while (p ! NULL i pos - 1) { // 走到目标位置的前一个节点 p p-next; i; } if (p NULL) { // 位置越界 free(newNode); // 越界要释放否则泄漏 return -1; } newNode-next p-next; p-next newNode; return 0; }pos 0分支里的*head是关键不传二级指针的话头指针的更新在函数结束后就丢了这是新手最常见的链表翻车点。另外注意越界分支里的free(newNode)很多代码在这里直接return -1newNode 就泄漏了。判断一份链表代码的质量就看这两处有没有写对。3.2 二叉搜索树递归遍历能看懂非递归才是常见考点树那一章是压缩包里质量最参差不齐的部分。递归实现的前中后序遍历大多能跑但很多人背下来了却不理解递归栈是怎么走的面试或考试要求写非递归就直接动手抄。我挑树的实现时会直接翻到中序遍历那一页看它是递归还是用栈模拟。递归版本留着理解思路非递归版本才值得当成考点好好练。// 非递归中序遍历用栈模拟系统递归调用栈 void inOrderIterative(Node *root) { if (root nullptr) return; stackNode* s; Node *p root; while (p ! nullptr || !s.empty()) { while (p ! nullptr) { // 一直压入左子树 s.push(p); p p-left; } p s.top(); // 左子树走到底弹出来访问 s.pop(); cout p-data ; p p-right; // 转右子树继续 } }外层的while (p || !s.empty())是这种写法的骨架少了p ! nullptr这个条件会导致退到根节点后循环提前结束。删除节点是另一个翻车重灾区叶子节点直接删只有一个孩子就把孩子接上有两个孩子要用右子树的最小节点替代被删节点然后递归删除那个最小节点。包里有三成代码会把第三种情况写错要么没更新父节点指针要么替代节点的右子树整个丢了。3.3 图的邻接表头插法还是尾插法初始化时就要决定图的存储是压缩包里最常见“半成品”的地方。邻接矩阵一般还好邻接表十份里有五份的firstEdge没有在构造函数里置空程序一跑就段错误。我挑图相关的代码先看构造初始化再看插入边的函数用的是头插还是尾插。struct EdgeNode { // 边表节点 int adjVex; // 邻接点的下标 int weight; // 权值 EdgeNode *next; }; struct VertexNode { // 顶点表节点 char data; // 顶点信息 EdgeNode *firstEdge; // 第一条边 }; class Graph { VertexNode *vertices; int vertexNum; public: Graph(int n) { vertexNum n; vertices new VertexNode[n]; for (int i 0; i n; i) { vertices[i].firstEdge nullptr; // 初始化置空不能漏 } } };构造函数里那个nullptr初始化是最容易丢的。很多包里的代码只做了new VertexNode[n]没有遍历置空后面插入边时拿空指针当链表操作直接崩溃。插入边的时候头插法快但遍历得到的邻接顺序是反的尾插法保序但每次要遍历到链表末尾。考试建议用头插代码短、不容易错题目没特殊要求不需要考虑顺序问题。还有一点无向图要插入两条边(u,v)和(v,u)有向图只插一条这个方向搞错的代码在压缩包里很常见。3.4 排序与查找警惕那些让你“背下来”的魔改快排排序章节的坑比较隐蔽因为代码短、看起来都好懂但快排的边界条件只要错一处排序结果就大部分正确、极个别数字错位。压缩包里流传最广的错误版本是递归出口写成if (left right)当区间变成空区间时直接越界或者枢轴交换后i和j的推进方向搞反。我这里给一份我验证过多次的参考实现void quickSort(int arr[], int left, int right) { if (left right) return; // 递归出口空区间或单元素 int pivot arr[left]; // 枢轴取第一个元素 int i left, j right; while (i j) { while (i j arr[j] pivot) j--; // 从右找第一个小于枢轴的 arr[i] arr[j]; while (i j arr[i] pivot) i; // 从左找第一个大于枢轴的 arr[j] arr[i]; } arr[i] pivot; // 枢轴归位 quickSort(arr, left, i - 1); // 递归处理左右两段 quickSort(arr, i 1, right); }注意两个细节left right用的是大于等于把空区间和单元素一起处理了右侧扫描的条件是arr[j] pivot等于枢轴的元素直接跳过不然会出现死循环。这个版本属于“填坑法”每次把枢轴存到临时变量扫描过程中覆盖式移动元素比交换法少一些分支判断。你拿到的包如果用的是交换法也可以但一定要当场用[5,3,8,1,2]这种乱序数组跑一遍再手动核对结果别直接背。4. 跑通代码包的最小编译流程Windows 与 Linux 下的命令和验证4.1 Windows 下用 GCC 命令行编译单个文件拿到一个.c文件在 Windows 上最可靠的编译方式就是装一套带 GCC 的工具链然后在命令行里直接编。你不需要打开专门的集成开发环境因为这类单文件的代码包用 IDE 建工程反而麻烦命令行一条命令就编译报错信息也更直接。gcc -g -Wall -o link_demo link_demo.c link_demo.exe-g生成调试信息后面用调试器查指针问题时必须有它-Wall打开所有常见告警代码里如果有隐含的问题编译器会直接指出-o link_demo指定输出文件名不指定的话默认生成a.exe会在后续调试时把自己搞混。如果源文件是.cpp把gcc换成g即可其余参数不变。编译通过后运行link_demo.exe看到输出正常这份代码才算真正跑通。这里有个环境上的常见坑如果命令提示符直接报“不是内部或外部命令”说明工具链装了但没把路径加到环境变量要么重新安装时勾选添加环境变量要么用工具链自带的一个命令行入口。先确认gcc --version能输出版本号再继续。4.2 Linux 下编译多文件先分步编译再链接省一半排错时间在 Linux 上处理一个多文件的代码包我不建议一条命令把所有.c都塞进去编译虽然那样能跑但链接报错时你会分不清是哪个文件出的问题。正确顺序是每个文件单独编译成目标文件再统一链接。gcc -g -Wall -c list.c -o list.o gcc -g -Wall -c queue.c -o queue.o gcc -g -Wall -o demo main.c list.o queue.o ./demo-c表示只编译不链接生成.o目标文件。前两条命令分别编译list.c和queue.c如果某个.c文件里有语法错误报错会精确到文件和行号不用从一堆输出里猜。第三条命令编译main.c并把前面两个.o链接成可执行文件demo。以后再改代码只需要重新编译改动过的文件再重新链接不用每次都全量编译。如果代码包里有头文件依赖记得在命令里加上-I指定头文件目录gcc -g -Wall -I./include -c src/list.c -o build/list.o-I./include告诉编译器去include目录找头文件。如果压缩包的目录结构是源码和头文件分开放这条参数是少不了的。4.3 用调试器验证指针操作断点、watch、打印三板斧编译跑通只是第一步链表和树的代码很多是“能跑但结果不对”这时候靠printf插桩太慢我一般直接上调试器。命令行调试器是排查这类代码最有效的工具核心就三个命令断点、监视、单步。gdb -q ./link_demo break insert # 在 insert 函数处下断点 run # 运行程序停在第一个断点 print *head # 打印头指针指向的内容 next # 单步执行一行 watch head-next # 监视字段变化break insert下断点后run会一直执行到函数入口print *head打印解引用后的结构体内容比肉眼盯着代码找 bug 快得多next单步执行配合print p-next可以看到链表指针一步步怎么走的。watch head-next监视某个内存地址只要它被修改就停止查链表被意外改断时特别好用。遇到段错误也不要慌用调试器运行它会在崩溃那一行停下来bt命令打印调用栈一眼能看到是哪个函数、哪一行越界比对着代码猜强太多。这一步是很多熟练工处理指针问题的常态说它是“玄学”其实是没把调试器用起来。5. 避坑手册这类代码包最常见的五个坑与排查记录5.1 链接报“无法解析的外部符号”或 undefined reference现象编译单个.c文件通过但最后生成可执行文件时报undefined reference to xxx或者 Windows 上报“无法解析的外部符号”。这是压缩包下载党最常见的翻车现场。原因多数情况是main调用的函数在别的.c文件里而编译命令只编了main.c另一种可能是头文件里声明了函数实现文件里函数签名和声明不一致比如声明insert(Node*, int, int)实现却写成insert(Node*, int, Node*)类型对不上。还有一种隐蔽情况用g编译.c文件时C 函数没有用extern C包裹名字修饰规则不同导致找不到符号。解决把涉及的所有.c/.cpp文件一起编进链接命令或者像我前面那样分步编译后统一链接。然后逐对核对头文件声明和源文件实现的函数签名参数类型和返回类型必须完全一致。如果是 C 文件被 g 编让 C 语言部分包装在extern C {}里或直接统一用gcc编译最后再用 g 链接。5.2 控制台中文乱码现象代码编译和运行都正常但凡是中文输出的地方全是乱码英文和数字正常。Windows 上最多见Linux 下偶尔也有。原因Windows 的命令行环境默认用 GBK 编码解析输出而源码文件是 UTF-8 保存的printf(中文字符串)里的字节流被命令行按 GBK 解码自然乱码。反过来如果你在 Linux 上用 GBK 编码的源文件终端是 UTF-8 也会乱。本质是源文件编码、运行环境、终端解析三者没对齐。解决Windows 下最省事的办法是把源文件另存为 ANSI 编码也就是 GBK。编辑器右下角有编码状态改成 ANSI 再重新编译乱码基本消失。Linux 下则反过来确保源文件是 UTF-8 无 BOM。还有一招兼容做法程序开头调用setlocale(LC_ALL, )让它跟随系统区域设置中文环境里通常能解决。注意这只是治标换个环境可能又乱长期做法是统一约定编码。5.3 链表程序能跑一调用释放函数就崩溃现象插入、遍历都正常但只要调freeList(head)之类释放函数程序就崩有时还伴随“段错误”或“内存访问违规”提示。这是链表代码包里出现频率最高的疑难杂症。原因典型的有三种。一是释放了栈上分配的变量比如有人把Node定义成局部变量再free(localNode)对非malloc得来的内存调用free是未定义行为二是重复释放两个指针指向同一块内存第一次free后没把指针置空第二次又free了一次三是释放后继续访问free之后代码里没有把head置NULL后面又用head-next去拿数据读的是已经归还给系统的内存。解决释放函数里每释放一个节点就立刻置空指针遍历时用一个临时指针保存下一个节点地址再释放当前节点顺序不能反调用方传入头指针时要传二级指针Node **head这样释放完成后能把调用方的指针置空。关键自检方法释放前用调试器print看一眼链表长度和节点地址释放后再看一眼调用方指针是否已经变成 0逻辑就清楚了。5.4 排序结果大部分正确个别的数错位现象跑完排序算法后数组前面几十个元素是有序的后面几个数错位或者某个数字跑到了它该在位置的前面一位。这类问题在快排和归并代码里最常出现。原因边界下标算错。快排的递归区间没写对比如左半区间应该收在pivotIndex - 1写成pivotIndex导致包含枢轴本身被重复处理或者归并时合并循环里的i和j递增时机不对有一个元素漏进结果。这类错误的共同点是“只在特定排列顺序下才能触发”测试数据碰巧有序就不会暴露。解决用最小用例去验证不要一上来就排一万个随机数。我用三个固定用例空数组、单元素数组、两个元素的逆序数组[2,1]。这三个用例能触发绝大多数边界问题。然后打印每次递归时的left、right、pivotIndex对照手动算的结果看区间收缩和根因。还有一招写一个辅助函数在排序前后检查数组的升序属性发现错误立刻定位到那段区间。5.5 源码用了 C11 特性旧编译器编译不过现象代码逻辑没问题但编译器报nullptr was not declared、auto was not declared这类错误或者函数体里用了for(int x : vec)这种范围 for 语法直接被判错。原因很直白压缩包里的代码是较新编译器写的你本机工具链默认语言标准没开。原因GCC 老版本默认用的是 C98 标准而nullptr、auto、范围 for 都是 C11 才有的。如果代码用了nullptr而编译器按 C98 解析就会把nullptr当成普通标识符报各种奇怪的错。解决编译命令里显式指定语言标准g -stdc11 -g -Wall -o demo demo.cpp-stdc11把语言标准切到 C11如果代码用了更新的特性比如结构化绑定或if constexpr可以尝试-stdc17。如果指定标准之后还报错那说明代码用了编译器完全不支持的特性两个选择换新版本的工具链或者把出现新特性的那一行改成老写法比如nullptr改成NULL。改老写法时要小心NULL在 C98 里是整数 0如果代码里同时存在指针和整数重载的上下文行为会不一样改了之后一定要重新跑测试。6. 把代码包改造成能讲清的版本我的四步重构习惯拿到一份能跑的源码和真正掌握它中间隔着一道坎“能不能不看代码把它讲明白”。我现在的习惯是把压缩包里的代码按四步重构成自己的版本过程既是对考点的复述也是在给自己攒一笔笔面试和考试用得上的资产。第一步原样备份。把压缩包解压出来的原始文件整个复制一份放在origin目录里要改动的文件放working目录。后面每改一处都能用文件对比工具跟原始版对照改坏了也有后悔药这是我从一次把链表改动搞砸后养成的血泪习惯。第二步按模块重新命名。所有fun1、f2、test3这类名字全部重命名为语义化命名find_max叫它findMax删除函数固定叫removeByPos。命名统一后代码的调用关系一眼能看清也方便查依赖。第三步补测试入口。每个模块的main函数里只留一组最简单的最小用例覆盖三种边界空结构、单元素结构、满结构。我常用的测试用例长这样测试对象用例期望结果空链表删除delete(0)返回失败不崩溃单元素链表删除delete(0)头指针变为空快排边界空数组/单元素直接返回不越界BST 删除两孩子节点删除根节点中序遍历仍有序第四步给关键函数写人话注释。非递归中序遍历里那个外层的while (p || !s.empty())我会在旁边注上“左子树到底弹出访问再进右子树”。注释不是写给编译器看的是写给两周后的自己看的。能对着注释把每一步的逻辑讲清楚这份代码才算真正是你的。这一步坚持下来比收藏任何一份包都重要。那次面试让我彻底改了习惯面试官让我手写二叉搜索树的删除我照着背的代码写到“有两个孩子的情况”就卡住了因为那部分我从没真正理解过只记得“要用后继节点替换”。从那以后我拿到任何一份代码包都先走这四步宁可慢一点也不让代码只在硬盘里躺着。希望你也能把这份压缩包变成一张自己能讲清楚的考点清单希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进