如果你正准备考研,或者正在期末复习数据结构,大概率对“436”这个数字不会陌生——它常被用作学校自命题科目的代码。我当年备考的时候,最困惑的就是:视频课看一遍明明都懂,一到动手做题就发现哪里都是漏洞。后来我才明白,数据结构这门课,真正重要的不是“看懂了”,而是“能不能自己写出来”。
这篇文章把这门课从知识框架、代码训练、复习策略到常见陷阱做一次完整梳理。适合三类人:一是考研党,二是期末冲刺的本科生,三是想系统补数据结构基础的在职开发者。内容会贴着我实际备考和辅导别人的经验来写,尽量把那些容易忽略但考试会考的点讲透。
1. 436这门课到底在考什么:先画清楚知识地图
1.1 五大知识模块的底层关系
很多同学一上来就埋头背定义、背代码,结果学完一遍发现脑子里全是散的点。数据结构这门课其实有一条非常清晰的递进主线:它研究的是“数据元素之间的关系”,而这个关系从简单到复杂分成了几个层次。
最基础的是线性结构,对应的就是线性表、栈、队列。线性表中的元素是一对一的关系,就像排队买奶茶,每个人只有一个前驱和一个后继。栈和队列实际上是受限的线性表——栈只能在栈顶操作,队列只能在一端入队、另一端出队。这种限制并不是缺点,反而让它们能在特定场景下发挥奇效,比如函数调用用栈、消息排队用队列。
再往上走是树,元素之间是一对多的层次关系,就像公司的组织架构,一个上级可以管多个下级,但每个下级只有一个直接上级。树的关键是“分层”和“路径”,二叉树的遍历、二叉排序树、堆,全部围绕这个核心展开。
然后是图,这是最复杂的结构,元素之间是多对多的任意关系,就像社交网络,谁跟谁都有可能产生连接。图的最短路径、最小生成树,都是在解决“在复杂关系中找最优解”的问题。
最后的查找和排序不是独立结构,而是贯穿前面所有结构的典型应用。你可以在顺序表上做二分查找,在二叉排序树上做动态查找,在堆上做排序。它们把前面学的结构真正用了起来,所以复习顺序上一定是“先线性、再树、再图,最后看查找排序”,跳着学容易卡壳。
1.2 各模块高频考点清单
我把这门课常考的内容按模块整理成一个清单,平时复习可以照着它自查:
| 模块 | 核心考点 | 代码要求 | 常考题型 |
|---|---|---|---|
| 线性表 | 顺序表与链表的插入、删除、反转、合并 | 手写链表操作 | 算法设计题、代码填空 |
| 栈与队列 | 进出栈序列判断、循环队列判空判满、表达式求值 | 栈的数组/链表实现、队列实现 | 选择题、应用题 |
| 树与二叉树 | 二叉树性质、先中后层序遍历、线索二叉树、哈夫曼树、二叉排序树、平衡二叉树 | 遍历递归与非递归实现 | 选择题、简答题、算法题 |
| 图 | 邻接矩阵/邻接表、DFS/BFS、最小生成树、最短路径、拓扑排序、关键路径 | 图的遍历框架 | 应用题、综合题 |
| 查找 | 二分查找、二叉排序树、AVL、B树、哈希表 | 二分、BST插入删除 | 选择题、计算题 |
| 排序 | 八大排序算法的过程、时间复杂度、稳定性 | 快排、归并、堆排 | 选择题、应用题 |
注意观察,考来考去其实就这几板斧。很多同学复习到后面慌,是因为总想什么细节都抓住,结果重点反而没吃透。上面这个清单里的内容,每一项都值得你花时间真正写一遍代码,而不是只看不练。
1.3 统考与自命题的复习思路差异
“436”通常是学校自命题科目的代码,和408统考的复习逻辑有区别。统考数据结构部分的知识点覆盖面广,但每题分值有限,更偏重基础理解和简单应用。而自命题风格的学校,往往会在算法设计题里要求你手写完整代码,分值占比也更高。
举个例子,有些学校的436真题会直接给你一道“将两个有序链表合并成一个有序链表”的题目,要求写出可运行的完整函数,甚至要求给出结构体定义。这种题目在统考里一般是选择题或简答题,不会让你写大段代码。所以你如果考的是自命题436,一定要找到目标院校的历年真题,仔细研究它的命题风格和代码题占比,而不是只按统考的套路来复习。资料上可以以王道为基础,但冲刺阶段必须以真题为核心。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手撕代码是最硬的门槛:把这三类模板练到肌肉记忆
2.1 链表:哨兵节点和指针操作是命门
链表代码题是数据结构考试里出现频率最高的题型,没有之一。原因很简单:它考的是指针操作和边界条件,这两个东西恰恰是区分“看懂”和“会写”的分水岭。
先看最基础的单链表插入操作。很多新手写的时候容易忘掉关键顺序,代码大概是这样的:
c复制typedef struct LNode {
int data;
struct LNode* next;
} LNode;
// 在带头结点的单链表第 pos 个位置插入值为 val 的新节点
int ListInsert(LNode* head, int pos, int val) {
if (pos < 1) return 0;
LNode* p = head;
int i = 0;
while (p != NULL && i < pos - 1) {
p = p->next;
i++;
}
if (p == NULL) return 0; // 位置不合法
LNode* s = (LNode*)malloc(sizeof(LNode));
s->data = val;
s->next = p->next;
p->next = s;
return 1;
}
这段代码有几个关键点必须想明白:第一,为什么要先执行 s->next = p->next,再执行 p->next = s?因为如果先修改 p->next,原来后面的节点就找不到了,链表会断掉。这个顺序错误是链表题里最常见的翻车点。第二,为什么插入位置从 pos < 1 开始判断?因为带头结点的链表,pos = 0 代表插入到头结点后面,有些教材的定义不同,考试时一定要先看清题目约定的下标规则。
链表里另一个高频技巧是快慢指针。找链表中间节点、判断链表是否有环、找倒数第k个节点,全部可以用快慢指针解决。比如找中间节点,慢指针每次走一步,快指针每次走两步,快指针到末尾时慢指针刚好在中间。这个技巧在复试面试里也经常被问到,值得练熟。
2.2 二叉树遍历:递归、迭代、层序三种方式一次吃透
二叉树的遍历是所有树相关算法的基础。你要是能把先序、中序、后序、层序这四种遍历写得滚瓜烂熟,后面几乎所有树题目都能往这个框架上套。
递归版本是最容易写的,也是一个很好的模板:
c复制void PreOrder(BiTree T) {
if (T == NULL) return; // 基线条件
visit(T); // 访问根节点
PreOrder(T->lchild); // 遍历左子树
PreOrder(T->rchild); // 遍历右子树
}
很多同学递归写得很顺,但一到非递归就卡住,因为不理解递归在底层其实是用“系统调用栈”实现的。迭代版本无非是用一个显式的栈来模拟这个过程。中序非递归遍历是考试里的高频考点,写法是:沿着左子树一直入栈,直到为空,然后出栈访问节点,再转向右子树。这个逻辑如果能亲手在纸上画一遍,比背十遍代码都管用。
层序遍历用的是队列,思路更直观:根节点入队,出队时访问它,然后把它的左孩子和右孩子依次入队,循环到队列为空。这种写法不仅在遍历里有用,在求二叉树高度、判断完全二叉树、输出每层节点等题目里都能套。可以说,树相关的算法题,八成都离不开这套基础框架。
2.3 排序:理解过程比背代码更重要
排序算法是选择题和应用题的“题库大户”,尤其是时间复杂度和稳定性,几乎每年必考。先把核心结论用一张表记住:
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 直接插入排序 | O(n²) | O(n²) | O(1) | 稳定 |
| 简单选择排序 | O(n²) | O(n²) | O(1) | 不稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
这张表光背不够,你得清楚“为什么”。比如快排为什么不稳定?因为 partition 过程中会跨越位置交换元素,相同值的相对顺序可能被改变。选择排序为什么不稳定?因为每次选择最小值和前面的元素交换,也可能改变相同元素的相对顺序。考试如果只问“稳定的排序有哪些”,答上表就行;如果问“为什么”,你就要会解释。
快排的核心代码在 partition 这一步,写法很多,我习惯用挖坑法,边界相对不容易错:
c复制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;
}
是不是有点难记?我教你一个口诀:“右找小,填左边;左找大,填右边”。反复执行到 low 和 high 相遇,再把基准放回去。整个快排的过程就像“把基准值送到它最终应该在的位置”,理解了这个思想,代码写不出来也能用口述把步骤讲清楚,而口述步骤在简答题里就能拿分。
3. 从“看懂”到“考场写对”:代码训练的具体方法
3.1 纸笔模拟:最被低估的训练方式
我见过太多同学,LeetCode 刷了几百题,但遇到学校的算法设计题还是写不对。原因在于:考场上是纸上写代码,没有编译器帮你检查,也没有报错提示。你平时依赖 IDE 的自动补全和实时调试,到考场全没了。
所以我强烈建议你练纸笔模拟。具体操作很简单:拿一张白纸,画一个链表或一棵二叉树,然后在旁边一步步写出算法执行的中间状态。比如快排,你在纸上写一个数组 [49, 38, 65, 97, 76, 13, 27],然后手动模拟每一趟 partition 的过程,看看基准值是怎么一步步归位的。这个过程看着慢,但对理解算法的帮助是视频课无法比的。
刚开始你会觉得效率很低,但坚持两周就会发现,写代码时对边界条件的敏感度明显提升。尤其是递归,在纸上展开一次递归调用链,比在脑子里想十遍都清楚。
3.2 刷题顺序与题量建议
如果你是考研复习,我不建议一上来就刷 LeetCode 困难题。考研的数据结构代码题,难度整体低于 LeetCode 中等题,更偏基础实现。建议按这个顺序刷:
- 顺序表和链表的插入、删除、反转、合并
- 栈与队列的数组实现和链表实现
- 二叉树的四种遍历
- 二叉排序树的查找、插入、删除
- 图的基础遍历 DFS 和 BFS
- 快排、归并、堆排的手写实现
每种类型保持在 10 道题左右的训练量就够了。这个量对着考研真题来说完全足够,不需要追求题海战术。关键是每一道题都要做到“能独立手写出来”,而不是“看答案觉得懂了”。如果一道题你看了答案后能默写出来,那才算真正掌握。
3.3 考场上的代码书写规范
考场写代码和平时刷题不一样,有几个细节能直接影响得分:
第一,自定义类型的名字要写规范。很多判卷老师看代码时首先看结构体定义是否清晰,typedef struct LNode {...} LNode 这种写法是C语言版教材的标准风格,尽量用统一的命名。
第二,函数接口要明确。比如写“在带头结点的单链表中删除值为x的所有节点”,你应该先写出函数签名 void DelAllX(LNode* head, int x),再写函数体。函数签名本身就是解题思路的体现,写上就能让老师知道你准备怎么组织代码。
第三,边界判断写在最前面。空链表、空树、pos 越界、查找失败,这些情况在函数开头先处理掉,后面代码就可以专注正常逻辑。这不仅让代码更完整,也能避免写到最后忘了处理边界。
第四,不要追求奇技淫巧。考场上用最基本的方法能写对,比用高级技巧但写错强得多。同样是求二叉树高度,递归遍历的方法简单直观,非要写非递归反而容易出 bug。
4. 考研、期末、自学三种目标下的复习路径
4.1 考研党:以真题为中心的复习节奏
考研数据结构的复习,时间节奏和资料选择都很关键。市面上的主流资料有王道和天勤,两套内容大同小异,选一套吃透即可,不要贪多。我的建议是以王道为基础,因为它的知识点归纳比较清晰,习题也贴合考研风格。
时间上可以分成三个阶段。基础阶段(建议4-6周):配合视频课过一遍王道知识点,每章结束做对应的选择题,重点是把知识框架搭起来。强化阶段(建议3-5周):开始做王道的大题和算法设计题,同时找到目标院校的436真题,分析出题风格。如果学校历年真题里代码题占比高,就多花时间在链表、树、排序的手写练习上。冲刺阶段(考前4-6周):以真题套卷为主,计时模拟,重点复习错题和常量公式。
特别提醒一点:王道课后习题的代码题质量很高,但有些题偏难。如果你报考的学校题目风格基础,不需要死磕难题,把基础题写熟才是性价比最高的做法。
4.2 期末冲刺:抓住得分效率最高的部分
期末复习时间紧,不可能面面俱到,必须要讲究优先级。根据题型来倒推复习重点:期末一般是选择、填空、简答、应用题、算法设计题这样的结构。
选择题和填空题考概念,优先复习时间复杂度比较、线性表操作、栈和队列的特点、二叉树性质、图的基本概念、排序的稳定性表。这些内容靠短期记忆就能拿分。应用题通常是“给定一个序列求各种排序每趟的结果”“给一棵树写遍历序列”“给一个图求最小生成树和最短路径”,这些都是有固定步骤的题型,练几道题就能掌握套路。算法设计题则优先准备链表反转、链表合并、二叉树遍历、二分查找、快排这几个经典题目,它们出现频率最高。
期末复习的核心思路是:把能靠套路的分数先拿住,再攻需要深度理解的题目。
4.3 在职补基础:用项目倒逼数据结构理解
如果你不是考研党,而是工作之后想补数据结构基础,我的建议是换个思路——不要从教材第一页开始啃,而是从你实际用到的中间件和框架反向学习。Redis 里有跳跃表,数据库索引用 B+ 树,消息队列的模型就是队列结构,HashMap 的底层是哈希表加链表/红黑树,操作系统的进程调度用到了队列和堆。你在使用这些组件时遇到的问题,本质上就是在和数据结构的底层实现打交道。
比如你发现 Redis 的 ZSET 排序性能很高,就可以去研究为什么用跳跃表而不用红黑树,跳跃表的插入删除为什么是 O(log n)。带着问题去学,比单纯背“跳跃表是什么”要高效得多。这个路径适合已经有工程经验、不需要应试的开发者,既能补基础,又能反哺实际工作。
5. 那些容易丢分的坑:从概念到代码的“反直觉”点
5.1 概念辨析里的高频陷阱
数据结构的概念题看似简单,实际上埋了很多容易踩的坑。我挑几个考试里出现频率高、错误率也高的点说一说。
完全二叉树与满二叉树的区别:满二叉树是所有层的节点都填满,完全二叉树是除最后一层外全满,且最后一层的节点集中在左侧。这两个定义在考试里经常被混淆,尤其是在求节点数和深度的题目里。
二叉树与度为2的有序树的区别:这是个很多同学都会搞混的知识点。度为2的有序树的子树有左右之分吗?有。那它和二叉树的区别在哪?区别有两点:二叉树允许子树为空,而度为2的有序树至少有一个节点;二叉树的孩子节点明确指出左右,而度为2的有序树虽然有序,但它的“有序”只强调相对次序。这个知识点在408和自命题都出现过,值得单独记一下。
拓扑排序的唯一性:拓扑排序序列不一定是唯一的。如果图中存在多个入度为0的节点,选择不同的节点作为起点会得到不同的拓扑序列。只有当图中每一时刻入度为0的节点都唯一时,拓扑排序才唯一。这个条件经常在选择题里以判断题的形式出现。
图遍历生成树:同一个图,从同一个顶点出发做 DFS 和 BFS,生成的生成树不一定相同;从不同顶点出发做同一种遍历,结果也可能不同。这是图的遍历里最容易被忽略的细节。
5.2 代码里最常见的一批错误
在我看过的代码作业里,出现频率最高的错误集中在以下几类:
第一,指针操作顺序颠倒。链表的插入和删除,先改谁的 next 后改谁的 next,这个顺序非常重要。判断标准很简单:任何一步操作之后,链表都不能断成两截。
第二,递归忘记基线条件。写二叉树遍历时忘了判断 T == NULL,或者写递归函数时没有终止条件,结果就是栈溢出。递归的基线条件不是可有可无的细节,它是整个递归能够成立的前提。
第三,数组越界。循环变量从 0 开始还是从 1 开始,边界是 < n 还是 <= n,快排 partition 里 while (low < high && arr[high] >= pivot) 如果少了 low < high,就会下标越界。这些边界条件在写完后一定要自己拿一个例子走一遍。
第四,循环队列的判空判满。循环队列牺牲一个存储单元来区分队空和队满,这是常见做法。判断条件是 front == rear 为队空,(rear + 1) % MaxSize == front 为队满。很多同学在应用题里算队列状态时会漏掉取模运算,导致结果错误。
5.3 复杂度分析里容易翻车的场景
时间复杂度分析看起来简单,实际上有不少反直觉的陷阱。最典型的是“双重循环不一定是 O(n²)”。比如下面这段代码:
c复制for (i = 1; i < n; i *= 2) {
for (j = 0; j < i; j++) {
// 执行某个操作
}
}
外层循环的次数是 O(log n),内层循环的总执行次数是 1 + 2 + 4 + ... + n = 2n - 1,所以整体时间复杂度是 O(n),不是 O(n²)。考试里遇到循环嵌套,不能只凭“看有几层循环”来猜复杂度,必须算出实际执行次数。
再比如递归函数的时间复杂度,不能简单套循环模板。斐波那契数列的递归实现,时间复杂度是 O(2^n),因为它每个节点都会展开两个子调用;而改成迭代实现后就是 O(n)。这种差距在选择题里经常以“下列哪种实现效率更高”的形式出现。
还有哈希表的复杂度:平均情况下查找是 O(1),但如果哈希函数设计不合理,大量元素冲突导致链长很长,最坏情况会退化成 O(n)。考试里一般会问“哈希表在什么情况下会性能退化”,答出冲突严重即可。
6. C、Java、Go的写法差异:不同语言怎么练
6.1 C语言:考研代码题的标准答案风格
C 语言是大多数考研教材的标准语言,严蔚敏版的《数据结构》用的就是类 C 伪代码。考试时用 C 语言写代码,最大的好处是和教材一致,老师判卷时最熟悉。
C 语言实现数据结构的两大核心是结构体和指针。链表节点、二叉树节点,都是用结构体来定义,用指针来连接:
c复制typedef struct BiTNode {
int data;
struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;
C 语言的难点在于内存管理,malloc 之后要记得 free,否则内存泄漏。考试代码一般不强制写 free,但在链表删除操作里,如果你写的是“释放被删除节点的内存”,这通常会成为一个加分点。
6.2 Java:用对象引用代替指针,关注容器类
Java 里没有指针的概念,但对象的引用本质上起到了指针的作用。链表节点的定义方式是:
java复制class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
Java 的 API 里有很多现成的容器类,比如 LinkedList、ArrayList、HashMap、Stack、PriorityQueue。笔试时可以直接调用这些类来简化代码。但要特别注意:很多学校的机试或笔试明确要求不能使用库函数实现核心算法,比如让你自己实现一个堆排序而不是直接调用 PriorityQueue。这种情况下,你还是要回到基础实现,不能偷懒。
如果你用 Java 复试,建议把二叉树节点类、链表节点类和常用的递归遍历模板都提前准备好,机试的时候可以快速调用。
6.3 Go:用切片和结构体,代码更简洁
Go 语言这几年在互联网公司用得越来越多,如果你考的是非传统的计算机专业,或者复试时允许自由选择语言,Go 也是一个不错的选择。Go 的切片(slice)比数组灵活得多,写顺序表和栈队列非常方便:
go复制type ListNode struct {
Val int
Next *ListNode
}
用 Go 写链表插入,思路和 C 语言一样,但不需要手动管理内存。Go 的 container/list 包提供了标准库的双向链表,但考试时建议还是自己实现节点结构体,方便展示你对数据结构的理解。
从我的经验来看,考试用什么语言不是最重要的,关键是你熟悉这套语言的写法和边界。用不熟悉的语言写代码,平时很容易写出来的小细节,考场上一紧张就会出错。
7. 最后分享几个我自己用的实操技巧
这篇文章写到最后,我想把几个我备考和后来辅导别人时反复验证过的小技巧分享出来,它们不一定写在任何教材里,但对提高做题能力很有帮助。
第一个技巧是错题本只记“卡壳原因”,不抄题。很多同学的错题本最后变成了抄题本,厚厚一本但复习时根本看不完。我记错题只写三行:这个知识点是什么、我当时错在哪一步、现在总结出来的正确思路是什么。复习的时候只看这几行,效率非常高。
第二个技巧是每天保持手写一段代码。不用多,20行左右就行。可以是链表插入,可以是二叉树遍历,可以是快排。关键在于每天都动笔写,保持手感和对边界条件的敏感。这个习惯坚持一个月,你会发现考场上写代码的速度和准确率都有明显提升。
第三个技巧是模拟考场环境。考前两周,每周抽一个固定时间,找一套真题,在规定时间内完整做一遍。包括大题和代码题,都要像真实考试一样动笔写,不能只看不做。这个训练能帮你发现很多问题——不是知识不会,而是时间分配不合理、答题节奏不对。
第四个技巧是睡前复盘。每天晚上睡前,在脑子里过一遍“今天写的算法是什么、核心步骤有哪些、有什么容易错的边界”。这个习惯特别适合数据结构这种需要记忆算法过程的科目,等于在无意识状态下又复习了一遍。
数据结构的学习没有捷径,但不代表要盲目堆时间。抓住线性表、树、排序这些核心模块,把代码模板练成肌肉记忆,把容易丢分的边界条件刻在脑子里,这门课就稳了。希望这篇梳理能帮你少走一些弯路。
