最近后台一直有读者问我:数据结构与算法到底该怎么学?很多人手里捧着严蔚敏老师的《数据结构(C语言版)》,或者跟着王卓老师的PPT课件、王道考研系列在啃,但学着学着就卡住了——不是看不懂代码,而是不知道这些东西学了能干嘛。
我太理解这种状态了。从408考研到软考,从外包面试到大厂算法题,数据结构与算法的确是一座绕不过去的大山。但我想先说一个反直觉的结论:你学不好数据结构与算法,问题往往不是出在“不够努力”,而是出在“不知道每个结构到底在解决什么问题”。这篇笔记,我会把自己这些年学习和实战的整理思路全部倒出来,串起数组、链表、栈、队列、树、图、排序、哈希这些核心模块,每一块都讲清楚“它是什么——它解决什么问题——工程里怎么用——有哪些坑”,尽量让零基础的读者也能顺畅地跟下来。
1. 学数据结构之前,先把这些基础概念钉死
1.1 逻辑结构、存储结构与“怎么选”的问题
数据结构这个词听起来玄乎,其实就两件事:数据元素之间的关系怎么描述,以及这些关系在计算机里怎么落地。
先看逻辑结构。教科书里分四种:集合、线性结构、树形结构、图状结构。集合就是一堆元素之间没什么特别关系,你中有我我中有你但彼此平等;线性结构就是一对一,像排队打饭,每个人都有且只有一个前驱和一个后继;树形结构是一对多,像公司组织架构,一个老板管好几个员工;图状结构是多对多,像地铁线路网,任意两个站点之间都可能连通。这是从“逻辑”上描述数据关系,你可以理解为画在纸上的蓝图。
再看存储结构,也就是这份蓝图在内存里怎么落地。主流两种:顺序存储和链式存储。顺序存储用一段连续的内存空间挨个存放,C语言里就是一个数组;链式存储则在每个节点里额外存一个指针,把分散在内存各处的节点串起来。C语言版数据结构教材里大量出现的struct Node { int data; struct Node *next; },就是链式存储的典型形态。
那实际开发中怎么选?我的经验是三个字:看操作。如果核心操作是按下标随机访问,比如“给我第100个元素”,顺序存储直接通过地址偏移搞定,时间复杂度O(1),链表却要一步步遍历,O(n)。反过来,如果是频繁在中间插入、删除,比如维护一批实时变化的订单,顺序表每插一个元素都要把后面所有元素往后挪,链表只需改几个指针就行。所以没有绝对的好坏,只有适不适合当前的业务场景。
1.2 时间复杂度和空间复杂度:不是考试专用,而是工程决策的依据
很多初学者把大O复杂度当成考试填空题来背,觉得跟写代码没什么关系。其实这是工程决策最重要的工具。大O描述的是当数据规模n趋向无穷大时,算法运行时间的增长趋势,它忽略常数系数、忽略低阶项,只保留最高阶。
常见的量级我排个序:O(1)是常数时间,不管数据多大都是一瞬间完成,比如数组按下标取值;O(log n)是对数时间,典型代表是二分查找,数据翻一倍,只多一次比较;O(n)是线性时间,比如遍历一遍数组找最大值;O(n log n)是线性对数时间,快速排序、归并排序就落在这个级别;O(n^2)是平方时间,冒泡排序、两层for循环嵌套就是;再往上还有O(2^n)、O(n!),这种规模到了几十基本就跑不动了。
怎么快速估算?核心是看循环。一段代码里如果有一个循环遍历n个元素,大概率是O(n);有嵌套的两层循环,每层都遍历n个,那就是O(n^2);如果循环变量每次迭代都减半,比如while (i > 0) i /= 2,那就是O(log n)。递归的情况稍微复杂,要看递归的调用树有几个分支、递归深度是多少,比如二叉树遍历,每个节点访问一次,总共O(n),但递归栈的深度是树高,平均O(log n),最坏O(n),这就是空间复杂度。
注意:空间复杂度不只算显式分配的数组,递归调用时的函数栈帧也要算进去。面试里常问“这题的空间复杂度是多少”,很多人漏掉递归栈,一答就错。
1.3 抽象数据类型:为什么说“接口优先于实现”
教材里还会反复出现一个概念叫ADT(Abstract Data Type,抽象数据类型)。听起来高大上,其实核心思想特别朴素:把“能做什么操作”和“内部怎么实现”分开。
举个例子,栈这个ADT定义了入栈push、出栈pop、取栈顶top这几个操作,但你用数组实现也行,用链表实现也行,外部调用者根本不关心。就像你点外卖只关心能不能送到,不关心骑手走哪条路。C++的STL、Java的集合框架,都是先定义接口再给出多种实现,这是软件工程设计里“面向接口编程”的基石。学数据结构的时候养成一个习惯:先问“这个结构支持哪些操作、各自的时间复杂度是多少”,再去看代码,思路会清晰很多。
2. 线性表、栈与队列:所有高级结构的积木
2.1 顺序表与链表:C语言实现里的经典对决
线性表是所有数据结构里最简单的形态,它的两种实现方式——顺序表和链表——是理解后面一切结构的地基。
顺序表说白了就是动态数组,C语言里可以是固定长度的数组,也可以是malloc出来的连续空间。它的优势是随机访问强、CPU缓存友好,因为数据都在连续内存里,读取时会一次性加载到缓存行,遍历速度极快。缺点是中间插入、删除需要搬移大量元素。
链表则完全不同,每个节点单独分配内存,通过指针串联:
typedef struct Node { int data; struct Node *next; } Node;在已知前驱节点的情况下,插入、删除都只要改指针,时间复杂度O(1),这是它最大的优势。但链表的每个节点要额外存一个指针,内存占用更大;而且节点在内存里是分散的,遍历时频繁发生缓存未命中,实际运行速度往往比顺序表慢。这也是很多初学者困惑的点:明明链表插入删除快,为什么实际项目里很多时候还是用数组?因为现代计算机里内存访问的代价往往比计算本身更大,顺序排列带来的缓存友好性能优势,很多时候能抵消掉算法层面的劣势。
我把两者的关键对比列在下面,方便你复习时一眼扫过:
| 对比维度 | 顺序表 | 链表 |
|---|---|---|
| 内存布局 | 连续 | 分散 |
| 随机访问 | O(1) | O(n) |
| 已知位置插入/删除 | O(n) | O(1) |
| 额外内存 | 基本无 | 每节点一个指针 |
| 缓存友好性 | 高 | 低 |
工程里最典型的链表应用是Linux内核的双向循环链表,以及各种LRU缓存的实现。平时写业务代码你可能很少直接操作链表,但这个结构的思想无处不在——比如数据库的B+树叶子节点之间就是用指针串联的。
2.2 栈:从递归到括号匹配,后进先出的哲学
栈是一个只允许在一端(栈顶)插入和删除的线性表,后进先出(LIFO)。你可以把它想成一摞盘子,只能从最上面拿,也只在最上面放。
栈的学习里,括号匹配是最经典的入门题:给你一串包含()[]{}的字符串,判断括号是否合法。解法思路就是用栈,遇到左括号就入栈,遇到右括号就检查栈顶是否匹配:
bool isValid(char *s) { Stack st = initStack(); for (int i = 0; s[i]; i++) { if (s[i] == '(' || s[i] == '[' || s[i] == '{') { push(&st, s[i]); } else { if (isEmpty(&st)) return false; char top = pop(&st); if ((s[i] == ')' && top != '(') || (s[i] == ']' && top != '[') || (s[i] == '}' && top != '{')) return false; } } return isEmpty(&st); }初看不觉得有什么,但你要是亲手实现一遍就会明白:栈天然适合处理“需要回溯最近状态”的场景。递归函数其实就是靠系统栈一层层压栈、出栈的,所以递归太深会爆栈(Stack Overflow)。反过来,当你想把递归改成迭代版本时,往往就要自己手动维护一个栈,这就是二叉树遍历迭代版的核心思路。表达式求值、函数调用、浏览器后退,都是栈在撑腰。
2.3 队列与循环队列:生产者和消费者之间的“传送带”
队列是先进先出(FIFO),跟排队做核酸一样,先来的先处理。它最常用的场景是“缓冲”——生产者产生数据,消费者处理数据,两者速度不一致时,队列就是中间那条传送带。
顺序实现队列时有个经典坑叫“假溢出”:入队出队都只移动rear和front指针,当rear到达数组尾部时,即使数组前面还有空位,也无法再入队了。解决方案是循环队列——逻辑上把数组首尾相接。判断队空是front == rear,判断队满是(rear + 1) % MAXSIZE == front,注意这里故意浪费一个存储单元来区分队空和队满,这个细节考试特别喜欢考,也特别容易记混。可以这样记:队空时两个指针重逢,队满时rear紧挨在front后面。
工程里队列到处是:操作系统的进程就绪队列、消息中间件里的Topic分区、BFS广度优先搜索里的待访问节点集合,全都是队列的舞台。学到这里,你就已经拿到了后面图论里BFS的钥匙。
3. 树与二叉树:从考研真题到工程应用的桥梁
3.1 二叉树的遍历:递归转迭代的关键思路
树是递归结构最自然的表达:一个节点下面挂着若干子树,子树又是一棵树。二叉树因为每个节点最多两个孩子,结构规整,成了研究和应用最广泛的形态。四种遍历方式——先序(根左右)、中序(左根右)、后序(左右根)、层序(逐层扫)——是所有树相关算法的基础。
递归版本的遍历代码只有几行,无脑好写,但面试官经常会追问一句:“不用递归怎么写?”这时你要意识到:递归在底层靠的是系统栈,迭代版其实就是用显式的栈模拟系统栈。先序遍历的迭代版本很简单,根先入栈,然后每次弹出节点、先压右孩子再压左孩子;中序和后序稍微麻烦一些,需要加一个状态标记或者用“反向思维”处理节点的访问时机。这个过程建议你亲手推演至少三遍,推明白了,你对“递归就是栈”这件事会有切肤的体会。
层序遍历则需要队列配合:节点出队时,把它左右孩子依次入队,天然符合一层一层扩散的节奏。很多实战场景——比如求二叉树的最小深度、二叉树的右视图——本质都是在层序遍历的骨架上加点逻辑。
3.2 二叉搜索树与平衡树:为什么AVL和红黑树是面试常客
二叉搜索树(BST)的规则是左小右大:对任意节点,左子树所有值都小于它,右子树所有值都大于它。这个性质带来一个巨大红利:查找、插入、删除都能通过比较大小不断缩小范围,平均复杂度O(log n)。但BST有个致命弱点——如果数据按有序顺序插入,树会退化成一条链表,操作复杂度直接掉到O(n),比没优化还惨。
于是平衡树出现了。AVL树强制任何节点的左右子树高度差不超过1,严格平衡,查询稳定O(log n),但插入删除时为了维持平衡需要频繁旋转,代价较高。红黑树则是“近似平衡”——它不追求绝对高度差,只保证最长路径不超过最短路径的两倍,牺牲一点点查询性能,换来了大幅减少的旋转次数。这也是为什么工程界最终普遍选择了红黑树:C++ STL的map/set、Java的TreeMap/TreeSet、Linux内核的CFS调度器,底层都是红黑树。你不需要能手写红黑树,但必须知道它“近似平衡、查询O(log n)、插入删除代价可控”这几个关键特征,以及“旋转”在维持平衡中的作用。
3.3 堆与优先队列:堆排序和TOP K问题的核心
堆是一棵特殊的完全二叉树,通常用数组来存储,而且存法很巧妙:根节点在下标1(或0),任意节点i的左孩子是2i(或2i+1),右孩子是2i+1(或2i+2),父亲是i/2。有了这套下标映射,完全不需要指针就能在数组里“长”出一棵树来。
大顶堆要求父节点值不小于孩子节点值,小顶堆则相反。堆的精华操作有两个:上浮(新元素插入到末尾后,不断和父节点比较、交换,直到满足堆序)和下沉(删除堆顶后,把末尾元素放到堆顶,再不断和较大的孩子交换)。复杂度都是O(log n),因为树高就是log n。
堆最经典的应用是TOP K:海量数据里找最大的K个,维护一个大小为K的小顶堆,堆顶是当前第K大的元素;每次来一个新元素,如果比堆顶大,就替换堆顶并下沉,最终堆里就是最大的K个。这个思路在搜索引擎的热词统计、实时日志里的异常值监控里用得极其频繁。堆排序也是基于堆:先建堆,再反复把堆顶与末尾交换,把最大元素沉到数组末尾,逐步形成有序序列。
3.4 哈夫曼树:最优前缀编码是怎么诞生的
哈夫曼树(最优二叉树)解决的问题是:给一批带权节点,怎么构造一棵二叉树,使所有叶子节点的带权路径长度之和最小?通俗说就是权重大的节点离根越近越好,这样总体代价最小。
构造过程是个典型的贪心策略:每次从集合中选择权值最小的两个节点合并,生成一个新的父节点(权值等于两者之和),放回集合,重复直到只剩一个根。最后左分支标0、右分支标1,就能得到每个叶子对应的哈夫曼编码。这套编码有个关键性质:任意一个字符的编码都不是另一个字符编码的前缀,所以可以无歧义解码。当年我第一次接触时觉得这纯粹是数学游戏,后来才意识到ZIP、JPEG这些压缩算法里到处都有它的影子——频率高的字符给短编码,频率低的给长编码,用更少的比特表达同样的信息,压缩的本质就这么回事。
4. 图论算法:最短路径、最小生成树与拓扑排序实战
4.1 图的存储选择:邻接矩阵 vs 邻接表
图比树更自由,任何两点之间都可能相连,所以存储方式需要更多权衡。两种主流方案:
邻接矩阵用二维数组edge[i][j]存储i到j是否有边(或者权重),判断任意两点是否相邻是O(1),但空间永远是O(V^2),适合稠密图。邻接表则是给每个顶点挂一条链表,只存储实际存在的边,空间O(V+E),适合稀疏图,但判断两点是否相邻需要遍历链表。
实际工程中真实的图——比如社交网络好友关系、城市道路网——基本都是稀疏的,所以邻接表是更常见的选择。但也别完全排斥矩阵:当顶点数量很少(比如几十个)、需要频繁判断两点连通性时,矩阵在代码简洁度和常数性能上有很大优势。这类“看着哪个都不错,要按场景选”的决策,就是数据结构这门课真正要训练你的核心能力。
4.2 迪杰斯特拉算法与负权值:一次面试追问引发的思考
迪杰斯特拉算法(Dijkstra)解决的是非负权重的单源最短路径问题:从起点出发,每次从未访问的节点中选出当前距离最小的节点,标记为已访问,并尝试用它去松弛(更新)相邻节点的距离,重复直到所有节点都被访问。这是一种贪心策略,之所以能成立,是因为所有边权非负时,当前未访问节点中距离最小的那个,已经不可能再被其他路径优化了。
我见过很多人被面试官追问“Dijkstra能处理负权边吗”时答不上来。正确答案是不行,原因藏在贪心的正确性前提里:一旦存在负权边,某个节点即使已经被标记为已访问,也可能通过一条包含负边的路径得到更短的距离。比如A到B权重1,A到C权重10,C到B权重-9,那么从A出发先确定B距离1就不对了,因为走A->C->B总距离只有1+(-9)但这是负权边场景,说明之前的最短路判定被打破。更简单的例子是三角形结构里,直接到B的路径是1,但绕道C再到B反而是1+(-9)=-8,比直接去还近。遇到负权边时,要改用Bellman-Ford算法(可以处理负权,还能检测负权环)或SPFA。这个问题背后的启示是:任何算法都有适用边界,边界往往就藏在它证明过程里那一步关键假设中。
4.3 从最小生成树看贪心策略:Prim和Kruskal怎么选
最小生成树(MST)解决的是:在带权无向图中找到一棵包含所有顶点的树,使所有边的权重之和最小。典型场景是网络布线、铺水管——要让所有城市连通,怎么修路总造价最低。
两个经典算法都是贪心,但切入点不同。Prim算法是“加点”:从一个点出发,每次从未连接的顶点里选一个离已连接集合最近的点加进来,适合稠密图,用优先队列优化后复杂度O(E log V)。Kruskal算法是“加边”:把所有边按权重排序,从小到大依次尝试加入,只要不形成环就保留,适合稀疏图,配合并查集实现,复杂度O(E log E)。判断“是否成环”这一步就是并查集的经典应用场景——学习的时候建议把并查集和Kruskal放在一起看,你会发现前者几乎是为后者量身定做的数据结构。
4.4 拓扑排序与关键路径:有向无环图的工程价值
拓扑排序解决的是依赖顺序问题:很多任务之间有先后关系,比如“必须先学完数据结构再去学算法分析”,“必须先完成需求评审才能开始编码”,怎么排出一个合法的执行顺序?拓扑排序的输出就是这样一个线性序列,使得任意一条有向边u->v,u都排在v前面。
最常用的实现是Kahn算法:统计每个节点的入度,把所有入度为0的节点入队,然后不断出队、把它指向的节点入度减1,减到0就入队。如果最后入队的顶点数少于总顶点数,说明图里有环——这在工程里意味着依赖循环,比如模块A依赖B、B又依赖A,编译系统会直接报错。拓扑排序在构建工具Makefile、包管理器依赖解析、编译器里都有应用,是一个看着冷门但实际特别实用的算法。
5. 排序算法:从冒泡到快排,八种排序到底在比什么
5.1 冒泡、选择、插入:O(n^2)三兄弟和它们的使用场景
排序算法是所有教材里篇幅最重的部分,也是初学者最容易迷失的地方。我的建议是别急着背代码,先分清楚三类O(n^2)算法的性格差异:
- 冒泡排序:相邻元素两两比较,把大的往后“冒”。好处是代码直观、最好理解;坏处是交换次数多,而且即使数据几乎有序,不加优化时仍然要跑完整趟。它的教学意义大于实战意义,但提前结束标志这个优化点值得记住:某一趟完全没有发生交换,说明已经有序,可以直接break。
- 选择排序:每趟扫描选出最小元素放到前面固定位置。思路清晰,交换次数少(最多n-1次),但不管数据本来多有序,比较次数永远是n(n-1)/2,所以“适应能力”最差。
- 插入排序:把当前元素插入到前面已排序区间的合适位置。它的优势被很多人忽视了:当数据基本有序时,插入排序的比较次数接近O(n),是这三兄弟里唯一拥有“准线性”表现的。这也是为什么复杂排序算法(比如Timsort、快排的优化版本)在数据规模较小或接近有序时,会回退到插入排序来收尾的原因。
5.2 希尔、归并、快速与堆排序:跨越O(n log n)的台阶
从O(n^2)跨到O(n log n),核心思想是分治——把大问题拆成小问题,解决小问题后合并结果。
快速排序是使用最广的排序算法,核心是partition(分区):选一个基准值,把数组分成小于和大于基准的两部分,递归处理左右。平均O(n log n),但因为基准选取不当可能退化成O(n^2),所以工程版快排通常采用“三数取中”或随机选基准来规避。归并排序则是彻底稳定的分治:先拆到单元素,再两两有序合并,代价是需要O(n)的额外空间。堆排序我们已经讲过去,它的优势是原地排序、最坏也是O(n log n),劣势是不稳定、常数较大。
对比这几个排序,我把关键指标整理成一张表,考试前复习这张表能省不少时间:
| 排序算法 | 平均时间复杂度 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
|---|---|---|---|---|
| 冒泡排序 | 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 log n) | O(n^2) | O(log n) | 不稳定 |
| 归并排序 | O(n log n) | O(n log n) | O(n) | 稳定 |
| 堆排序 | O(n log n) | O(n log n) | O(1) | 不稳定 |
5.3 工程中排序的真实选择:稳定性、原地性与系统库
真到了工程项目里,绝大多数情况下你不会手写排序,而是直接调用语言内置的排序函数。但理解排序算法依然必要,因为你要能回答“为什么这个库这么选”。
稳定性在业务上非常关键:比如表格先按时间排序,再按用户排序,如果算法不稳定,第二次排序会把第一次的结果打乱。Java对对象数组用的Timsort就是稳定排序的典范,它本质上是归并排序和插入排序的融合,还特别擅长处理接近有序的数据。Python内置的sorted也用Timsort。C语言的qsort则是不稳定的快排变体,因为它更关注平均速度和原地性。你不需要记住每种语言的具体实现,但要形成一种判断力:当数据量级、有序程度、稳定性需求发生变化时,你能说出哪种算法更合适,这才是面试官真正想考察的东西。
6. 哈希表与更多经典算法:面试题库背后的工程思维
6.1 哈希表:从散列函数到冲突消解
哈希表(Hash Table)可能是工程应用最广泛的数据结构,它的核心是用一个散列函数把键映射到数组下标,从而实现O(1)的插入、查找、删除。但散列函数可能把不同键映射到同一个下标,这就是哈希冲突,所有哈希表的设计本质都在围绕如何减少和处理冲突。
两种主流策略:拉链法在冲突位置挂一条链表(现代主流实现是链表加红黑树),Java的HashMap在链表长度超过8且数组长度超过64时会树化;开放寻址法则在冲突时向后探测空闲位置,Redis字典、Go的map早期版本都用到这类思路。无论哪种策略,哈希表性能都受一个指标影响叫负载因子——已存元素占桶数量的比例,超过阈值就要扩容rehash,这是一次全量重排,代价很高。所以工程上当你能预估数据规模时,提前指定初始容量是极其划算的优化。
6.2 字符串匹配:从暴力到KMP的思维跳跃
字符串匹配是文本处理的基础,搜索引擎、编辑器查找替换、病毒特征码扫描,底层都是它。暴力匹配的思路是逐个位置尝试,一旦失配就把模式串整体右移一位重新比,最坏O(n*m),在长文本上会卡到怀疑人生。
KMP算法的精髓在于:失配时不是在模式串上傻乎乎地移到头,而是根据已经匹配部分的信息(next数组)决定跳到哪个位置继续比。这个next数组记录的是模式串每个前缀里“相同前后缀的最大长度”,代码写出来十几行,但理解它需要反复推演。比如模式串ABABAC,当匹配到字符C时失配,根据next数组可以直接跳回位置,因为前缀ABA与文本的ABA已经配上了,不需要从头开始。KMP的复杂度是O(n+m),这事最妙的点在于:它用预处理阶段O(m)的代价,换来了匹配阶段线性时间的收益,这个“用空间换时间”的思想在大数据处理里到处都是。
6.3 贪心、二分与动态规划:三个容易混的算法思想
刷题的人最常挂在嘴边的三个词就是贪心、二分、动态规划,但很多人其实分不清它们的适用场景。
贪心算法每次做出当前看起来最优的选择,寄希望于局部最优能导向全局最优。它最典型的特征是“不可反悔”,所以能用贪心的问题往往需要严格的证明,比如活动选择问题(每次选最早结束的活动)、哈夫曼编码(每次合并最小两个)。二分算法则依赖一个单调性条件:待搜索区间内的元素一定满足某种有序性质。常见的坑是边界条件——left < right还是left <= right,mid = (left + right) / 2还是(left + right + 1) / 2,一个符号写错就是死循环。我的建议是固定记住一套写法,不要每次现场推理,比如统一用while (left < right)配合mid = left + (right - left) / 2,并在循环外处理终止条件,能省下大量调试时间。
动态规划则是三者里最通用也最难掌握的,它适合有重叠子问题和最优子结构的问题:把大问题拆成小问题,记录每个子问题的答案避免重复计算。拿零钱兑换来说,给定面额和总金额求最少硬币数,暴力递归会反复计算同一金额的最优解,DP则用一维数组从0开始逐步推到target。判断一个题能不能用DP,关键不是看题目的长相,而是看“剪掉一个选择后,剩下的问题是不是一个更小但同类型的问题”。
6.4 从数据结构到真实系统:检索、加密、调度里的算法身影
写到最后一个章节,我想回应很多读者心里的疑问:“这些算法我在业务里根本用不到啊?”我的看法是:算法知识确实不会每天出现在你写的CRUD里,但它是你理解真实系统的“隐形眼镜”。
搜索引擎的倒排索引,本质就是哈希表加链表的组合;数据库的索引,是B+树与哈希索引的取舍;消息队列的延迟消息,依赖优先队列(堆)来管理时间戳;实时监控里的增量式PID控制算法,本质也是“按误差动态调整输出”的经典算法……再比如安全领域,AES、国密SM2/SM3/SM4这些加密算法,表面看是数学公式,实际上每一步都依赖精心设计的数据排列和异或、移位等位运算组合。还有热词里那些看起来很高端的粒子群算法、模拟退火算法,它们本质上就是“在解空间里搜索最优解”的不同策略,和你在数据结构课里学的贪心、分治、动态规划是同一层思维在不同问题域里的延伸。
所以不要把自己框在“算法只是应付考试”的认知里。学数据结构与算法,真正学到的是两样东西:一是分析问题复杂度、评估方案优劣的思维框架,二是把现实关系抽象成结构模型的能力。这两样东西一旦建立,学任何新框架、新中间件,你都会比别人快一截。拿我自己的体会来说,当年啃Dijkstra的证明时觉得费劲,后来做地图路线的技术方案时才意识到,那套“贪心加松弛”的框架直接帮我理解了链路状态路由协议的设计意图。这也是我一直建议身边人不要囫囵吞枣背代码的原因——那些教科书里的定理和证明,才是你将来判断一个技术方案靠不靠谱的底层依据。