news 2026/11/6 8:10:17

算法学习笔记:从背模板到会推导,覆盖竞赛与面试核心

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
算法学习笔记:从背模板到会推导,覆盖竞赛与面试核心

第一次把算法书翻烂的那年,我犯了一个后来复盘很久才想明白的错误:把算法当成解题模板在背。题目稍微换个条件,我就不会了。这篇算法学习笔记,是我在实战、竞赛和笔试面试的反复螺旋中沉淀下来的主线,我没有打算把知识点面面俱到地塞进来,只挑了几类最值得反复咀嚼的方向,顺便把很多资料里不愿写的坑和判断依据讲透。内容适合准备算法工程师面试、打蓝桥杯这类算法竞赛、或者在工作中想补齐数据结构与算法基础的同学参考。文章不会追赶热点算法,更多在讲怎么把一道题从“会背答案”变成“会推导”。

1. 学算法前先解决三个原点问题

1.1 算法不是知识,是缩小搜索空间的决策序列

我常用的一个比喻:算法就是找钥匙。假设你把钥匙丢在一个有一万扇门的走廊里,暴力枚举就是把每一扇门都推一遍;知道钥匙只藏在房间号含偶数的房间里,你立刻剪掉一半;房间按编号排序,你还能二分着找。绝大多数基础算法,本质都是在回答“如何更小地搜索可能解”这个问题。排序为什么重要?因为它把无序集合变成有序结构,让搜索空间有了位置信息,后续可以用折半、双指针、树结构来进一步加速。触类旁通以后,遇到一个新问题,我的第一反应不是“它属于哪类模板”,而是“当前搜索空间有多大、有什么结构可以利用”。

再补充一个判断信号:如果暴力法只能到N=30,剪枝后勉强能到N=50,但数据范围给到N=1e5,那通常说明你的剪枝方向错了,真正该考虑的是把枚举状态压缩成递推关系,比如动态规划,把同一个搜索空间从指数级压到多项式级。所以真正难的不是学会某一个算法,而是学会建立“搜索空间—约束条件—算法选择”的映射。这个映射没建立起来之前,任何算法手册看起来都像天书,这也能解释为什么有些人刷了几百道题,换一道没见过的变体就懵。

1.2 用“数量级感觉”代替背公式

很多初学者拿着复杂度公式算,但一到实际题目就懵。我建议先把自己对数量级的直觉练出来:1秒内现代CPU大概能跑1e8到3e8次简单运算;N=1e5时,O(N^2)基本到极限;N=1e6以下,O(N log N)很安全。先把这张表刻进脑子里,比背任何公式都管用:

数据规模可接受的复杂度级别常见算法举例
N ≤ 10O(N!) 或 O(2^N·N)暴力排列、状态压缩
N ≤ 20O(2^N)子集枚举、暴力搜索
N ≤ 100O(N^3)Floyd、朴素区间DP
N ≤ 1000O(N^2)两两枚举、朴素DP
N ≤ 1e5O(N log N)排序、堆、二分、KMP、Tarjan
N ≤ 1e6O(N)线性扫、哈希、计数排序

这张表我自己用了很多年,在没有现成系统的情况下,它直接决定我往哪个方向想。复杂度的直觉还有一个逆运用:根据题目给的数据规模和时空上限,把解题方向压到很小的集合里。空间也一样要估,1e6个int是4MB,题目通常给256MB,开三五个这样的数组没问题,可如果开成二维long long就得仔细算。这些细节写进笔记里,比记一百个模板有用。

1.3 选语言和资料的三个建议

语言这块,我见过太多人纠结“什么语言好”,最后把时间浪费在反复切换上。如果你要打蓝桥杯或算法竞赛,C++最稳,STL的vector、stack、queue、map、priority_queue都是现成的,图论手写邻接表也很自然;Java和Python适合以日常开发为主的人,Java在蓝桥杯里也很常见,Python写原型快,但处理大常数场景要小心。资料选择上,我建议主线只选一本算法书,配一个题集,其他全是辅助。最容易踩的坑是收藏几十篇博客但没有一篇跟到底,收藏代替不了推导。

2. 花最多时间打磨的几个基础算法

2.1 枚举与剪枝:暴力的正确姿势

