严蔚敏《数据结构》算法设计题源码改写与考研408实战指南 简介严蔚敏《数据结构C语言版》第二版算法设计题答案与书中算法源码包适配CLion 2020~2021开发环境面向正在学习数据结构、需要对照参考答案与源码进行验证的本科生、考研读者以及需要应对期末考试的在校生。压缩包大小仅三点一四兆字节内含C语言源码、CMake构建配置及使用说明文档已有超过一千二百人次浏览学习。作者结合书中章节顺序对部分算法进行了优化并逐一纠正参考答案中的错误同时将可能触发的bug及其触发条件、算法不同实现方法、优化思路以及执行过程以批注形式详细说明对于常见运行异常也给出了主动规避与排错建议。读者可直接部署CMake工程运行验证也可在阅读答案时同步查看对应源码与提示便于跟踪函数调用过程适合作为课后自学、作业自查与考研复习的辅助材料。附赠的五本经典算法或数据结构书籍链接已打包在说明文档中方便拓展阅读。1. 严蔚敏《数据结构》第二版的算法设计题与源码这本教材的答案为什么值得自己重做一遍很多人在期末周或考研复习时抱着严蔚敏这本《数据结构C语言版》第二版翻到每章结尾的算法设计题就开始发怵书上的伪代码怎么改都编译不过“源码版”到底长什么样也不清楚。这本书的算法描述用的是类C的ADT语言和真正能跑的C程序隔着三层墙——类型要自己typedef、引用要改成指针、函数指针和内存管理都要重新补全。这篇文章要落地的就是这三件事把书中算法源码落成可运行文件把算法设计题的答案按题型拆成通用套路再用GDB和断言验证它确实对。适合人群很明确在准备数据结构期末复习、数据结构考研或408的人以及那些靠实验报告拿学分但不想抄错代码的学生。2. 把书中伪代码改造成可运行源码顺序表、链表和二叉树的最小样例2.1 为什么书上的代码不能直接编译伪代码与C语言的三个错位严蔚敏教材的算法描述是ADT风格的类C语言它优先表达逻辑不保证能喂给编译器。最常见的三处错位是Status和ElemType这种自定义类型、传参用的引用还有malloc的返回值与强制类型转换。这三处不处理gcc第一行就会报错。书中的伪代码惯用写法纯C可运行落地写法要这么改的原因Statustypedef int Status配合OK/ERROR宏书中Status是抽象返回类型C编译器不认识ElemTypetypedef int ElemType让线性表、树、图的元素类型可统一替换函数参数写ElemType e写成ElemType *e调用处传e是C引用语法纯C不支持malloc不写类型转换(ElemType *)malloc(sizeof(ElemType)*cap)旧标准下void*隐式转换会有警告显式转换更稳我一般会在每个.c文件的顶部放一个“可编译最小骨架”头文件、类型定义、OK/ERROR宏、打印函数。这样每个算法题的源码文件都能单独编译不用依赖工程配置。这个习惯后来帮我省了很多事因为实验报告和复试上机都要求你交一个能直接跑的.c文件。2.2 顺序表InitList 与 ListInsert 的落地改写顺序表的核心是数组加长度加容量三个字段。书中ListInsert的插入逻辑依赖“从尾到头后移”的写法这对新手是个大坑如果从头开始后移后面的元素会被覆盖掉。正确的移动方向只有一个就是从最后一个元素开始逐个往后挪。#include stdio.h #include stdlib.h #define MAXSIZE 100 #define OK 1 #define ERROR 0 typedef int Status; typedef int ElemType; typedef struct { ElemType *data; // 堆上分配的数组 int length; // 当前元素个数 int capacity; // 分配容量 } SqList; Status InitList(SqList *L, int cap) { L-data (ElemType *)malloc(sizeof(ElemType) * cap); if (!L-data) return ERROR; L-length 0; L-capacity cap; return OK; } Status ListInsert(SqList *L, int pos, ElemType e) { int i; if (pos 1 || pos L-length 1) return ERROR; // 合法位置是 1..length1 if (L-length L-capacity) return ERROR; // 容量不足简化处理不扩容 for (i L-length; i pos; i--) { L-data[i] L-data[i - 1]; // 从最后一个元素开始后移 } L-data[pos - 1] e; // 序号转下标要减1 L-length; return OK; } int main(void) { SqList L; InitList(L, MAXSIZE); ListInsert(L, 1, 10); ListInsert(L, 2, 20); printf(length%d, first%d\n, L.length, L.data[0]); free(L.data); // 堆内存记得释放 return 0; }逻辑说明Status在这里是int的别名返回OK或ERROR比返回void更能表达插入是否成功pos是题面里的序号从1开始而数组下标从0开始所以写数据时要用pos-1后移循环从L-length开始每轮把前一个位置的值复制到当前位置这样不会覆盖还没移动的元素。参数说明cap是初始容量MAXSIZE取100对大多数课后题够用如果题目要求“动态扩容”把容量不足时的ERROR分支改成realloc重新分配即可这也是顺序表这章常考的变体。2.3 链表逆置和二叉树遍历指针操作与回调函数单链表就地逆置是第2章算法设计题的高频题核心是头插法加一个q指针保存后继。没有qp-next被改写后就找不到下一个结点了。typedef struct LNode { ElemType data; struct LNode *next; } LNode, *LinkList; void ReverseList(LinkList *L) { // 就地逆置带头结点的单链表 LNode *p (*L)-next; // p 指向第一个数据结点 LNode *q; (*L)-next NULL; // 先把新链表置空准备头插 while (p ! NULL) { q p-next; // q 保存后继防止断链 p-next (*L)-next; // 头插法新结点插到头结点后面 (*L)-next p; p q; } }逻辑说明为什么用二级指针LinkList *L因为ReverseList要修改头指针的next域如果只传LinkList L函数内改的是形参副本调用结束后原链表纹丝不动。这是C语言指针最经典的坑。参数说明这里假设是带头结点的链表考试时看清题面有没有“带头结点”四个字不带头结点时逻辑大体相同但开头要单独处理第一个结点的next改为NULL。二叉树遍历设计题的答案几乎都要用函数指针。书中visit(T-data)这种写法在纯C里要按照回调函数的标准形式声明参数。typedef struct BiTNode { ElemType data; struct BiTNode *lchild, *rchild; // 左右孩子指针 } BiTNode, *BiTree; void PreOrder(BiTree T, void (*visit)(ElemType)) { if (T) { visit(T-data); PreOrder(T-lchild, visit); PreOrder(T-rchild, visit); } }逻辑说明递归出口是T为空visit把访问操作解耦——同一套遍历代码你只需要换visit的实现就能变成求深度、数叶子、打印全部元素等多个版本。参数说明void (*visit)(ElemType)是一个函数指针调用时传一个自己写的print函数进来例如void print_elem(ElemType e) { printf(%d , e); }然后调用PreOrder(root, print_elem)。函数指针是很多数据结构设计题答案的骨架不会用的话树的题会写得很吃力。这一章做下来如果按上面的骨架重写10个文件大约一个晚上能完成。边改边编译才能发现书中代码省略了多少类型信息。手边备一份按这个思路整理的源码版是省时间的正路。3. 算法设计题答案的拆解思路查找、排序与图遍历的通用套路3.1 算法设计题的五类高频题型与答案组织方式结合考研数据结构和408的真题风格严蔚敏每章结尾的算法设计题虽然变化多但题型能按数据组织方式归成五类。我习惯用一个表格把它们框起来然后逐类补代码。题型对应章节核心考点答案验收标准顺序表/链表改造第2章线性表插入、删除、逆置、合并空表、单元素、重复值都正确栈和队列模拟第3章栈和队列括号匹配、双栈模拟队列出栈/出队顺序正确树的性质计算第6章树和二叉树深度、叶子数、相似性、层次遍历空树返回0递归不越界图遍历与拓扑第7章图DFS/BFS非递归、邻接表、拓扑排序结点不漏、不重入度处理正确排序与查找改造第9章查找、第10章内部排序折半查找、快排改进、堆排序复杂度不劣化稳定性说明清楚按这个表把每个题目整理成“题目一句话、思路三步、可运行代码、边界用例”四段式数据结构实验报告和期末复习都能直接复用这个结构。我见过很多人拿着答案抄抄完连题目要考什么都说不清根源就是答案组织得太散没有按题型归类。3.2 查找类设计题折半查找的非递归写法与二叉排序树判断折半查找是第9章必考设计题非递归写法比递归更常被要求上机。关键是维护一个循环不变量目标只可能存在于闭区间[low, high]里。每轮比较后收缩区间直到区间为空。int BinarySearch(int a[], int n, int key) { int low 0, high n - 1, mid; while (low high) { mid low (high - low) / 2; // 防止 lowhigh 整数溢出 if (a[mid] key) return mid; else if (a[mid] key) low mid 1; else high mid - 1; } return -1; // 查找失败 }逻辑说明mid low (high - low) / 2这个写法比(lowhigh)/2更安全low和high都很接近上限时后者可能溢出。循环条件是low high而不是low high否则当区间只剩一个元素时会漏判。参数说明数组a必须是有序的n是长度key是待查值如果题面说“下标从1开始”那high要改成n返回时也要处理好下标偏移。查找失败返回-1是常见约定也有考场要求返回0以题面为准。二叉排序树判断本身就是设计题答案里的一个固定模式中序遍历序列严格递增树就是BST。实现时用全局变量保存上一个访问值。int pre -1; // 初始值要小于树里所有元素 int flag 1; // 一旦发现逆序就置0 void InOrderCheck(BiTree T) { if (T flag) { InOrderCheck(T-lchild); if (T-data pre) flag 0; // 要求严格递增相等也不行 pre T-data; InOrderCheck(T-rchild); } }逻辑说明这个思路把“判断性质”转化成了“遍历序列校验”代码量比递归比较左右子树小很多。flag的作用是短路——已经判定不是BST了就不要再往下递归。参数说明如果树内允许重复值即非严格递增把条件改成T-data pre即可但题目如果说“二叉排序树”默认是严格递增考生需要在答案开头写清假设。这里初始pre设成-1如果元素可能为负用INT_MIN更稳。3.3 图遍历设计题邻接表定义与DFS非递归实现图这一章的设计题直接考邻接表定义的居多。先把结构体定义写对后面代码才不飘。邻接表 顶点数组 每条边的弧结点链。#define MAXV 100 typedef struct ArcNode { // 边表结点 int adjvex; // 邻接顶点编号 struct ArcNode *next; // 下一条边 } ArcNode; typedef struct VNode { // 顶点表结点 int data; ArcNode *first; // 第一条边 } VNode; typedef struct { VNode adjlist[MAXV]; int n, e; // 顶点数、边数 } AdjGraph;逻辑说明这条定义链是图章节所有算法的基础。first指向第一条边访问某个顶点的所有邻居就是沿这条链走下去。参数说明如果边带权ArcNode里加一个int weight字段MAXV按题目最大顶点数定上机题一般给到100够用更大就改成动态分配。DFS递归转非递归用栈保存待访问结点visited数组防止重复入栈。这个转换是图设计题里最容易翻车的地方因为访问顺序和递归版不完全一样但结点了不漏这个要求必须满足。void DFS(int v, int visited[], AdjGraph *G) { int stack[MAXV], top -1; ArcNode *p; visited[v] 1; stack[top] v; while (top 0) { v stack[top--]; printf(%d , v); for (p G-adjlist[v].first; p; p p-next) { if (!visited[p-adjvex]) { visited[p-adjvex] 1; // 入栈前标记避免重复入栈 stack[top] p-adjvex; } } } }逻辑说明和递归版的一个明显差异是非递归版是入栈时立刻标记visited而不是出栈时标记。如果出栈才标记同一个顶点可能被周围多个邻居重复压栈栈会膨胀一倍还可能死循环。参数说明visited数组初始全0调用前由外部创建栈大小按顶点数定这里用MAXV严格做法是malloc一个G-n大小的数组。拓扑排序是另一个套路统计入度入度为0的顶点入栈每出栈一个顶点就把它所有邻接点的入度减1减到0再次入栈最后出栈序列就是拓扑序列。判断有环的办法是记录出栈顶点数少于顶点总数说明有环。这些图的答案正确性不好肉眼判断最稳的是打印访问序列再写一个校验函数确认每个结点恰好出现一次这个验证方法放到第5章统一讲。4. 严蔚敏书中源码移植避坑从引用符号到内存泄漏的五个常见问题4.1 现象gcc报错 expected ‘;’ before ‘e’指到一个奇怪的符号原因是严蔚敏书里的函数参数大量使用ElemType e这种C引用写法纯C编译器不识别参数列表里的。解决方法是把形参里的改成指针对应实参处加取地址。例如书上的DeleteElem(L, e)如果写作ElemType e落地就改成ElemType *e调用处写成DeleteElem(L, e)。这个坑几乎每个照着书上代码敲的人都会踩一遍我当年在实验课上卡了二十分钟才反应过来是语言语法问题不是算法问题。4.2 现象程序能跑但循环一万次后内存越来越少或Valgrind报definitely lost原因是删除链表时只free了头结点数据结点全漏了或者顺序表退出前忘了free(L.data)。解决方法是遍历链表逐个释放先用q保存下一个结点再free当前结点最后把头指针置NULL。顺序表则在main函数return前补一句free(L.data)。记住free之后如果不置NULL后续误用就成了野指针翻车现场。这个坑在写“清空链表”这类设计题时几乎是必经之路写完顺手跑一次Valgrind能少懊恼半天。4.3 现象printf打印的顺序和实际执行顺序不一致或者程序结束后才一次性输出原因是stdout在交互终端下是行缓冲重定向到文件或管道时变成全缓冲输出内容积压在文件缓冲区里没刷出来。解决方法是每条调试输出都带\n必要时调用fflush(stdout)强制刷缓冲。另外连着用scanf和getchar时前面输入残留的换行符会被下一次读取吞掉这也是缓冲区相关的常见玄学。遇到“输出顺序不对”先别怀疑算法先看是不是缓冲问题很多刚学C语言基础的人在这里浪费时间。4.4 现象链表反转题里的a b这类表达式看不懂甚至算出错值原因是C语言里ab是先自增再赋值和ab完全两回事。书中源码有时把指针后移和取数据写在同一行阅读负担很大。解决方法是不要追求一行写完把p p-next;和q p-next;拆开成两步写可读性和正确率都上去。这是我的一点血泪经验考场上手写代码时多写一行不会扣分写错一个运算符直接零分。4.5 现象VSCode里源码报“无法打开源文件 stdio.h”或虚拟机Ubuntu里gcc都找不到原因是编辑器没有配置includePath或者系统里确实没装build-essential。解决方法是先执行sudo apt install build-essential确认gcc存在再在VSCode的c_cpp_properties.json里设置includePath和compilerPath。{ configurations: [ { name: Linux, includePath: [${workspaceFolder}/**, /usr/include], defines: [], compilerPath: /usr/bin/gcc, cStandard: c11 } ], version: 4 }逻辑说明includePath告诉IntelliSense头文件在哪里compilerPath指向gcc可执行文件。配置好后重载窗口红波浪线就会消失。参数说明如果用的是Windows下的MinGW路径改成你的编译器实际安装目录macOS用户则写成/usr/bin/clang。这个文件配好后一劳永逸之后再遇到“无法打开源文件”就知道是路径问题不是代码问题。这些坑有个共同规律都不是算法逻辑本身的错而是书的伪代码语言和C编译器语言之间存在翻译层。建立一套“先编译、后看输出、再谈算法”的检查顺序能省掉大量玄学排查时间。5. 验证算法设计题答案从GDB断点到408机试的完整闭环5.1 用断言和边界用例做回归验证算法设计题答案写完第一件事不是看打印结果而是写断言。assert能在条件不满足时直接终止并报告行号比肉眼扫输出可靠得多。常见做法是给每个函数配一个小测试函数把正常用例、空输入、单元素、重复值全部跑一遍。#include assert.h void test_reverse(void) { LinkList L CreateList(5); // 创建一个 1 2 3 4 5 的链表 ReverseList(L); assert(GetLength(L) 5); // 长度不能变 assert(GetFirst(L) 5); // 反转后第一个元素必须是 5 printf(test_reverse passed\n); }逻辑说明断言函数的返回值或状态程序没崩就说明这一项过了。每次改动算法后跑一遍全部测试回归成本几乎为零。参数说明CreateList、GetLength、GetFirst这些函数按题目自行实现测试函数只关注结果不关注过程。边界用例至少覆盖空表、单元素表、两个元素表和全是重复值的表这四种情况能拦下八成隐蔽bug。5.2 GDB在源码层面观察指针到底指到哪VSCode的调试器底层也是GDB命令行会了图形界面自然懂。编译时加-g生成调试信息然后gdb进入交互界面。gcc -g -o test test.c gdb ./test (gdb) break ReverseList (gdb) run (gdb) next (gdb) print *L (gdb) print *p (gdb) backtrace (gdb) quit逻辑说明break在函数入口停住run跑到断点next单步执行print *L能直接看到头结点next指向是否已经改变。当递归树题写崩时backtrace看调用栈能快速定位是哪个递归层出了问题。参数说明-g是生成调试信息的关键参数没有这个符号表print命令只能输出地址看不到结构体内容。GDB这套命令十分钟就能上手但能救命的场景往往是考前最后一晚。提示调试链表和二叉树时print一个结构体指针不如print *p直观多按几次print能看到指针指向的结点内容。5.3 对接考研数据结构与408答案怎么才算“过”很多读者买这本书是为了数据结构考研、408专业课或数据结构期末复习。单纯把答案抄出来不算完要按三关验收。第一关是边界关空输入、单元素、最大规模都能跑第二关是复杂度关设计题题面明确要求O(n)或O(log n)时答案里不能出现嵌套循环第三关是表达关机试按函数接口给分时函数签名要和题面一致比如题目要返回下标答案就不能返回指针。交叉验证的办法也很简单拿王道单科书对应章节的题目把严蔚敏教材里验证过的算法跑一遍伪输入再把PTA上的字符串逆序、冒泡排序、完数这类C语言基础题当作热身它们本质是那一章设计题的低配版。这样验证过的答案才敢写进实验报告。408和考研数据结构不会直接考“背答案”但会把同一套算法换一个数据组织方式再考一遍所以按题型整理答案比按题号整理答案更有价值。6. 把严蔚敏习题答案沉淀成自己的算法模板仓库到这里最值得做的下一步不是继续刷题而是把验证过的代码整理成自己的模板仓库。我一般按章节建目录从第2章线性表一路放到内部排序每个文件只放一个算法设计题的“题目一句话、思路三步、可运行代码、边界用例”文件开头写清楚编译命令和踩过的坑。以后做实验报告、期末复习、考研数据结构或准备408直接翻这个仓库比翻书快得多。整理时有个口味上的建议优先保留自己亲手调通的版本不要保留书上的伪代码原文。伪代码是参考能跑的是答案。格式统一成函数接口在前、静态逻辑在后、main函数里只留测试断言这样复试上机时直接把函数抠出来贴到考场代码里就能用。配合翁恺的练习题或谭浩强教材的习题做二次交叉验证覆盖的题型会更广。我自己的教训是当年抄书上的算法抄得很爽考前三天试着编译那一整段才发现到处是引用符号和malloc缺转换那个晚上基本没睡。后来坚持“写完一个文件就立刻gcc编译”才把这些坑从玄学变成流程。这个方向值不值得投入如果你在准备数据结构期末复习或考研答案是值得——它把一本需要脑补的教材变成一套能跑、能验、能复用的本地工程。希望帮到你。本文还有配套的精品资源点击获取