
简介这是一份数据结构C语言版经典教材的完整PDF由严蔚敏、吴伟民编著适合计算机专业本专科学生、考研人群以及希望夯实算法基础的程序员阅读。全书系统讲解线性表、栈和队列、串、树与二叉树、图、查找、排序等核心知识配合C语言描述数据存储结构与基本操作帮助读者将抽象理论转化为可实现的编码能力。压缩包内仅包含1个PDF文件文件大小约29.15MB内容完整清晰可直接在电脑、平板等设备上阅读或打印使用。目前已有4046人学习下载是自学数据结构与备战考试的高频参考资料。借助书中的算法示例和课后习题读者可以逐步掌握递归、指针、结构体等C语言技巧并理解各类数据结构的实际应用场景为后续学习算法设计与操作系统等课程打下坚实基础。1. 严蔚敏《数据结构C语言版》PDF考研题库的经典底稿考研复习到二叉树那一章的时候我翻着手机里的严蔚敏《数据结构C语言版》PDF一遍一遍手写前序中序的递归展开才意识到不少 408 真题的模型都在这本书的例题里。这份 PDF 是严蔚敏、吴伟民合编的经典教材从线性表一路覆盖到外部排序代码示例采用类 C 伪代码适合手抄、改写成可运行程序也适合按章节刷复杂度推导。准备考研数据结构或期末考的在校生可以把这本书当“题库底稿”用准备技术面试的开发者则能拿它当链表、树、图的手写代码复习清单刚学完 C 语言的人也可以借它补上“程序算法数据结构”的完整视图。别把这本书用来“读”它是用来“抄”的抄完再谈理解。2. 线性表全书最薄却最常考的章节两份能直接抄的代码模板2.1 顺序表与链表的选择先看读多还是写多线性表这章在书里篇幅不算长但它把所有数据结构的两种基本存储形式说透了顺序存储和链式存储。顺序存储下逻辑上相邻的元素在物理内存里也相邻查找按下标直接算出地址时间复杂度是 O(1)但插入一个元素需要把后面的元素整体后移平均时间复杂度 O(n)而且受数组容量限制。链表则相反逻辑相邻不代表物理相邻每个结点靠指针串起来插入删除只需要改指针但按下标访问必须从头遍历。面试里高频问的“vector 和 list 怎么选”对应的就是这张顺序表与链表的对比表。我自己写日志模块时一开始用顺序表存待落盘的消息中间频繁删除导致整块移动性能瓶颈一眼就能看出来改成链表后删除只断链吞吐量立刻上去了。严蔚敏这章的价值就是先让你把“访问复杂度”和“修改复杂度”分开记别混在一起算。下面结合书里“线性表的链式表示和实现”的思路给一个可直接编译的单链表模板。注意我用的代码是带头结点写法这是严蔚敏书里很强调的统一处理方式面试时也建议保持这个习惯空表和非空表的插入删除就能共用同一套逻辑。2.2 带头结点单链表的插入与删除可编译的 C 实现#include stdio.h #include stdlib.h typedef struct LNode { int data; struct LNode *next; } LNode, *LinkList; // 在第 i 个位置插入值为 e 的新结点链表带头结点i 从 1 开始计数 int ListInsert(LinkList L, int i, int e) { LNode *p L; // p 从头结点出发找第 i-1 个结点 int j 0; while (p j i - 1) { p p-next; j; } if (!p || j i - 1) return 0; // i 不合法小于 1 或超过表长加一 LNode *s (LNode *)malloc(sizeof(LNode)); s-data e; s-next p-next; // 先连后继 p-next s; // 再改前驱 return 1; }这段代码的边界条件要细看。i从 1 开始计数循环结束后p指向第i-1个结点如果p为空说明i超过了表长加一j i-1的情况主要用来防御循环条件异常考试时可以不用深究但面试官喜欢追问这一点。插入时“先连后继再改前驱”的顺序不能反过来否则新结点还没接上旧链表就先断了。再看删除它和插入有一个容易混淆的细节// 删除第 i 个结点并把它的数据赋给 *e int ListDelete(LinkList L, int i, int *e) { LNode *p L; int j 0; while (p-next j i - 1) // 注意这里判断的是 p-next { p p-next; j; } if (!p-next) return 0; // 第 i 个结点不存在 LNode *q p-next; // q 是要删除的结点 *e q-data; p-next q-next; // 跳过 q free(q); return 1; }删除时循环条件用p-next而不是p因为我们要找的是“被删结点的前驱”。如果用p判断表尾结点存在时循环会多走一步最后p-next为空才退出正好错过目标。严蔚敏书里反复用p-next这个写法不是随手写的是为了配合头结点统一处理空表和非空表这个细节值得单独标一下。提示插入用p判断删除用p-next判断两段代码对照着抄五遍比单独背哪一行更重要。2.3 链表反转书上没单独列但上机必考的经典题严蔚敏这章正文讲的是建表、插入、删除、按值查找和按序号查找但实际考试和面试里链表的出镜率几乎都集中在反转、合并两个有序链表、找中间结点这三题上。反转链表可以从“就地逆置”的思路出发用三个指针滚动实现// 带头结点的单链表就地反转 void ReverseList(LinkList L) { LNode *prev NULL; LNode *cur L-next; while (cur) { LNode *next cur-next; // 先记住后继否则断链后找不到 cur-next prev; // 当前结点指向前一个 prev cur; // 前驱指针前进 cur next; // 当前指针前进 } L-next prev; // 最后把头结点指向新的首结点 }我第一次手写反转时漏了next cur-next这一步cur-next被改写后原链表后半段就成了孤儿结点。为什么需要三个指针因为链表是单向的每改一个next就必须先用临时指针保住原来的后继这是链表类题目“掉链子”的根源。把这段模板和 2.2 的插入删除模板放一起熟记线性表这块的地基就算铺开了。从这章的考点分布也能看出严蔚敏的编排思路线性表是后面所有结构的地基树用链式存储图的邻接表也用链式存储甚至哈希表的链地址法还是它。所以学完这章比记代码更重要的是建立“物理结构”和“逻辑结构”两个概念后面学跳表、B 树时才不会觉得突兀。3. 栈、队列与二叉树递归的两种容器和三个遍历模板3.1 栈的括号匹配压栈弹栈的典型应用栈是“后进先出”的线性表只在表尾插入删除。严蔚敏书里给了进制转换、括号匹配、表达式求值三个例子最直观的是括号匹配。思路是遇到左括号就压栈遇到右括号就检查栈顶是否匹配匹配就弹栈不匹配直接返回失败。#include stdio.h #include string.h int isMatch(const char *s) { char stack[100]; // 固定容量栈上机够用 int top -1; int len (int)strlen(s); for (int i 0; i len; i) { if (s[i] ( || s[i] [) { stack[top] s[i]; // 压栈 } else if (s[i] )) { if (top 0 || stack[top] ! () return 0; // 栈空说明右括号多了不匹配也返回失败 top--; // 弹栈 } else if (s[i] ]) { if (top 0 || stack[top] ! [) return 0; top--; } // 其他字符直接忽略 } return top -1; // 栈空才算彻底匹配 }用top而不是封装的栈结构是因为这道题只关心“顶端”的变化。这里有两个关键判断top 0必须写在stack[top]取值之前否则“空栈取顶”会数组越界。我自己练习时把top 0写在了后面右括号一多gdb 追到一半才看出来是越界问题。这段代码还有一个值得扩展的点如果题目改成三种括号() [] {}只需要在判断里加一层映射逻辑完全不变。3.2 二叉树的前中后序递归访问顺序的三种版本树这一章是严蔚敏书里最需要耐心的部分核心是遍历。前序根左右、中序左根右、后序左右根三个递归模板结构完全一样区别只在访问结点的时机typedef struct BiTNode { char data; struct BiTNode *lchild, *rchild; } BiTNode, *BiTree; void PreOrder(BiTree T) { if (T) { printf(%c , T-data); // 前序先访问根 PreOrder(T-lchild); PreOrder(T-rchild); } } void InOrder(BiTree T) { if (T) { InOrder(T-lchild); printf(%c , T-data); // 中序左、根、右 InOrder(T-rchild); } } void PostOrder(BiTree T) { if (T) { PostOrder(T-lchild); PostOrder(T-rchild); printf(%c , T-data); // 后序先左右再根 } }这段代码的“递归展开顺序”可以用栈来理解每次调用函数当前结点的状态被压栈返回时弹栈继续执行它下面的语句。所以中序遍历就是“先尽量往左走走不动了访问根再走右子树”。很多人不理解为什么二叉搜索树的中序序列一定是递增的其实就是这个递归顺序决定的。把这三个模板抄下来再手动模拟一棵只有三个结点的“根、左、右”树跑一遍流程比背十遍文字都管用。面试如果追问“给你前序和中序怎么重建二叉树”答案也在递归模板里前序第一个元素是根中序里找根的左右区间然后递归处理两个子区间。3.3 循环队列的满与空模运算为什么会吞掉一格空间这一节是常见考区尤其期末复习时总有一道“判断循环队列空/满”的选择题。用 front 指向队头、rear 指向队尾的下一个位置时rear front既可能是空也可能是满。严蔚敏书里的循环队列用模运算让 front 和 rear 在数组内转圈并且牺牲一个存储单元来区分空和满当(rear 1) % MAXSIZE front时判定为满。#define MAXSIZE 100 typedef struct { int data[MAXSIZE]; int front; // 队头下标 int rear; // 队尾下标指向下一个写入位置 } SqQueue; int EnQueue(SqQueue *Q, int x) { if ((Q-rear 1) % MAXSIZE Q-front) return 0; // 队满 Q-data[Q-rear] x; Q-rear (Q-rear 1) % MAXSIZE; return 1; }这里的% MAXSIZE保证 rear 从 99 自然回到 0从而成环。MAXSIZE一般设为实际数组长度能存放的元素个数是MAXSIZE - 1那一格用来区分空满。面试问“循环队列怎么判断空和满”至少要有三种方案牺牲一个存储单元、用计数器记录元素个数、用 flag 标记最后一次操作是入队还是出队。书里选的是牺牲存储单元很多人对“被浪费的一格”耿耿于怀但这正是考点。4. 图的存储与查找邻接表与折半查找的选型逻辑和边界坑4.1 邻接矩阵与邻接表怎么选先看稀疏还是稠密图的存储是考研数据结构里的常驻考点期末复习时也总有一道填空或者简答。邻接矩阵用一个 n×n 的二维数组存边判断两个顶点是否有边是 O(1)但不管边多边少都要占 n² 空间邻接表为每个顶点挂一条链表空间只跟边数成正比适合稀疏图。维度邻接矩阵邻接表空间复杂度O(n²)适合稠密图O(ne)适合稀疏图判断两点是否相连O(1)O(度)找某个顶点的所有邻接点O(n)O(度)常见命题方向Prim、Floyd 的实现基础DFS、BFS、拓扑排序备考时可以把选择逻辑压缩成一句话“边少用表点多用阵。”理解成本并不高关键是做题时先判断边的数量级。比如顶点只有几十个直接上矩阵写起来省事如果顶点上万邻接表就是必然选择。严蔚敏书里两种结构都给了完整定义我建议把邻接表的结构体定义多抄两遍因为它的图例和代码对照起来很直观。4.2 DFS 和 BFS 模板图遍历的可复用写法图遍历是算法题的地基DFS 的递归版和 BFS 的队列版需要形成肌肉记忆。下面用邻接矩阵做一个最简版本矩阵本身就充当了“边判断表”代码里最需要注意的就是 visited 数组的清理#include stdio.h #include string.h #define MAX_VERTEX 100 int visited[MAX_VERTEX]; // 邻接矩阵方式的 DFSv 是起始顶点下标n 是顶点总数 void DFS(int G[MAX_VERTEX][MAX_VERTEX], int n, int v) { visited[v] 1; printf(%d , v); for (int i 0; i n; i) { if (G[v][i] !visited[i]) { DFS(G, n, i); } } }BFS 则是用一个数组队列维护“待访问”的顶点int queue[MAX_VERTEX]; // 邻接矩阵方式的 BFS void BFS(int G[MAX_VERTEX][MAX_VERTEX], int n, int v) { int front 0, rear 0; visited[v] 1; queue[rear] v; while (front ! rear) { int cur queue[front]; printf(%d , cur); for (int i 0; i n; i) { if (G[cur][i] !visited[i]) { visited[i] 1; // 入队前就标记避免重复入队 queue[rear] i; } } } }使用时最常翻车的点不是遍历本身而是忘记清理 visited。在主函数里调用前做一次memset(visited, 0, sizeof(visited))否则上一个测试用例留下的标记会让第二次遍历结果错误。这个坑在小数据集上不显眼一旦测试用例多了get 到的全部是脏数据。4.3 折半查找low/high 更新的两个边界坑折半查找在严蔚敏书里是查找章第一个完整给出的算法代码非常短但边界的讨论能拖出三个考点。最经典的版本长这样// 在有序数组 a 里查 key返回下标查不到返回 -1 int BinarySearch(int a[], int n, int key) { int low 0, high n - 1; while (low high) { int mid low (high - low) / 2; // 防止 leftright 溢出 if (a[mid] key) return mid; else if (a[mid] key) low mid 1; else high mid - 1; } return -1; }第一个坑是循环条件low high不能写成low high。写成后者会漏掉最后剩下单个元素的情况比如数组只有一个元素且它就是要找的 key循环根本进不去直接返回 -1。第二个坑是mid的写法标准教材里常见(low high) / 2但在算法题里low high可能溢出成负数安全的写法是low (high - low) / 2。面试时能主动说出这一点通常比写对代码本身更加分。5. 严蔚敏这本教材的避坑清单四个最常见的翻车点5.1 书上代码直接抄进 IDE编译报错一大片**现象**把书里的线性表代码原样敲进 IDE报错信息里有Status未定义、ElemType未定义、malloc找不到头文件好像书是故意写不全的。**原因**严蔚敏这本书里的代码是“类 C 伪代码”不是可以直接交付的完整程序。教材会先用Status、ElemType这类抽象类型做讲解再省略头文件和宏定义把注意力集中在算法逻辑上而不是编译细节。**解决**自己补全前置声明。一般我会在文件头部加#include stdio.h、#include stdlib.h再用typedef int ElemType;和typedef int Status;把抽象类型落到具体类型上。抄代码时保持“先补头、再补类型、最后补 malloc 检查”的顺序这份代码就基本能直接跑起来。5.2 链表修改函数用一重指针传参返回后链表没变**现象**写了一个void initList(LinkList L)函数内部执行L malloc(sizeof(LNode))返回后 main 里的 L 还是 NULL链表好像凭空消失了。**原因**C 语言是传值调用形参L只是实参的一个副本。函数内部修改形参的指向不会影响实参指针本身。只有修改指针所指向的内容时一重指针才够用。**解决**需要让函数修改指针本身时改用二级指针void initList(LinkList *L)或者在函数里返回新指针。简单记口诀是“改内容用一级改指向用二级”。我用这个习惯检查所有链表函数后来写二叉树的插入时也少踩了很多坑。5.3 数组下标从 1 还是从 0 开始前后越写越乱**现象**书里描述顺序表插入时用“第 i 个位置”i 从 1 开始C 语言数组下标却从 0 开始。两套逻辑混在一起循环边界不是差一位就是把表头元素判成表尾。**原因**教材用的是“逻辑序号”和数学语言线性表的第 1 个元素对应数组下标 0。严蔚敏的书为了讲解抽象结构全程用 1 到 n 的区间而 C 语言实现必须映射到 0 到 n-1。**解决**一是在每个数组相关的函数开头加注释写明“本函数使用 0 基下标”二是接口层把用户的 1 基位置转换成内部 0 基下标再做数组操作。别试图在过程中来回换算容易把自己绕晕。5.4 只背复杂度结论一遇到递归递推就推错**现象**知道快排平均时间复杂度是 O(n log n)但题目换一个递归式T(n) 2T(n/2) O(n)就让算复杂度却不会展开只能反推答案对不对。**原因:**把复杂度当成背诵材料没有理解“递归树展开”和“主定理”的本质。考研数据结构里递推式的展开比结论本身更容易出选择填空。**解决**回看严蔚敏书第 1 章算法分析的部分把归并排序的递推式手动展开成递归树数一遍每层的工作量再把斐波那契递归的指数级复杂度算出来体会“重复子问题”的膨胀速度。自己推过一遍之后再遇到复杂度题就不会心虚。6. 因式分解之后把教材压成可用模板的两周验证路径6.1 按章节建立“抄写→验证→复盘”的节奏我之所以推荐把教材当成模板库而不是普通 PDF 来读是因为数据结构这门课的知识点太稠密。只看不写三周后能剩下的只有“链表大概是个链状结构”这种模糊印象。我的习惯是给每个章节配一个固定动作读完一节立刻把书上的伪代码改写成可运行程序然后在题目集里找两到三道同类题验证。时间段教材章节必须手写的模板验证方向1-2 天第 2 章线性表插入、删除、反转链表链表基础题两道3-4 天第 3 章栈和队列括号匹配、循环队列带栈顶变化的模拟题5-7 天第 6 章树和二叉树三序遍历、层序遍历、求深度二叉树遍历和重建8-12 天第 7 章图DFS、BFS、邻接表建图图遍历和最短路径13-14 天第 9 章查找、第 10 章排序二分查找、快排、归并手写排序和查找的边界题这个表不是固定不变的但有一个原则每段内容必须留下至少一个能运行的产物。如果当天读的是概念性的章节就把复杂度递推式在纸上展开两遍。用这种方式过了两周我再看到“链表头插”这样的题目时手指已经能自动写代码不需要现场想。6.2 用 gdb 验证链表插入的每一步状态对于链表和递归这类指针密集的代码我最推荐用 gdb 做单步验证。先编译时带-g参数然后把断点打在插入函数的关键行上gcc -g list.c -o list gdb ./list break 12 if p-next NULL run print L print p-data step print p-next-dataprint L看头结点地址print p-data验证当前指针是否停在预期位置step单步跟进看 next 指针的变化顺序。这个流程能把“指针丢失”类的 bug 变成可视化过程比干瞪眼改代码快得多。从那以后我每次复习一个新模块都强制走一遍“读一章→抄一遍模板→跑 gdb 验证→隔天默写”的流程这套方法帮我避开了“看了三天但什么代码都没留下”的假复习希望帮到你。本文还有配套的精品资源点击获取