暴力枚举是整个算法体系的起点,也是我面试别人时很喜欢考的方向。枚举质量决定了之后所有优化的空间:你要能准确列出所有可能的取值组合,还要会判断状态合法。以N皇后问题为例,每一行放一个皇后,冲突剪枝用列、主对角线、副对角线三个布尔数组来判定,复杂度从N!级别直接降到很小的搜索树。

def dfs(i): if i == n: ans += 1 return for col in range(n): if not col_used[col] and not diag1[i + col] and not diag2[i - col + n - 1]: col_used[col] = diag1[i + col] = diag2[i - col + n - 1] = True dfs(i + 1) col_used[col] = diag1[i + col] = diag2[i - col + n - 1] = False

剪枝算法一般分三类:可行性剪枝,当前状态已经不可能到达答案;最优性剪枝,当前累计已经比已知最优解差;顺序剪枝,先搜索更容易到达解的分支。实际做题时,剪枝的收益往往靠实验说话,我会在笔记里记录同一个数据量下剪枝前后的差异,而不是凭感觉说“剪了肯定更快”。另外不要把剪枝和状态压缩混为一谈,剪枝是去掉不可能分支,状态压缩是把等价的重复状态合并,两者经常一起用,但思维路径不同。

2.2 排序算法:不是背复杂度,是背场景

冒泡排序我用来练“循环不变式”:每一轮把最大的元素像气泡一样顶到末尾。它虽然慢,但代码和正确性证明都简单,适合理解“程序为什么是对的”。堆排序有意思的地方在于把一个数组看成完全二叉树,用下沉或上浮维护堆性质,它在原地排序、常数空间,但不稳定。归并排序则是另一个极端,它稳定、复杂度稳定,还附带一个重要的衍生能力——求逆序对个数。归并时左半边指针指向的元素比右半边指针指向的元素大时,右半边剩余元素的个数就是跨区间的逆序数,这个思想比单独写树状数组容易理解,也更容易迁移到别的合并场景里。

实际工程里,标准库的sort通常不是某个单一算法,而是混合策略,比如C++的introsort:快速排序为主,递归深度过深切到堆排序,小规模区段切到插入排序。我建议你把冒泡、堆、归并都自己实现一遍,不是为了以后在工程里用,而是为了理解“稳定”“原地”“比较排序下界”这些概念是怎么从实现里长出来的。排序链表时归并排序是自然选择,因为链表不方便随机访问;求乱序数组的第K大,quickselect又更合适。每种排序背后是“数据存储方式+稳定性需求”的合力,记场景比记复杂度更靠谱。

2.3 查找算法:二分与哈希的边界感

查找算法是笔试和开发的常客,核心是两个流派:二分和哈希。二分的适用前提是单调性,注意这里的单调不一定是严格递增,而是“存在一个分界点,左边都满足一种性质,右边都满足另一种性质”。我一开始写二分总死循环,后来养成了固定模板的习惯:搜索区间表示成[left, right),每次取左中位数,更新时left=mid+1或right=mid。这个模板能规避“中间两个数选哪个”“什么时候比较等于”等大部分边界问题。

l, r = 0, n # 在 [l, r) 中找第一个满足 a[mid] >= target 的位置 while l < r: mid = (l + r) >> 1 if a[mid] >= target: r = mid else: l = mid + 1

哈希的核心是空间换时间,O(1)的平均查询很诱人,但要考虑冲突、扩容和内存开销。选择二分还是哈希不单看复杂度,还要看数据是否有序、是否要区间查询、键值规模多大、是否有人会构造冲突数据。比如在有序数组里找“第一个不小于target的位置”,二分可以顺带给出排名信息,哈希做不了。这些关系我每次都会用表格记录在笔记里,复习时一眼扫到。

3. 中阶算法:字符串、图论与动态规划

3.1 KMP:让字符串匹配学会“回头”

字符串匹配里,暴力做法是匹配失败后把模式串向右挪一格,等于把已经匹配过的信息全部扔掉。KMP的核心贡献是:利用模式串自身的重复结构,失败时跳到最长相等前后缀的位置继续,而不是从头再来。具体实现里,关键是前缀函数pi数组,pi[i]表示模式串以第i位结尾的前缀中,最长那个“既是前缀又是后缀”的长度。

void build_pi(string& p, vector<int>& pi) { int m = p.size(); pi[0] = 0; for (int i = 1; i < m; i++) { int j = pi[i - 1]; while (j > 0 && p[i] != p[j]) j = pi[j - 1]; if (p[i] == p[j]) j++; pi[i] = j; } }

