数据结构,学完就忘?这份知识点总结帮你把线串起来
大学里数据结构挂科率常年居高不下,考研复习时看着树、图、排序算法一头雾水,面试前又要临时抱佛脚看八股文——这些都是我经历过的事。数据结构这门课最大的问题不是难,而是知识点太散,教材一页一页翻完,脑子里剩下的是“什么是二叉树”和“快速排序好像很快”这种模糊印象,真做题、上机的时候全乱套。
这篇文章就做一件事:把数据结构复习的核心考点整体梳理一遍,按照“逻辑结构—物理结构—线性结构—树—图—查找—排序”的顺序,把每个模块里最容易被考到、最容易踩坑的地方挑出来,配上具体的例子和实操心得。不敢说你读完不用刷题,但至少能让你知道“该复习什么”“怎么理解和记忆”“哪些细节是考官和面试官爱挖的坑”。适合期末复习、考研冲刺、软考备考,也适合已经工作但想快速捡起数据结构底子的人。
1. 数据结构到底是什么?先把底层规则搞清楚
1.1 四个逻辑结构和两个物理结构,别搞混
数据结构的第一节课通常都在讲“逻辑结构”和“物理结构”。很多初学者觉得这就是概念,背下来就行,但没过两天就分不清“线性表”和“栈”到底算哪一类,一写代码就乱。
逻辑结构描述的是数据元素之间的逻辑关系,脱离存储方式独立存在。考研和期末最爱考的四种逻辑结构是:集合结构、线性结构、树形结构、图状结构。互相之间的区别不复杂——集合结构里元素之间“没有关系”,只是同属于一个集合;线性结构是一对一的线性关系,像排队买饭;树形结构是一对多的层级关系,像公司组织架构;图状结构是多对多的关系,像地铁线路网。
物理结构或者说存储结构,关心的是这些逻辑关系怎么在内存里落地。主流就两种:顺序存储和链式存储。顺序存储用连续的地址空间挨个放,数组是典型;链式存储用指针把不连续的节点串起来,单向链表是典型。还有一种索引存储和散列存储的说法,但在面试和考试里出现频率最高的还是前两种。
这里要提醒一句:逻辑结构和物理结构不是一一对应的。线性表可以用数组实现(顺序表),也可以用链表实现。树既可以用数组按下标关系存,也可以用孩子兄弟表示法之类的链式方式存。想清楚这一点,后面学树和图会顺畅很多。
1.2 为什么算法分析总说时间复杂度和空间复杂度
数据结构和算法分析经常一起出现在教材前几章,很多人觉得大O记号很难,其实它可以理解成“当数据量变大,程序耗时或者占内存的增长趋势”。O(1)意思是无论数据多少,耗时基本不变;O(n)意思是数据翻一倍,耗时也大约翻一倍;O(n²)意思是数据翻一倍,耗时可能翻四倍。
期末考试爱考怎么求时间复杂度,面试爱问“这段代码复杂度是多少”。基本功是先会数循环的嵌套层数和执行次数。比如双重循环嵌套在n规模下基本是O(n²),递归算法则要看递归调用了多少次、每层做了什么。
不过复杂度分析里面也藏着不少坑,后面在排序和查找章节再结合具体算法展开理解会更深刻。
1.3 复习顺序怎么安排比较顺
按我个人的经验,最顺的复习路线是这样的:
第一步,先吃透线性表、栈、队列这种线性结构,这是后续所有结构的基础。第二步,学串和数组,相对独立但考试偶尔会考KMP、稀疏矩阵。第三步,树和二叉树,重点在于遍历、二叉树性质、二叉搜索树和堆。第四步,图,重点在于存储、遍历和最短路/最小生成树。第五步,再回头系统刷查找和排序,因为这两个模块的知识需要前面所有基础。
建议有一份自己整理的思维导图,或者直接照着目录把所有算法写成单页卡片。数据结构这门课最忌“只看不练”,代码不写一遍、题不刷一遍,光靠眼睛看是绝对记不住的。
2. 线性表、栈、队列、串:基础中的基础最容易忽略细节
2.1 顺序表和链表,本质上是“空间换时间”还是“时间换空间”
线性表是n个数据元素的有限序列,常见实现方式就两种:顺序表和链表。顺序表说白了就是动态数组,底层是一片连续内存,支持随机访问,可以通过下标O(1)找到第i个元素,但插入和删除要搬动后面的元素,平均O(n)。链表则是节点不连续,用next指针串起来,插入和删除只需要修改指针,但查找第i个元素必须从头遍历,O(n)。
面试官最喜欢问的就是“什么场景选顺序表,什么场景选链表”。我一般这样答:如果你经常按下标查元素、且插入删除集中在尾部,用顺序表;如果你有大量中间插入删除、且不确定总长度,用链表更灵活。实际项目中,顺序表仍然是主角,因为CPU缓存对连续内存友好,链表在高性能场景里碰到的缓存未命中问题比你想象中严重。
顺序表还有个隐藏考点是动态扩容。懂的人都清楚,当数组装不下了,一般会重新申请一块大小为原来两倍的内存并拷贝过去,均摊下来插入操作还是O(1),这就是“均摊复杂度”思想。这块考研、面试都容易当成延伸题来问。
2.2 栈和队列:操作受限的线性表
栈和队列在线性表基础上加了限制:栈只能从一端(栈顶)插入和删除,先进后出;队列一端进另一端出,先进先出。这个限制不是多此一举,而是让操作模型更清晰,方便解决特定问题。
栈的经典应用:函数调用栈、括号匹配、表达式求值、撤销操作、深度优先遍历。函数调用本身就是天然的栈结构,理解递归时脑子里始终要有一张“调用栈”的图,否则很容易晕。另一个高频场景是“用栈实现队列”和“用队列实现栈”,这类题是很多大厂的一面手写题,核心就靠两个栈倒腾和两个队列倒腾。
关于栈要特别注意一个实现细节:顺序栈里栈顶指针指向的是栈顶元素还是栈顶元素的下一个位置,不同教材不一样。严蔚敏那本C语言版习惯让top指向栈顶元素的上一个位置,有些教材直接让top指向栈顶元素。如果考试要写代码,先看清教材约定,否则压栈出栈时容易矮一头。
队列的考点主要围绕“循环队列”展开。为了区分队空和队满,常见做法是牺牲一个存储单元:队空条件是rear == front,队满条件是(rear + 1) % maxSize == front。判断队列长度是(rear - front + maxSize) % maxSize。很多同学连取模都写不对,建议多用小例子推演几遍,比如maxSize=5时入队4个元素后front和rear分别在哪。
2.3 串的模式匹配:KMP算法到底在优化什么
字符串可以看成一种特殊的线性表,它的数据元素是单个字符。期末考试和面试里关于串,几乎必考KMP算法。很多人背next数组背得痛苦,其实可以反过来理解:KMP优化的点在于当匹配失败时,不用把模式串的指针退回开头重新匹配,而是根据“模式串自身的前后缀公共部分”决定跳到哪个位置。
计算next数组的核心就是找模式串每个前缀里“最长相等前后缀的长度”。例如模式串“ABABC”,当匹配到C失败时,前面已经匹配了“ABAB”,最长相等前后缀是“AB”,所以模式串可以跳到下标2的位置继续比较,而不是回到0。理解这一点之后,next数组就不是靠背,而是能手推出来,笔试遇到KMP填充next数组也能稳拿分。
不考代码的考试里通常会给一个串让你手算next或者nextval,这种题关键是多练。注意不少教材对next数组的定义是“前一位失配后跳转的值”,还有教材规定next[1]=0,所以不同参考书的答案会差一位。看题时先确认教材定义再算,不然对答案永远对不上。
3. 树与二叉树:递归思维的训练营
3.1 二叉树遍历:递归、迭代、层序都要会
二叉树遍历是树这一章的地基。前序(根左右)、中序(左根右)、后序(左右根)、层序(按层从左到右)四种方式,分别对应了递归、栈和队列的经典用法。
递归写法非常简单,核心三行代码调换顺序就能得到前中后序。可面试里不会只让你写递归,一定会追问“用迭代实现中序遍历”。迭代中序遍历需要借助栈,思路是:从根节点出发,先把左子树一路压栈,过程中不断往左走到空,然后弹出节点访问它,再切到右子树继续同样的流程。这个流程我在面试时写过不下五次,熟练度非常重要。
层序遍历用队列实现,属于广度优先搜索在二叉树上的应用。框架很固定:根节点入队,循环里取队首、访问、把左右孩子入队,直到队列为空。基于层序遍历还可以延伸出求树高度、判断是否完全二叉树、打印之字形遍历等上层考题。
有个高频判断题必须提醒:已知二叉树的前序遍历序列和中序遍历序列,可以唯一确定一颗二叉树;已知后序和中序也可以;但已知前序和后序不能唯一确定。原因在于,只有中序能清楚区分左子树和右子树的分界点。
3.2 二叉搜索树、平衡树、二叉堆:从概念到应用
二叉搜索树(BST)的规则是左子树所有节点都小于根节点,右子树所有节点都大于根节点。这个规则让查找、插入、删除平均复杂度变成O(log n),但最坏情况退化成链表时就是O(n),因为如果插入的数据已经有序,树就变成一根斜线了。
为了解决“退化成链表”的问题才引入平衡因子概念。平衡二叉树(AVL树)要求每个节点左右子树高度差绝对值不超过1,每次插入删除后通过旋转来恢复平衡。AVL旋转有四种标准形态:LL、RR、LR、RL,很多考研题会给你一个插入序列让你画出最终AVL树,这种题必须亲自画几遍,不然考试时容易绕晕。
红黑树是面试常客,它不是绝对平衡,但通过颜色约束和局部调整保证最长路径不超过最短路径的两倍,性能稳定且插入删除时旋转次数更少。Java的TreeMap、C++的std::map底层都常用红黑树,只是部分教材不深入讲,工作后要是想做底层开发还是要补上。
二叉堆则是“用数组表示的完全二叉树”,大根堆的堆顶是最大值,小根堆的堆顶是最小值。它最重要的应用是堆排序和优先队列。堆的插入是上浮操作,删除堆顶是下沉操作,这两个操作的代码逻辑不复杂,但边界条件特别容易写错,建议自己完整实现一遍,看看上浮时父节点下标和当前下标的关系到底怎么算(通常父节点是(i-1)/2,左右孩子是2i+1和2i+2,从0开始计数时)。
3.3 树、森林和二叉树相互转换
考研里还有一个让人头疼的知识点:树转化为二叉树、森林转化为二叉树。核心口诀是“左孩子右兄弟”——把每个节点的第一个孩子作为左孩子,把它的下一个兄弟作为右孩子。转换后,任何一棵树都能表示成一颗没有右子树的二叉树。
反过来,二叉树转化为树或森林也依赖这条线索。只要看到二叉树中某个节点只有左孩子没有右孩子,就要意识到这可能是在表达原树中的兄弟关系。期末考试如果出这类转换题,动手画一遍比背十遍文字都管用。
4. 图结构:从建模到路径搜索,难点集中在思维转换
4.1 图的存储:邻接矩阵、邻接表怎么选
图按边有没有方向分为有向图和无向图,按边上是否带权分为带权图和不带权图。存储方式最常考的就是两种:邻接矩阵和邻接表。
邻接矩阵用二维数组存边关系,优点是判断两个顶点之间是否连通是O(1),缺点是不论实际边多不多都要占n²的空间,适合稠密图。邻接表则是每个顶点维护一条链表,存它能到达的邻居,优点是空间上更省,适合稀疏图,缺点是判断两点是否相连需要遍历链表。
面试里有个嵌入式问题我也遇到过:如果要实现一个社交好友推荐系统,是选邻接矩阵还是邻接表?这类问题没有标准答案,但你要能说清楚各自复杂度以及为什么在“好友关系稀疏”的场景下邻接表往往更合理。图的存储结构理解到位,后面的遍历和算法才能在脑子里形成画面。
4.2 DFS和BFS:不只是遍历,是搜索思想的原型
深度优先搜索(DFS)和广度优先搜索(BFS)是图论算法的基础。DFS用栈或者递归实现,从起点一直往深处走,走不通再回头,适合找连通分量、判断是否有环、拓扑排序、回溯类问题。BFS用队列实现,从起点逐层扩散,天然适合求无权图的最短路径(按层数走,第一次到达终点时的步数一定最短)。
BFS的模板代码其实很固定,我在面试中写得最多的事先准备好队列和visited数组。visited数组用来防止走回头路,不管是有向图还是无向图都要有,否则碰到环就会死循环。对于较小规模的图,也可以考虑在入队时而不是出队时标记访问,这样能避免同一个节点重复入队带来的浪费。
DFS里的“回溯”并不是一个抽象的概念,你可以理解成递归返回上一层后,要把当前状态恢复成进入时的状态。比如用DFS求迷宫所有路径,每尝试完一个方向,退回来时要记得把刚才标记为“已走”的格子复原。漏掉回溯是很多人写DFS最容易犯的错误,没有之一。
4.3 最小生成树和最短路径:算法脉络必须理清
图这一章真正的难点是算法,复习时可以按“解决什么问题—用什么策略—时间复杂度多少”来做对比。
最小生成树解决的是“用总权值最小的边把所有顶点连起来”的问题,典型算法有Prim和Kruskal。Prim适合稠密图,从某个顶点出发逐步生长,复杂度主要O(n²)或堆优化后O(E log V)。Kruskal适合稀疏图,把所有边按权值从小到大排序,再用并查集判断是否成环,复杂度主要由排序决定O(E log E)。
单源最短路径看Dijkstra,它要求图中边权不能为负,核心思想是贪心加松弛:每轮从未确定最短距离的顶点里挑一个距离最小的,用它去更新邻居的最短距离。很多初学者把Dijkstra和Prim搞混,都是“每次选距离最小的点”,区别是Prim维护的是到生成树的距离,Dijkstra维护的是到源点的距离,对比记忆效果更好。
如果边权可能为负,得用Bellman-Ford或者SPFA。任意两点最短路径则直接用Floyd,用三重循环依次把每个顶点当作中间点更新距离,实现极其简洁,但复杂度是O(n³),只适合顶点数不多的场景。
5. 查找和排序:笔试面试里出镜率最高的两大块
5.1 二分查找的边界条件,90%的人都会写错
二分查找本身思想很简单,但“是low<high还是low<=high”“mid取左中位还是右中位”“更新边界时mid是+1还是-1”这些细节几乎每次写都会纠结,面试中因为边界问题写崩的人非常多。
我习惯用一种不容易出错的左闭右闭写法:初始化low=0,high=n-1;循环条件while(low <= high);mid=(low+high)/2;如果target小于nums[mid],high=mid-1;如果target大于nums[mid],low=mid+1;相等就返回mid。这套写法配合左闭右闭的区间定义很自洽,不容易乱。
另一个高频延伸题是“查找第一个等于target的下标”或“查找最后一个小于等于target的下标”。这类题本质上是在模板循环里调整收缩方向:如果要找“第一个>=x”的位置,当nums[mid]>=x时,应该让high=mid,而不是high=mid-1,因为当前mid也可能是答案。但要注意这时代码里不能再用原来的low<=high循环,要改成low<high,才不会死循环。建议把基础二分模板和这个变体分别手写三遍,写熟练比看十遍解析都管用。
5.2 哈希查找:用空间换时间的极致方案
哈希查找的核心是把关键字通过哈希函数映射到数组下标,理想情况下查找O(1)。但哈希冲突不可避免,常见的解决冲突方法有开放定址法(线性探测、二次探测)和链地址法。链地址法实现简单、删除方便,是工程中很常见的方案,Java的HashMap在冲突严重时还会把链表转成红黑树来防止性能退化。
复习哈希表时,有个概念容易混淆:装填因子。装填因子是表中元素个数除以表长度,它越大代表冲突概率越高,一般超过0.75就该考虑扩容。面试如果聊到HashMap底层,从哈希函数讲到负载因子再讲到扩容后的rehash,基本就能撑起一段完整回答。
需要提醒的是,哈希表的遍历顺序是不确定的,它适合“按key精确查找”,不适合范围查询。如果题目要求找某个区间内的元素,应该优先考虑二叉搜索树、跳表或者有序数组二分,这是学数据结构时必须建立的“选型意识”。
5.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(1) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 快速排序 | O(n log n) | O(n²) | O(log n) | 不稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
| 计数排序 | O(n+k) | O(n+k) | O(k) | 稳定 |
| 桶排序 | O(n+k) | O(n²) | O(n+k) | 稳定 |
| 基数排序 | O(d(n+r)) | O(d(n+r)) | O(n+r) | 稳定 |
背表只是最低要求,你得理解几个关键点。第一,插入排序虽然平均是O(n²),但数据基本有序时非常快,几乎接近O(n),这也是很多高级排序(比如TimSort)在数据量小时回退到插入排序的原因。第二,快速排序最坏情况发生在每次选的基准都恰好把序列分成极度不均匀的两部分,比如对一个已经有序的序列每次选第一个元素当基准,就退化成O(n²),优化方法有随机选基准和三数取中。第三,归并排序是稳定的,但需要额外O(n)的空间,所以它更适合对稳定性有要求、又不介意额外内存的场景。
在工程里,大部分人用的是C++的sort或Python的sorted,这些库函数已经混合了多种排序策略,不需要自己从零实现,但看八股时还是要能说出为什么快速排序不是稳定的、稳定的排序有哪些。这是面试区分度很高的问法。
6. 踩过的坑和复习避坑指南
6.1 数据结构学习中的五个经典错误
第一个坑是只背结论不写代码。考试判断“二叉树第k层最多有2^(k-1)个节点”这种题可以靠背,但让你写出二叉树中序非递归遍历、堆排序的调整过程,不亲手实现过就很容易在考场上卡住。数据结构这门课的规律是:手写过的代码才是自己的,只看不算等于没学。
第二个坑是搞不清引用和指针。C语言版的严蔚敏教材大量使用指针、二级指针和引用。我当年写树的时候总是忘记在函数里修改指针时要传指针的指针,导致节点根本没接上。如果你也用C/C++复习,建议重点关注参数传递到底传的是值、指针还是指向指针的指针,这里一旦通了,链表、树、图的代码都会顺手很多。
第三个坑是忽视边界条件。空表删节点、循环队列满时再入队、二叉树只有左子树时的遍历、Dijkstra里遇到未访问节点初始化距离等,都是考试和面试喜欢挖细节的地方。每次写完代码,先主动想一遍“空、一个元素、满、有环”这些极端输入,能少踩很多坑。
第四个坑是没有对比记忆。数据结构里很多算法解决的是相似问题,比如Prim和Dijkstra、DFS和回溯、BFS和层序遍历,如果不做横向对比,学完很容易混。方法也很简单,每次学完一个新算法就停下来画张表,写下它和之前学过的相似算法的区别和适用场景。
第五个坑是忽略大题的步骤分。期末和考研的算法设计题是按步骤给分的,哪怕没有完整写出代码,能写出思路、数据结构定义、关键伪代码片段也能拿到不少分。复习时可以背一些常用模板,比如遍历模板、Dijkstra模板、并查集模板,考试时直接改改就能用。
6.2 复习时用的资料怎么选,才不容易踩雷
热词里出现了很多资料:严蔚敏《数据结构》(C语言版)、王道考研数据结构、王卓的数据结构PPT课件、李春葆的数据结构教程、各种电子书和网盘资源。我第一次复习时也下载了一堆材料,结果打开发现有的排版混乱、有的版本不一致,浪费了很多时间。后来总结出的经验是:选两本为主,其他只当作补充查询。
严蔚敏的《数据结构》(C语言版)是很经典的教材,内容偏学院派,算法严谨,考研和很多高校教材都按它的风格来,缺点是代码风格比较旧,新手容易读不下去。王道的数据结构更偏考研考点,适合以刷题和过考点为目标的人,但王道默认你有一定基础,零基础直接看会有点跳。李春葆的教程更偏应用和例题讲解,适合期末复习跟练。
网盘里流传的严蔚敏电子书、各种PPT课件,我建议用来查漏补缺——遇到上课没听懂的知识点,去PPT里翻翻推导过程,比硬啃教材强,但不建议从头到尾把所有资料都刷一遍。资料贵精不贵多,时间花在读代码和刷题上,比花在整理资料库里更值。
6.3 实操阶段的“刻意练习”怎么做才有效
想真正掌握数据结构,唯一可行的方法就是上机写代码和刷题。计算机专业的学生都知道,看懂和写出来之间差了十万八千里,很多代码你以为理解了,一运行就报错。
我建议按照这样一个顺序做刻意练习:
第一轮,实现线性表、栈、队列的最基本操作:初始化、插入、删除、查找。不需要整复杂的需求,把底层逻辑写对即可,用C或Python都行。C语言版的重点是链表和指针,Python版本的重点是思考和数据结构无关的逻辑,只关注本质。
第二轮,手动模拟迭代算法,尤其自己画图模拟排序、遍历。快速排序、归并排序、堆排序、Dijkstra、KMP这些算法,用纸笔跟着数据走一遍,在数组下标和指针变化的细节里会看到很多平时忽略的问题。
第三轮,刷LeetCode热点题。不用贪多,把高频的结构题刷明白就行,比如反转链表、LRU缓存、二叉树遍历、合并两个有序链表、用栈实现队列、环形链表、岛屿数量这类题,覆盖链表、栈、队列、树、图、哈希表、堆这些核心结构,刷一轮基本能应对一般面试。每道题做完以后回顾一下它用了什么数据结构,并且问自己为什么用这个结构而不是另一个,这是打通“数据结构怎么应用”的重要一步。
学到最后,数据结构拼的是“能不能画出来”
写到这里,最想分享的经验是:数据结构这门课不要靠背,要靠画。画就是学习时在纸上画出顺序表扩容的过程、链表插入时指针变化的过程、二叉树递归遍历的递归栈帧过程、图里邻接表的结构和最短路径的迭代过程。能把这些过程画明白,代码自然就能写出来;画不明白就去翻教材、看PPT、查资料,直到能徒手画到全对为止。
我个人从“听完课什么都懂、一做题就懵”到能比较流畅地写出各种结构代码,最有效的方法就是在白纸上不看书地默写这些结构和算法的过程图。每次默写完再对照教材改错,收获远比看十遍网课大。数据结构的内容也许很碎,但它有一条完整的主线,只要抓住“数据结构是组织的艺术”这一句,所有算法背后都是对数据如何存、如何查、如何增删的思考,学起来就不会再觉得东一块西一块了。