尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

数据结构与算法核心考点总结:从链表到动态规划的复习指南

数据结构与算法核心考点总结:从链表到动态规划的复习指南 1. 内容整体设计与思路拆解1.1 为什么说数据结构与算法是“硬通货”数据结构与算法这几年几乎成了计算机相关岗位的入门门槛。不管是考研408、软考程序员/软件设计师还是大厂校招笔试、外包机试翻来覆去考的就是那几板斧顺序表、链表、栈、队列、二叉树、排序、查找、哈希以及二分、贪心、动态规划、回溯这些基础算法思想。很多人一开始觉得难是因为知识点零散学完一章忘一章到后面做题时发现连“该用树还是用图”都分不清。我见过不少同学C语言学得不错指针也玩得明白但只要一提“数据结构”立刻就开始背定义、抄代码根本不知道这些结构在什么场景下才有优势。这是最典型的误区。数据结构不是一堆抽象概念的堆砌它解决的是“数据怎么组织和操作才够快、够省空间”的问题。搞清楚这一点你才能把你的C语言底子转化成真正能用的编程能力。这篇文章我按自己复习时的思路重新梳理了一遍核心知识点先把知识体系按“线性结构—树形结构—图结构—查找与排序—算法设计思想”五条主线铺开然后把每一条线下最常考、最容易出错的关键点拆开讲最后结合面试机试和笔试的实战场景聊聊怎么高效复习、怎么避坑。1.2 这套知识框架的整理逻辑先说整体框架的划分依据。数据结构教材不管是严蔚敏C语言版还是王道章节顺序虽然略有差异但本质上都遵循同一个逻辑从最简单的数据组织方式开始逐步增加复杂度。线性结构顺序表、链表、栈、队列、串、数组——数据之间是一对一的线性关系树形结构二叉树、BST、AVL、堆、哈夫曼树、并查集——数据之间是一对多的层次关系图结构邻接矩阵、邻接表、DFS/BFS、最短路径、最小生成树、拓扑排序——数据之间是多对多的任意关系查找与排序顺序查找、二分查找、哈希查找、各类内部排序算法——针对不同组织形态下的数据怎么高效地检索、重排算法设计思想分治、贪心、动态规划、回溯、剪枝——解决复杂问题的通用思考模板我复习时发现一个规律前面四块是“数据结构本身”后面一块是“怎么用这些数据结构去解题”。很多人卡在动态规划上不是因为他笨而是因为前面树、图、堆这些基础结构没形成肌肉记忆拿到题根本联想不到该用什么结构。所以我的建议是不管你是为了考试还是面试前四块必须能手写代码第五块可以靠刷题积累套路。2. 核心细节解析与实操要点2.1 线性表别只会背概念要能手写链表的增删改查线性表是数据结构的地基。顺序表和链表这两种实现你必须达到“闭着眼都能写”的程度。顺序表底层是数组优点是随机访问O(1)缺点是插入和删除需要移动大量元素平均移动次数约n/2时间复杂度O(n)。链表底层是节点加指针优点恰好反过来插入删除只要改指针O(1)时间复杂度但查找只能从头遍历O(n)。考试和面试最爱考的是单链表的反转、合并两个有序链表、查找倒数第k个节点、判断链表是否有环。这几个问题几乎是必背模板。以反转链表为例迭代写法里最关键的是要三个指针配合prev、cur、next每次循环先保存cur的下一个节点再把cur的next指向prev然后整体后移。很多人写错就是忘了先保存next导致链表在中间断掉。还有一个高频考点是“用两个栈实现队列”以及“用两个队列实现栈”。这类题考察的是你对这两种线性结构的理解深度。两个栈实现队列的思路很简单入队往stack1压出队时如果stack2为空就把stack1的元素全部倒进stack2再从stack2弹出。这样每个元素最多被搬运两次均摊时间复杂度O(1)。注意数组和链表的选择不是绝对的。如果数据量小、查询多、插入删除少数组明显更合适如果插入删除频繁链表更好。实际开发中C的vector、list、deque就是这两种结构在不同场景下的工程化封装你在刷题时也想想这些容器底层的取舍会理解得更深。2.2 栈与队列表达式求值、括号匹配、单调栈是重点栈的特点是后进先出队列是先进先出。不要小看这两个结构很多看似复杂的算法核心就是操作这两个结构。括号匹配是最经典的栈应用遍历字符串遇到左括号就压栈遇到右括号就弹栈并检查是否匹配最后栈为空说明全部匹配。代码量很小但考察的是对栈“后进先出”特性的理解。表达式求值也是一个高频考点。中缀表达式转后缀表达式逆波兰式用栈实现扫描中缀表达式操作数直接输出运算符与栈顶比较优先级优先级高或相等且左结合则入栈否则弹出栈顶运算符并输出。最后弹出栈中剩余运算符。有了后缀表达式求值就容易了遇到操作数入栈遇到运算符弹出两个操作数计算结果再入栈。这套流程在编译原理课程里也是核心内容值得一次弄懂。单调栈是这几年笔试和面试的热点典型题目是“每日温度”和“下一个更大元素”。单调栈维护一个栈内元素单调递增或递减的序列每个元素最多入栈一次、出栈一次所以总时间复杂度是O(n)。很多读者第一次看到单调栈的代码会觉得抽象我建议你找一张小纸手动模拟一遍入栈出栈的过程比看十遍代码都管用。窗口最大值问题滑动窗口用单调队列思路类似只是把栈换成了队。2.3 树与二叉树遍历序列反推、递归与层序是重中之重二叉树是数据结构的分水岭。很多同学在线性表部分还能跟上一到树就开始掉队。核心原因是树的很多操作天然适合用递归描述而对递归的不适应会让代码看起来像天书。先攻克遍历。四种遍历方式必须滚瓜烂熟前序根左右、中序左根右、后序左右根、层序从上到下、从左到右。前中后序用递归实现非常简洁层序则用队列实现根节点入队循环判断队列非空出队一个节点访问它然后左孩子入队、右孩子入队。层序对应的是广度优先搜索BFS在树的题目里非常常用。“由前序中序推后序”是笔试和面试的经典题。原理是前序遍历的第一个节点一定是根节点在中序遍历中找到这个根节点它左边就是左子树序列右边就是右子树序列然后递归处理。如果不理解这个原理光靠背代码很容易在边界条件上出错。我建议一定要亲手推一遍比如前序124536、中序425163按上面的方法推出整棵树再写出后序序列。二叉搜索树BST的特性是左子树所有节点值小于根右子树所有节点值大于根中序遍历BST得到一个递增序列。这个性质使BST成为查找、插入、删除都很快的树结构理想情况下O(log n)。AVL树和红黑树是对BST的平衡优化考研和面试主要考概念和旋转思想不要求完全手写但你要能说清楚左旋、右旋的作用以及为什么平衡能保证查找效率。堆是一个特殊的完全二叉树用数组存储大顶堆的父节点大于等于孩子节点小顶堆反之。堆排序和优先队列都依赖它。建堆的复杂度是O(n)这个结论很多人会搞错记住是O(n)而不是O(nlog n)因为越底层的节点调整代价越小。2.4 图邻接矩阵 vs 邻接表DFS和BFS的代码模板图这章的信息量很大但常考的点非常集中存储方式、遍历、最短路径、最小生成树、拓扑排序。存储方式两张表要能默写。邻接矩阵适合稠密图判断两点之间是否连通是O(1)但浪费空间邻接表适合稀疏图省空间但判断连通需要遍历链表。考试时根据题目给出的顶点数和边数快速判断用哪种存储结构合适也是一种基本功。DFS和BFS是图的核心也是很多复杂算法的骨架。DFS用递归或栈实现BFS用队列实现。刷题时你会发现岛屿数量、克隆图、课程表等很多题无非是套用DFS/BFS模板再做一些剪枝和状态标记。写DFS最重要的一点是避免死循环已访问的节点必须做标记否则在环上会无限递归。最短路径方面单源最短路径用Dijkstra算法不能有负权边全源最短路径用Floyd算法带负权边但没有负环的单源最短路用Bellman-Ford。考研408重点在前两个。Dijkstra算法的核心是“贪心”每次从未标记节点中选距离源点最近的那个更新它邻接节点的距离。你要理解为什么这个贪心策略在没有负权边时是正确的而不是死记硬背。最小生成树有两种经典算法Prim算法从点出发每次选择连接已选集合和未选集合的最小边Kruskal算法从边出发把所有边按权值排序从小到大选择不构成环的边用并查集判断是否成环。对比记忆会轻松很多。拓扑排序是图的另一个高频考点常用于任务调度。思路是不断找入度为0的节点输出并从图中删除同时更新邻接点的入度。如果最终输出的节点数小于总节点数说明图中存在环任务调度不可行。“课程表”这道题就是典型的拓扑排序应用。2.5 查找与排序复杂度表要背熟快排和归并必须手写查找和排序是数据结构里“背诵量”最大的一块但也是性价比最高的一块。二分查找代码很简单但边界条件经常写错。我推荐统一使用“左闭右开”的写法while (left right)每次根据mid与目标的大小关系移动left或right避免死循环和越界。二分查找的变体找左边界、右边界、插入位置在面试中极其常见建议把三个变体各写三遍直到能秒出。排序算法的重点在于稳定性、时间/空间复杂度、每趟之后的结果。下面这张表是我复习时反复对照的建议直接背排序算法平均时间复杂度最坏时间复杂度空间复杂度稳定性冒泡排序O(n^2)O(n^2)O(1)稳定简单选择排序O(n^2)O(n^2)O(1)不稳定直接插入排序O(n^2)O(n^2)O(1)稳定希尔排序O(n^1.3左右)O(n^2)O(1)不稳定快速排序O(nlog n)O(n^2)O(log n)不稳定归并排序O(nlog n)O(nlog n)O(n)稳定堆排序O(nlog n)O(nlog n)O(1)不稳定计数排序O(nk)O(nk)O(k)稳定快排必须能手写因为它是“分治”思想最典型的应用之一。核心是partition函数选一个基准值把比基准小的放左边、大的放右边然后递归处理左右两边。注意在最坏情况下快排退化成O(n^2)所以实际工程中常用三数取中法选基准。归并排序也重要它的稳定性和O(nlog n)的复杂度使它很适合外部排序和解决“逆序对”问题。逆序对那道题就是归并排序的简单扩展理解了归并过程就能做出来。哈希表要做到“能说出哈希冲突的解决方案”。链地址法拉链法最常用开放定址法线性探测、二次探测、再散列也要懂概念。哈希表的负载因子、扩容时机也是面试官爱问的点。C的unordered_map底层就是哈希表遇到需要快速查找的题优先考虑它。3. 实操过程与核心环节实现3.1 手写一份C语言版的单链表反转很多同学反映“看得懂别人代码自己一写就废”。这很正常代码能力靠背、靠练不靠看。我建议你按下面步骤实操一遍单链表反转用C语言写不要依赖C的STL容器。#include stdio.h #include stdlib.h typedef struct Node { int data; struct Node* next; } Node; // 反转链表迭代法 Node* reverseList(Node* head) { Node* prev NULL; Node* cur head; while (cur ! NULL) { Node* next cur-next; // 先保存下一个节点 cur-next prev; // 反转指针 prev cur; // prev 后移 cur next; // cur 后移 } return prev; // prev是新的头节点 }建议你在本地IDE里加上打印函数初始化一个1-2-3-4-5的链表然后调用reverseList把返回值从头打印一遍。重点关注哪些节点在什么时刻指向谁。等你把图画出来、代码跑通这道题就算真正掌握了。如果想挑战递归写法思路是reverse(head-next)之后让head-next-next head并让head-next NULL作为递归结束条件。代码更简洁但对理解要求更高。我建议两种写法都练笔试时用迭代更稳妥面试时能说出递归写法会加分。3.2 快速排序的partition到底怎么写快拍的partition写法有很多种最不容易写错的是“挖坑法”。它的步骤是把数组第一个元素存到临时变量pivot中此时第一个位置是“坑”。从右往左找比pivot小的元素填入坑中该位置变成新坑。从左往右找比pivot大的元素填入坑中该位置变成新坑。重复2、3直到左右指针相遇把pivot填回坑里。这样partition结束时pivot左边的元素都小于等于它右边的都大于等于它。之后递归处理左右区间即可。写递归代码时特别注意递归终止条件当left right时直接返回否则会无限递归或越界访问。void quickSort(int arr[], int left, int right) { if (left right) return; int i left, j right; int pivot arr[left]; // 挖坑法基准取第一个元素 while (i j) { while (i j arr[j] pivot) j--; arr[i] arr[j]; while (i j arr[i] pivot) i; arr[j] arr[i]; } arr[i] pivot; quickSort(arr, left, i - 1); quickSort(arr, i 1, right); }实操时我建议你用一个长度为5~8的数组在纸上一步步模拟挖坑过程标出每轮i、j的位置和坑的位置。把这一步做扎实了快排就再也不会出问题了。此外需要理解内层while中为什么写arr[j] pivot而不是arr[j] pivot——用可以避免数组中有大量重复元素时左右指针不移动、导致死循环。3.3 从一道“树的层序遍历”看BFS模板层序遍历是树的题里出镜率最高的一类。很多进阶题比如二叉树的最大深度、右视图、之字形遍历、二叉树的最小深度都是在层序遍历基础上加一点变量维护。所以我会建议先把层序遍历模板写熟练。#define MAX 100 int* levelOrder(struct TreeNode* root, int* returnSize) { if (root NULL) { *returnSize 0; return NULL; } struct TreeNode* queue[MAX]; int front 0, rear 0; int* res (int*)malloc(sizeof(int) * MAX); int count 0; queue[rear] root; while (front rear) { struct TreeNode* node queue[front]; res[count] node-val; if (node-left) queue[rear] node-left; if (node-right) queue[rear] node-right; } *returnSize count; return res; }注意这段代码为了演示简洁使用定长数组模拟队列实际工程中应该用动态扩容。另外如果你刷的是LeetCode通常需要“分层输出”也就是结果是一个二维数组每一层一个数组。那就要在while循环里额外维护一个“当前层节点数”变量比如先用int size rear - front然后只处理size个节点这样就能知道这一层的边界在哪里。这个技巧几乎百试百灵值得写进你自己的模板里。3.4 从“背包问题”看动态规划的状态设计动态规划是算法面试中的“拦路虎”。0-1背包是最经典的人门题有n件物品每件有自己的重量w[i]和价值v[i]背包容量为W问能装下的最大价值。状态定义是dp[i][j]表示“考虑前i件物品、背包容量为j时能获得的最大价值”。转移方程则为dp[i][j] max(dp[i-1][j], dp[i-1][j-w[i]] v[i])其中第二项仅在j w[i]时取。可以观察到dp[i]只依赖dp[i-1]因此可以用一维滚动数组优化空间但要特别注意j必须从W往0倒序遍历否则一件物品会被重复使用变成“完全背包”的语义。这个“为什么倒序”的问题笔试和面试里经常被追问一定要能讲清楚。实际做题时动态规划难的不是转移方程本身而是“怎么定义状态”。我的经验是先尽量把题目中所有约束变量列出来那几个变量往往就是状态数组的维度。如果感觉状态太多想想能不能合并或忽略不影响最优策略的量。0-1背包为什么要二维因为“前几件”和“当前容量”这两个变量都会影响后续决策。多练几道经典DP题比如爬楼梯、不同路径、最长递增子序列、编辑距离状态设计的感觉就会慢慢建立起来。4. 常见问题与排查技巧实录4.1 链表题老是死循环是哪里出了问题链表题出错十有八九是丢掉了下一个节点的引用或者在边界上没有处理NULL。比如反转链表时如果忘了保存cur-next下一次循环cur就没法移动最终导致死循环。解决这类问题的通用排查方式有两条画图。把每个节点的地址、指针方向画出来模拟每次循环之后的状态。链表的指针操作几乎都能靠画图避免错误。加断言。比如在每次给cur赋值之前检查cur是否为NULL。虽然笔试环境里不能这么做但本地调试时特别有效。另一个常见的坑是“断尾”。链表节点释放或移动后新链表最后一个节点的next必须置为NULL否则遍历时会越界或打印出莫名其妙的地址。每次写完链表题都检查一遍末尾节点的next是否真正指向NULL。4.2 递归树的高度与递归栈溢出树的题目用递归非常舒服但递归深度过大会导致栈溢出。最典型的是求二叉树的高度如果用一个递归实现树的高度是10000那递归深度就是10000在默认栈大小下很可能崩掉。这时可以用层序遍历BFS来求高度每遍历一层计数器加一空间复杂度是O(w)w为最大层宽度。对于极端退化的链表式树递归版快排也会栈溢出这就是为什么很多工程实现里快排用非递归或者限制递归深度。如果你在笔试时遇到“二叉树的最大深度”建议默认先写递归版因为测试用例通常是平衡树不会触发栈溢出但如果题目明确说树可能很深就要主动改成层序遍历。判断这类问题的依据很简单看一眼树节点的数量范围。如果节点数在10^4以上且树可能退化果断用BFS。4.3 排序算法里的稳定性和每趟结果总记混记混的主要原因是把“稳定性”当概念死背。稳定性的本质是如果两个元素的键值相等排序后它们之间的相对顺序是否保持不变。冒泡排序和直接插入排序为什么稳定因为它们只在相邻元素比较且逆序时才交换相等的元素不会被换到对方前面去。选择排序为什么不稳定比如序列[5, 5, 3]第一趟选择会把3和第一个5交换两个5的相对顺序就变了。“每趟之后的结果”这类填空题其实不用死背只要理解每一趟做了什么。快排每趟确定一个基准元素的最终位置冒泡每趟把当前未排区间的最大元素“浮”到最后面堆排序每趟把堆顶元素交换到当前未排区间的末尾然后重新调整堆。理解了过程任何变形题你都能应付。4.4 笔试和面试中“想不出最优解”的应对策略很多人一看到算法题就紧张觉得必须一步到位写出最优解。实际上按“暴力解→优化解→最优解”的顺序推进才是面试官更欣赏的状态。先给出暴力解法说明时间复杂度和空间复杂度再逐步优化。即使最终没达到最优解你也展示了自己清晰的思考过程。在笔试尤其是机试里时间和分数是有限的。如果一道题五分钟没有思路先跳过做后面的最后有时间再回来。机试反而不太看过程只看你交的代码能不能通过测试用例所以“可行但不够优”的解法也比空着强。平时刷题的时候我习惯给每道题设置一个“思考闹钟”前10分钟自己思考10分钟后还没思路就看题解然后合上题解自己手写一遍。这样既不会浪费太多时间又能保证训练效率。4.5 经典高频考点速查表下面把数据结构与算法中最常考的考点整理成一个速查表也可以作为你复习最后几天的自查清单。每一项都问自己能不能在几分钟内写出核心代码或说清楚原理如果不能回到对应章节重点补。考点核心要点常见出题方式链表反转三指针/迭代、递归手写代码括号匹配栈、左右括号配对手写代码/判断合法性二叉树三种遍历递归与迭代、遍历结果推导给序列求另一序列层序遍历队列、分层统计节点个数求深度/右视图/之字遍历BST性质中序递增、查找插入删除判断BST/验证BST快排partition挖坑法、左右指针交换手写/分析复杂度二分查找边界左闭右开、边界收缩查找插入位置/旋转数组动态规划状态设计dp数组含义、转移方程、滚动数组背包/爬楼梯/LIS/编辑距离图的DFS/BFSvisited数组、递归/队列连通分量/拓扑排序/最短路径哈希冲突链地址法、线性探测概念选择/手写简单哈希5. 从复习策略到考场实战5.1 不同时间周期的复习安排如果你准备时间充裕比如还有3个月以上我建议按“教材精读课后题每日刷题”的节奏走。教材我建议以严蔚敏C语言版或王道单科书为主重点在于理解每一章的核心思路而不是抄代码。每学完一章节把课后习题中涉及原理推导的题目做一遍代码题至少要在本子上写一遍完整的函数体。如果时间很紧比如两周后考试那就不要从头到尾翻教材了直接过历年真题和核心考点速查表。把排序复杂度表、二叉树遍历、快排、归并、二分、DP经典题这些高频考点全部手写一遍然后把错题反复看。考前一天的晚上不要再接触新题重点复盘自己写过的代码框架和易错点。5.2 关于软考、考研408和程序员笔试的针对性建议我在实际备考中发现不同场景的侧重点差别很大。软考程序员、软件设计师偏重对概念和原理的理解上午题会考时间复杂度比较、排序稳定性、二叉树性质、图的基本概念等下午题主要是算法和程序设计重点练C语言对顺序表、链表、二叉树的操作。题目整体难度比考研408低一些但覆盖面很广容易出现“知道不熟”的题所以建议考前把基础概念刷一遍。考研408的数据结构部分代码题越来越灵活往往不直接考教材原题而是考某种思想的应用。比如给出一个场景让你设计一个高效的算法并分析复杂度。这种题短时间突击很难平时一定要多积累“用哈希换时间”“用双指针处理线性表”“用栈处理括号类问题”这类模板化思路。408还非常喜欢考“时间/空间复杂度分析”所以不只是会写代码还要能严谨地推导复杂度。面试笔试则完全不同重点放在手写代码和解题思路上。面试官关注的是你能不能把自己思路讲清楚边界条件能不能考虑全面。笔试呢只要代码能过测试就行偶尔用暴力解法也能拿大部分分。这里我必须说一个核心观点不要因为“面试官可能不问某个知识点”就跳过它。数据结构和算法是一个完整体系每个知识点之间都有联系看似冷门的堆排序可能会出现在“求数据流中的中位数”这种高频题里。5.3 如何培养“算法直觉”而不是被题海淹没我遇到过很多人刷了几百道题遇到新题还是没思路。问题往往出在“没有做题型归纳”。刷题不只是为了数量更是为了总结套路。比如看到“最近/最大/最小/滑动窗口”就考虑单调栈或单调队列看到“最短路径”就考虑BFS或Dijkstra看到“子集/组合/排列”就考虑回溯看到“最优化/选择/是否可行”就考虑DP或二分答案。这些条件反射不是靠天赋而是靠大量同类题目刺激出来的。另外一个容易被忽略的点是复杂度分析。很多时候你不会做一道题是因为没先估算数据范围。看到n是10^5你就要立刻想到O(n^2)基本过不了逼迫自己去想O(nlog n)甚至O(n)的解法。这种“先看范围再定算法”的意识在笔试中非常提分。我个人在实际复习过程中习惯准备一个错题本但不会只记代码而是记“这道题为什么卡住”“卡在哪一步”“下次该怎么从读题联想到正确方法”。几天后重新做一遍错题检验自己是否真正内化了思路。这个方法我推荐给所有备考的人比反复刷简单题有用得多。数据结构与算法这条路没有什么捷径但绝对有“正确的方法”。把每类数据结构的本质特性弄明白把每个算法思想与它适用的场景对应起来再通过持续手写代码和题型归纳形成肌肉记忆你就能在考试、面试甚至实际工作中真正感受到这门课的强大。最后再分享一个小技巧每学完一个章节尝试用一句大白话概括这个结构或算法“最擅长解决什么问题”。如果你能说出来说明你是真懂了。
返回列表