我见过不少人在KMP上卡壳,要么是算pi数组时更新逻辑写错,要么是匹配时忘记主串指针不回退。有一个小技巧:写代码前先手工跑一遍例子,把pi数组算出来,再把匹配过程一步步写在纸上,代码出错的概率会大幅下降。把这道题吃透之后,可以继续想一个经典问题:模式串在文本串里可以重叠出现,怎么改匹配循环?答案其实就藏在“主串指针不回退,模式串回退到pi[j-1]”这个逻辑里。自动化机、字符串哈希、后缀数组都算后续延伸,但先吃透KMP,才能理解“预处理+滚动匹配”这套模式。

3.2 图论三件套:Tarjan、Prim与匈牙利

图论算法里我挑三个最常被面试和竞赛当作考点的:Tarjan、Prim、匈牙利。Tarjan利用DFS时间戳和low值,找出强连通分量、割点和桥。网上很多文章把它写得很玄,我后来用一句话记住:在DFS树里,如果子树内有一个节点能通过回边或横边到达比当前节点更早的祖先,那当前节点就不是割点;low值的意义就是“从当前子树能回溯到的最早时间戳”。

Prim算法是求最小生成树的代表之一。它从起点开始,每次从已选节点集合外选择一条连接集合内外的、权值最小的边,配合优先队列可实现O(E log V)。真正要理解的是切割定理:对一个切分,若横跨切割的边中有一条最小权边,它一定属于某棵最小生成树。吃透这个定理,才算真懂Prim和Kruskal之间为什么是殊途同归。

匈牙利算法解决二分图最大匹配问题。它的主角是增广路:从某个未匹配点出发,走交替路,如果到达另一个未匹配点,则把路径上的匹配边和非匹配边对调,匹配数就加一。这套逻辑说出来很抽象,动手模拟一个小例子,比如两边各4个点、画出几条可能的边,跑一遍匹配和增广,马上就能理解为什么找不到增广路就意味着已经达到最优匹配。图论算法特别依赖“把定义吃透加手动模拟”,只看代码不动手画图,很难真正内化。

3.3 动态规划:从记忆化到状态机

动态规划是最容易被“背模板”毁掉的一块。我的经验是三步走:先写暴力递归,观察递归参数里哪些会重复计算,然后把“递归参数+返回结果”缓存下来,这就是记忆化;找出状态和依赖关系后,再把依赖写成循环,就是递推。以背包问题为例:物品逐个考虑,决策只有“放/不放”,状态dp[i][w]表示前i个物品、容量w时的最大价值。代码可以很朴素,但它演示了“通过聚合子问题的结果来推导当前结果”的核心思想。

for i in range(1, n + 1): for w in range(W, weight[i] - 1, -1): dp[w] = max(dp[w], dp[w - weight[i]] + value[i])

状态设计是DP里最费脑子的部分。常见套路是按位置设计、按区间设计、按数值设计,再降维到滚动数组。比如区间DP的经典石子合并,dp[i][j]表示合并第i到第j堆的最小代价,转移要枚举最后一次合并的分割点。做题多以后会发现,很多所谓难题无非是“状态多一维”或“转移更复杂”的变体,复杂度分析是判断可行性的第一道闸门。学习DP时我强烈建议做题目笔记,不是抄题解,而是记录你为什么这样设计状态、边界为什么这样初始化、误写的转移会导致什么后果。

4. 把算法放到真实系统里看

4.1 推荐系统里排序和检索是怎么协作的

很多同学对“B站首页web推荐算法”这类话题感兴趣,想弄明白推荐引擎到底用了什么算法。真实推荐链路通常分为召回、粗排、精排、重排四个阶段。召回阶段要从海量视频里快速选出候选集合,这里常用近似近邻检索、倒排索引等结构,本质上就是一个“缩小搜索范围”的动作;精排阶段则用复杂的评分模型对候选打分排序,经典排序题在这个场景里变成“对打分结果排序”。换个角度,你会发现数据结构与算法并不是孤立考点,而是下一层基础设施:倒排索引、堆排序取TopK、布隆过滤器、近似哈希,都在为上一层的排序和召回服务。

这对学习者的启示是:不要只在题目里背一个算法,要能讲清它在一条真实链路上所处的位置。面试官如果问“为什么推荐页要召回而不是遍历全部视频”,答案就藏在那句“搜索空间太大,必须先用低成本结构缩范围”。算法学习的边际收益,在真实系统中往往体现在这类权衡判断上。

