简介:严蔚敏、吴伟民编著的《数据结构(C语言版)》是计算机专业广泛使用的经典教材,适合高校学生、考研复习者及C语言自学者,用来系统搭建数据结构与算法知识体系。该资源为单册PDF电子书,仅1个文件,压缩包大小约29.15MB,可方便地在电脑、平板或手机上随时翻阅。全书以线性表、栈与队列、串、树与二叉树、图、查找和排序为主线,配有大量类C伪码与算法步骤,并覆盖顺序存储与链式存储的设计对比、递归思想、图的遍历与最短路径等核心内容,帮助读者把抽象概念落实到具体编码。已有4032人学习下载;这份电子版既适合课堂同步参考,也适合考研专业课复习和期末冲刺。数据结构也是后续算法设计、操作系统、编译原理等课程的基础,保留一份PDF便于长期查阅与重点标注。
1. 严蔚敏数据结构C语言版PDF:这本经典教材的正确打开方式
严蔚敏、吴伟民的《数据结构(C语言版)》在国内计算机教育里的地位,从大一的专业课到考研408统考大纲,几乎贯穿整个本科阶段。很多人第一反应是先找这本书的PDF,原因很实际:纸质书又厚又贵,电子版方便随时检索算法代码、跳转课后习题,还能在阅读器里做标注。PDF拿到手只是第一步,真正拉开差距的是怎么读——哪些章节要手写代码,哪些只看概念就够,线性表、树、图之间怎么串起来,这些才是决定学习效率的分水岭。这篇笔记就把教材框架、代码落地和常见踩坑一次说清楚。
2. 先拆教材框架:线性表、树、图三条主线的最省力读法
这本教材的章节安排乍看是标准套路:绪论、线性表、栈和队列、串、数组和广义表、树和二叉树、图、动态存储管理、查找、内部排序。但深入读会发现这些章节不是并列关系,而是层层依赖。线性表是所有结构的底层基础,栈和队列是受限的线性表,树和图的存储也要借助线性结构表达。我自己第二遍复习时的策略是:先按目录过概念建立全局感,再按“线性表→树→图”三条主线分别集中攻代码,查找和排序放到最后冲刺阶段。这个顺序和课堂教学顺序不完全一样,但对期末复习和408备考效率更高。
2.1 线性表部分:顺序表和链表的代码起点是后续所有结构的底层
线性表章节的第一道代码门槛是结构体定义和两种存储方式的对比。教材里顺序表用数组实现,链表用动态节点实现,这个二元选择贯穿整本书的存储设计。顺序表要记住三个属性:存储起始位置、当前长度、最大容量。链表要理解头指针、节点、链域三者的关系。很多人这一章只记住“链表插入不需要移动元素”这个结论,却没注意教材用的到底是有头节点还是无头节点的版本。
带头结点的写法让插入和删除在空表时不用单独判断头指针,代码分支更统一,代价是额外占一个头节点空间。不带头结点的写法更贴近物理结构,但边界条件多。教材先用不带头结点的版本讲原理,再用带头结点的版本做封装,这两种写法之间的转换经常被期末试卷拿来出代码填空题。我做过一个对比实验:同样是“按位置删除”操作,带头结点版本约15行,不带头结点版本要20行以上,而且后者更容易漏判空表。408真题也在这里设过陷阱,比如头节点的数据域到底存不存有效值。
2.2 树与二叉树:递归函数的设计模式是这章的核心能力
二叉树章节是整本书的分水岭。很多前期顺利的同学到这里开始翻车,原因是递归思维没有建立起来。教材把前序、中序、后序、层次遍历放在一起,前三个用递归解决,层次遍历用队列解决。我在这一章的练习方式比较笨但有效:把教材里的三种递归遍历函数抄在纸上,用同一棵三节点二叉树手动画出每一层的函数调用栈。画到第三遍才真正理解“访问节点的时机决定遍历顺序”这句话。
这章还有一个容易被忽略但考试常考的知识点:线索二叉树。教材花了不少篇幅讲线索二叉树的建立与遍历,有的同学直接跳过,但期末和408选择题都喜欢在这里出题。线索化的本质是让空链域指向遍历前驱或后继,理解它需要先熟悉三种遍历顺序。我的建议是不用背完整代码,但必须能在给定一棵树和指定遍历顺序时,画出每个空指针应该指向的位置,这一步过关之后再去看建立线索的代码会容易很多。
2.3 图结构:邻接矩阵存稠密图、邻接表存稀疏图的边界条件
图这章的存储部分有明确的工程取舍逻辑。邻接矩阵的内存是V²级别,判断两个顶点是否相邻是O(1)复杂度;邻接表的内存是V+E级别,判断相邻顶点需要遍历链表。教材把两种结构并列介绍,看起来像是都要掌握,实际选择标准就一条——边数接近N*(N-1)/2就用矩阵,边数远小于这个值就用邻接表。408真题出现过“给定边数和顶点数判断应使用哪种存储结构”的选择题,本质就是在考这条边界条件。
图的遍历算法里,DFS的递归版本和树的前序遍历几乎一样,BFS用到的队列就是栈和队列章节的产物。我在这一章终于意识到教材为什么花一整章讲栈和队列,它们就是为树和图遍历准备的工具。如果前面线性表部分没有好好写链表实现,到图这里再做邻接表会非常吃力。所以读图这章之前,先回去确认两件事:链表的插入删除能否不看书写出来,队列的数组实现能否不看书写出来。这两关没过,图章节的代码题基本写不对。
3. 把教材伪代码写成能跑的C程序:链表、栈、队列的实操细节
严蔚敏教材里大量算法以类C伪代码呈现,考试要求手写,工程里要能跑。我一般的学习顺序是先把伪代码翻译成完整可运行的C程序,再对照教材检查边界条件。这个翻译过程能暴露大量细节:函数参数该用一级指针还是二级指针、结构体成员需不需要初始化、malloc后要不要判断返回空指针。下面三个案例是高频出现的基础模板。
3.1 单链表插入删除的完整实现:函数参数为什么要用二级指针
先看不带头结点的单链表在头部插入的场景。很多人第一次写会翻车,原因在C语言函数传参机制上:
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node *next; } Node; // 不带头结点的头插法:必须用二级指针 int insertHead(Node **head, int val) { Node *newNode = (Node *)malloc(sizeof(Node)); if (newNode == NULL) return 0; newNode->data = val; newNode->next = *head; // 新节点指向原来的头 *head = newNode; // 更新头指针 return 1; } // 带头结点的尾插法:只需要一级指针 int insertTail(Node *head, int val) { if (head == NULL) return 0; Node *p = head; while (p->next != NULL) { p = p->next; } Node *newNode = (Node *)malloc(sizeof(Node)); if (newNode == NULL) return 0; newNode->data = val; newNode->next = NULL; p->next = newNode; return 1; }逻辑说明:insertHead必须传Node **,因为要在函数内部修改头指针自身。C语言的函数参数是值传递,只传Node *的话,函数内部修改head只影响局部副本,外部的头指针不会变化。带头结点的insertTail则只需一级指针,因为头节点不变,只修改链表某个节点的next域。
参数说明:第一个函数传入的是&head,即头指针的地址,这是“不带头结点头插法”的标准写法;第二个函数传入head本体即可。两个函数都做了malloc失败的判断,这是教材伪代码里没有、但实际工程必需的部分。
3.2 栈的数组实现和循环队列的数组实现:容量和指针回绕
教材里栈和队列各给了两种实现,考试常考哪个?顺序栈和循环队列的概率最大。循环队列的“牺牲一个存储单元”判断队满的方式是经典考点。
#include <stdio.h> #define MAX_SIZE 100 // ---- 顺序栈 ---- typedef struct { int data[MAX_SIZE]; int top; // 栈顶指针,空栈时 top = -1 } SeqStack; int push(SeqStack *s, int x) { if (s->top == MAX_SIZE - 1) return 0; // 栈满 s->data[++s->top] = x; return 1; } int pop(SeqStack *s, int *x) { if (s->top == -1) return 0; // 栈空 *x = s->data[s->top--]; return 1; } // ---- 循环队列:牺牲一个空间判断队满 ---- typedef struct { int data[MAX_SIZE]; int front; // 队头指针,指向队头元素 int rear; // 队尾指针,指向队尾元素的下一个位置 } CirQueue; int enQueue(CirQueue *q, int x) { if ((q->rear + 1) % MAX_SIZE == q->front) return 0; // 队满 q->data[q->rear] = x; q->rear = (q->rear + 1) % MAX_SIZE; return 1; } int deQueue(CirQueue *q, int *x) { if (q->front == q->rear) return 0; // 队空 *x = q->data[q->front]; q->front = (q->front + 1) % MAX_SIZE; return 1; }逻辑说明:出栈用*x带出元素值,这是C语言“通过指针返回值”的惯用写法,也可以直接返回元素,但那样栈空时没有合适的错误表示。循环队列的% MAX_SIZE是回绕关键,front和rear都是数组下标,回绕后仍落在[0, MAX_SIZE-1]范围内。
参数说明:循环队列“队满”的条件写成(rear+1) % MAX_SIZE == front,这意味着数组最多存MAX_SIZE - 1个元素。如果不想浪费这个空间,可以用加一个size计数器或tag标志位的方式,教材在课后题里也提了这两种方案。408真题对这里的考法通常是让考生判断某个状态下队列中元素个数,公式是(rear - front + MAX_SIZE) % MAX_SIZE。
3.3 二叉树递归遍历和快速排序:两段考研代码题的必背模板
二叉树遍历是递归的典型应用,快速排序则是排序章节里手写概率最高的算法。这两个模板我建议直接背到条件反射的程度。
#include <stdio.h> #include <stdlib.h> // ---- 二叉树节点定义 ---- typedef struct BiNode { int data; struct BiNode *lchild; struct BiNode *rchild; } BiNode; // 前序遍历:根 -> 左 -> 右 void preOrder(BiNode *root) { if (root == NULL) return; printf("%d ", root->data); preOrder(root->lchild); preOrder(root->rchild); } // 中序遍历:左 -> 根 -> 右 void inOrder(BiNode *root) { if (root == NULL) return; inOrder(root->lchild); printf("%d ", root->data); inOrder(root->rchild); } // ---- 快速排序 ---- int partition(int arr[], int low, int high) { int pivot = arr[low]; // 取第一个元素为枢轴 while (low < high) { while (low < high && arr[high] >= pivot) high--; arr[low] = arr[high]; while (low < high && arr[low] <= pivot) low++; arr[high] = arr[low]; } arr[low] = pivot; return low; } void quickSort(int arr[], int low, int high) { if (low < high) { int pivotPos = partition(arr, low, high); quickSort(arr, low, pivotPos - 1); quickSort(arr, pivotPos + 1, high); } }逻辑说明:二叉树三种遍历的差别只在于printf的位置,前序在递归左子树前输出,中序在递归左右之间输出,后序在两次递归之后输出。写错访问顺序是初学者最常见的失误。快速排序的partition采用“挖坑填数”策略,pivot先被保存,右侧小于pivot的元素填入左侧空位,左侧大于pivot的元素填入右侧空位,最后把pivot放回low==high的位置。
参数说明:快排的递归边界是low < high,否则当数组只有一个元素时也会进入递归导致死循环。partition里的两个内层while都加了low < high条件,防止high减到low以下发生数组越界。这些边界在考试手写代码时最容易丢分,也是面试官最常追问的位置。
4. 学这本教材最常见的踩坑:指针传参、野指针与递归边界
严蔚敏教材的代码很多是“教学版”,省略了工程实现里的防御性写法。照着教材抄代码能看懂,但改成实际可运行的程序时会遇到一堆问题。下面几条都是我见过别人、也自己踩过的坑。
4.1 指针传参之后修改不生效:C语言函数参数永远是值传递
现象:写链表头插法时只传Node *head,函数内部执行了head = newNode,返回主调函数后打印头节点发现还是NULL。
原因:C语言传参是把实参的值复制一份给形参,函数内部的head是外部head的副本。修改副本不会影响外部变量,这是一个极其隐蔽的玄学点。教材里用类似C的伪代码表达算法,没有强调传二级指针的细节,照抄时很容易掉坑。
解决:需要修改头指针本身时用二级指针,调用处写成insertHead(&head, x)。如果坚持用一级指针,那就借助带头结点的结构,让头结点固定不变,只修改后续节点。优先级上,带头结点的写法更省心,不带头结点的写法在考概念时更常出现。
4.2 malloc之后没有初始化成员:野指针的翻车现场
现象:Node *p = (Node *)malloc(sizeof(Node));后直接p->next = q,运行到打印链表时出现段错误,地址随机跳。
原因:malloc只分配内存,不会清零内存内容。p->next里存的是这块内存原有的随机残留值,把它当成有效地址访问就会段错误。教材在创建链表的伪代码里画了节点示意图,但示意图默认空白等于NULL,实际C语言里不是这样。
解决:malloc之后立刻给所有成员赋值,尤其是指针成员初始化为NULL。快排算法里数组不需要初始化,但链表和树这类结构只要用malloc就必须立刻初始化。还有一个习惯:每次free之后把指针置NULL,避免悬空指针被二次访问。
4.3 递归出口条件不完整导致栈溢出
现象:写二叉树求高度的递归函数,小树计算结果正常,深度上百的树直接崩溃,报错栈溢出。
原因:递归函数少写了root == NULL出口,只在root->lchild == NULL && root->rchild == NULL时返回,导致空节点继续访问孩子节点,沿空指针反复递归。
解决:树相关的递归函数无论遍历还是计算高度,第一步都先写if (root == NULL) return;(或返回0)。这个出口是递归终止的保底条件,没有它,空子树会无限递归下去。另一个常见类似问题是求叶子节点数时先判断非空再左右递归,漏掉根节点单独存在的情况,结果永远是0。
4.4 链表反转的三指针更新顺序错一拍
现象:迭代反转单链表,写出来死循环,或者反转后链表断了,中间节点丢失。
原因:反转需要三个指针pre、cur、next同时协作,标准顺序是先保存next = cur->next,再执行cur->next = pre,最后pre = cur; cur = next;。最常见的错误是先改cur->next再保存next,结果原链表后半段丢失。
解决:反转操作里“先保存后继”这个顺序没得商量。我习惯在代码里注释标注这三行的顺序,考试手写时也按这个顺序核对一遍。教材给出的递归反转版本更短,但理解成本更高,工程上迭代版本更稳。
4.5 排序算法稳定性概念混淆
现象:快速排序和堆排序选出一个pivot时,相同元素的相对位置变了,题目问哪种排序稳定,答案填错。
原因:稳定性指排序后相等关键字的相对顺序不变。冒泡、插入、归并、基数排序稳定,快排、选择、希尔、堆排序不稳定。很多人把“时间复杂度O(nlogn)”和“稳定”混为一谈,以为快的就稳定。
解决:把稳定性归因到具体操作——比较时是否跨越多个元素交换。快排的partition跨越式移动元素,堆排序交换父子和末尾元素,这些操作都会破坏相对顺序。考试里最常考的两个反例是快排和堆排序。面试问“如何让不稳定的排序变稳定”时,思路是给每个元素加序号做比较键,但空间和比较成本会上升。
5. 从教材到期末和408考研:高频考点和代码题的转化路径
教材的价值一半在算法本身,一半在考试映射。期末试卷和408真题的出题风格有明显差异,需要两条腿走路。期末偏重概念记忆和简单推导,408偏重综合应用和代码复杂度判断。
5.1 数据结构期末复习:高频考点与教材章节对照表
期末复习最忌按教材从头到尾平铺,我用一张表把高频考点和教材章节对应起来,复习时按优先级往下打。
| 考点 | 教材章节 | 常见题型 | 复习优先级 |
|---|---|---|---|
| 顺序表与链表插入删除 | 线性表 | 代码填空、算法设计 | 高 |
| 栈与队列特性 | 栈和队列 | 选择、判断 | 高 |
| 二叉树遍历与线索化 | 树和二叉树 | 画图、代码题 | 高 |
| 图的DFS/BFS | 图 | 应用题、遍历序列 | 高 |
| 最小生成树与最短路径 | 图 | 手算、画图 | 中高 |
| 查找算法与ASL计算 | 查找 | 计算题 | 高 |
| 各种排序性质对比 | 内部排序 | 选择、代码题 | 高 |
优先级标注的依据是出现频率,不代表低优先级不考。ASL(平均查找长度)计算是期末的固定考点,通常配一道关于二分查找或哈希表的计算题,这类题考的是细心程度,背过公式后反复练习即可。
5.2 408考研代码题:教材模板和王道参考书的配合使用
408统考的代码题不会直接照搬教材源码,而是把教材的算法思路放在新的应用场景里。比如“找出单链表倒数第k个节点”这类题,教材里没有现成代码,但用快慢指针就能在O(n)时间内解决,快慢指针的思维源头还是链表的遍历那几页。王道408参考书的价值在于把教材知识点重新按考纲整理,并标注了历年真题的考查次数。
我备考时的方法是一章教材对应一章王道,先看教材理解原理,再刷王道题检验输出,最后回到教材把错误对应的知识点重读一遍。这个闭环不需要额外整理笔记,因为王道书边上就能写要点。408的代码题阅卷看关键逻辑,不要求语法完全精确,但核心的指针操作和循环边界必须清楚。
5.3 考前一周的代码题突击方法
考前一周再系统过所有章节来不及,我把代码题分成三类突击:线性表操作、树的遍历、排序算法。每天各写一遍,写完后对照教材检查是否有遗漏pivot赋值、是否漏判空指针。线性表重点关注插入删除和反转,树重点关注三种递归遍历的转换,排序重点关注快速排序和堆排序的手写,堆排序建议用教材的调整堆函数模板而不是自己发明写法。
这几段代码里最容易在考场紧张时写错的是快排的partition循环条件,考前一晚把这段单独抄一遍,比看十页笔记有用。
6. 读完教材后拿什么验证自己会了:三个自测方向
检验是否读懂教材,最好的方式不是再看一遍目录,而是合上书做三个方向的验证。
方向一是把教材里的典型结构重构为完整可运行的程序。链表、栈、队列、二叉树、图各写一遍,编译运行通过以后再随机传入几个边界输入,比如空链表、空树、长度超过数组容量的数组。边界输入能跑通才算真的掌握,写一次就过但输入空表就崩,说明还有条件判断没到位。
方向二是做一个稍微复杂的小项目,把零散的知识点串起来。常见做法是表达式求值计算器,核心是栈和运算符优先级的配合;哈夫曼编码器则覆盖了树、优先队列和编码表的全套流程。挑其中一个做就行,做的时候强制要求自己不能看教材代码,忘记了就回想算法思路,用自然语言描述思路后再自己写代码。只抄不想的学习效率极低,抄的时候觉得都懂了,合上书就忘。
方向三是算法对比实验。教材对每个排序算法都给了时间复杂度的结论,这些结论可以用几组不同规模的随机数验证。跑一遍冒泡、快排、归并对同一组数的排序时间,输出的差异会让这些结论真实落脚。我做过一次对10000个随机数的排序对比,冒泡排序耗时是快排的几十倍,从此再没忘过快排为什么重要。
做完这三个验证,数据结构这科基本能应付课程、期末和考研三个场景。我自己带过的学生里,坚持把方向二做完的人后续学算法设计和操作系统时整体顺很多,数据结构就是底层的地基。最后说一句我自己的习惯——每次拿到一本技术书先不读正文,先花半小时看目录把它拆成自己想问的问题列表,带着问题去读,比从头翻到尾有效得多。希望这些方法对你有帮助。
本文还有配套的精品资源,点击获取