4.2 图像二值化:几个经典算法的优劣比较

热词里有一条很具体的考题:“简述几种针对灰度图像的二值化算法,并简单分析优劣”。这确实是常见的图像算法基础考点。核心目的,是把灰度图的每个像素变成黑白两值的图像,以便去噪、提取轮廓或作为后续分类的输入。最粗略的是固定阈值:手动定一个值,高于它置白、低于它置黑,优势是简单高效,劣势是光照不均匀时很容易把暗部细节全部抹掉。另一种常见的是Otsu大津法:通过最大化类间方差自动求阈值,适用于灰度直方图呈双峰分布的情况,但当双峰不明显时效果会明显下降。自适应局部阈值则把图像分成小块,在每块里算阈值,适合光照不均、阴影、渐变明显的场景,代价是计算量更大且对窗口参数敏感。

把这类图像算法纳入算法研究范围,我觉得很有价值,因为它能训练一种很重要的能力:面对一堆算法时,先明确约束条件,再做选择。单纯背代码不能回答“何时用哪个”,而一旦能比较优劣,才算是把算法笔记变成了真正的工程判断力。

4.3 工业控制和硬件里那些看着“小”的算法

热词中还有PID、MPPT、完整性校验这类工程性很强的关键词。它们表面看起来和竞赛题相距很远,但底层思维相通。PID是控制领域最常用的算法,比例、积分、微分三项分别对应“当前误差”“历史累计误差”“误差变化趋势”,典型调参就是从这三个方向去观察系统的超调、稳态误差和响应速度。MPPT是光伏系统里的最大功率点跟踪,常用爬山法本质上是一个“沿着当前梯度方向试探移动”的贪心优化。完整性校验算法,如CRC、哈希校验,则是“压缩信息形成指纹”的思路,用于检测数据是否被篡改。现在很多AI方向的“新”算法,比如深度强化学习算法、粒子群算法原理,底层也还是搜索、反馈和策略优化这套东西。

对于只打算法竞赛的人来说,这些内容未必是出题范围,但如果你以后要做嵌入式、工业AI或后端中间件,这些“小算法”往往比花哨的模型更决定系统是否稳定。学习时我会做一个小结:看一个工程算法,先问输入是什么、输出是什么、约束是什么、失败模式是什么。能把这四个问题答清楚的算法,基本就不会用错。

5. 学算法时踩过的坑与排查技巧

5.1 最常见的低级错误

这道题在笔记里值得列一个“黑名单”:第一,边界条件,例如空数组、只有一个元素、所有元素相等,每道题都要专门构造;第二,整数溢出,比如mid=(left+right)/2时left+right可能越界,改成left+(right-left)/2更稳妥;第三,死循环,常见于二分和迭代式DFS,用“循环体是否保证状态前进”来判断;第四,索引错位,KMP里pi数组的减一操作容易手滑,字符串题尤其容易出错;第五,递归深度超过栈上限,在竞赛里要改成循环加显式栈。

我把这些错误写成一张表格,每道题Review前先对照自查。不要小看这些问题,很多“题做得不少但正确率低”的人,绝大多数是输在这些细节上。算法题的正确性评价标准很严格,一个边界用例没过就是不过,和思路好不好关系不大。

5.2 造数据、对拍、打印中间态

排查算法错误,我有一个百试不爽的组合拳:随机小数据造数 + 暴力解做基准 + 对拍。最简单的方式是写个脚本生成几千组小规模随机数据,把优化算法和一个明显正确但很慢的暴力版本分别跑一下,比较输出。这个小技巧可以定位几乎所有“逻辑写歪”的问题,因为小数据能暴露出所有分支。对拍之外,另一种技能是打印关键中间态。比如看排序过程中某一轮数组长什么样,往往立刻就能发现问题,这比盯着代码瞪几小时管用得多。

这个习惯我从竞赛时期一直带到工作里。真实业务中你没办法对拍,但你可以把中间结果落盘、对比基线样本、逐步缩小输入范围,本质上还是同一套思路:先用可验证的小数据观察逻辑,再去信任优化实现。

5.3 刷题与复习的节奏

我见过有人一年刷了上千道题,面试时基础反而答得很虚;也有人只刷了三百道,却把常见套路讲得清清楚楚。差异就来自“是否做了提取”。我会按这样的节奏推进:第一阶段,专练基础数据结构与复杂度题,比如二叉树遍历、二分、排序、哈希;第二阶段,按算法专项推进,一个DP周期、一个图论周期、一个字符串周期;第三阶段,做综合和真题,模拟限时;第四阶段,回头整理错题和模板,把知识点之间的连接画成自己的知识图。复习比新题重要,一道题做透的三个维度是:会证明复杂度、会写边界用例、能讲出如果数据规模变化会怎么处理。

关于竞赛,蓝桥杯这类比赛很适合入门,它既考代码也考算法,投入产出的成就感很高。刷题不要只盯通过率,要给自己设置“输出型学习”的指标:每道题能否在白板上讲明白,才算真的会了。

6. 最后的一点心态建议

我自己写算法笔记的时候,有一个固定的格式:每个算法占一页纸,上半页写原理和代码,下半页写三个东西——这个算法解决了什么问题、什么情况下不能用、如果输入规模变化会怎样。坚持写几十个算法以后,我发现真正被记住的并不是代码,而是那些“为什么能这么做”的逻辑链条。算法学习是一个需要反复回滚的过程,今天看不懂的题,过两个月回来看可能一下就通了,关键是你得给未来的自己留下足够多的上下文。如果你现在正在为一道题焦虑,不妨先把代码关掉,把问题模型画清楚,再把暴力解法写出来,很多时候答案自己就会浮现出来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/6 8:10:16

STM32F103VET6硬件设计深度解析:从原理图到信号完整性

1. 这块板子到底值不值得拆&#xff1f;——从“指南者”三个字说起你手里的这块STM32F103VET6指南者开发板&#xff0c;不是一块普通的学习板。它名字里带“指南者”&#xff0c;不是营销噱头&#xff0c;而是实打实的设计意图&#xff1a;它是一份可触摸、可测量、可修改、可…

作者头像 李华
网站建设 2026/10/29 23:55:09

WPF实战指南:从数据绑定到MVVM与3D动画的全栈解析

从"wpf 28"这个标题说起吧。我猜它多半是从某篇WPF教程的第28回截出来的&#xff0c;或者只是个随手起的名字——但正是这种模糊的标题&#xff0c;反而把WPF这个技术栈的完整版图给勾出来了&#xff1a;从基础的数据绑定、MVVM&#xff0c;到有点进阶味道的命令定义…

作者头像 李华
网站建设 2026/11/5 21:18:34

智能停车场管理系统全链路拆解:SpringBoot+Vue车牌识别与计费实战

简介&#xff1a;这份资源是面向Java全栈学习者与课程设计/毕业设计开发者的智能停车场管理系统完整工程包&#xff0c;采用Springboot后端与Vue.js前端组合&#xff0c;覆盖车辆出入、车位状态、用户与订单等核心业务&#xff0c;适合作为信息管理系统分析与设计的实战参考。压…

作者头像 李华
网站建设 2026/10/23 2:46:39

OpenHarmony上Flutter二维码扫描与短信生成实战

1. 项目概述与技术选型思考1.1 这个项目到底在做什么最近在做一款基于 Flutter for OpenHarmony 的二维码扫描 App&#xff0c;核心功能就两条&#xff1a;一是调用摄像头实时扫码&#xff0c;二是能把短信内容生成二维码让别人扫。其中短信二维码这块&#xff0c;不是简单把一…

作者头像 李华
网站建设 2026/10/23 6:04:12

用Claude Code跑marketingskills:独立站SEO与CRO自动化实战

1. 从“marketingskills”说起&#xff1a;一个被低估的增长工具箱第一次看到marketingskills这个词&#xff0c;是在一个做独立站的朋友群里。有人甩了个链接&#xff0c;说“这套东西把 SEO 和 CRO 的活儿全串起来了&#xff0c;还能挂到 Claude Code 里跑”。我当时的第一反…

作者头像 李华
网站建设 2026/10/23 5:59:02

鸿蒙适配踩坑记:Flutter权限申请用permission_handler_ohos解决

如果你最近在把 Flutter 应用往鸿蒙设备上迁移&#xff0c;八成会踩到同一个坎&#xff1a;应用能跑起来&#xff0c;一碰权限就露馅。permission_handler 这个在 Android 和 iOS 上都无比顺滑的三方库&#xff0c;到了鸿蒙上要么不弹授权框&#xff0c;要么权限状态永远返回 d…

作者头像 李华