最近帮一个学弟看某厂的算法笔试卷,他考完一脸懵地问我:"题目我都能看懂,但就是不知道从哪里下手,感觉平时刷题白刷了。"我把那张卷子从头到尾过了一遍,发现一个挺扎心的事实:程序员算法笔试卷这个场景,和很多人理解的"刷题"根本不是一回事。笔试不是考你会不会背某个模板,而是在短时间内考察你拆解问题、建模、选型、边界处理和复杂度判断的综合能力。
如果你也正在准备这类笔试,或者已经在面试流程里被算法题折磨过,这篇文章应该能帮你省下不少试错成本。我会从面试官出题的视角、高频题型的拆解方法、动态规划和贪心的那些"看似正确实则翻车"的陷阱,到手撕代码时的实际细节,完整复盘一遍。我尽量把筛选标准、准备思路和避坑经验都讲透,让它不仅是"又一份题解",而是能落地到下一次笔试里的方法论。
1. 面试官出一套算法笔试卷时,心里到底在想什么
1.1 算法题不是考试的"计算题":它在筛选一种建模能力
很多程序员准备算法笔试,会把精力放在"背题"上:看到二叉树就递归,看到子序列就想DP,看到字符串匹配就套KMP模板。但你别忘了,面试官出题的时候,从来不是为了让你默写模板。他真正想看到的是:你拿到一个从没见过的、甚至有点绕的问题时,能不能把它转化成一个自己熟悉的问题模型,然后用严谨的代码把它表达出来。
举个例子。我用过一道题,原题是"一排房子,每个房子里有不同数量的金币,但不能偷相邻的两家,问最多能偷多少"。很多候选人第一反应是贪心:每次都取当前能取的最大值。然后很自信地写完了。但跑两个测试用例就露馅了,因为这道题其实是个典型的动态规划:dp[i] = max(dp[i-1], dp[i-2] + nums[i])。所以面试官真正考察的是你"能不能看穿问题本质"——并不是题目本身多难,而是你有没有建立"模型识别"的习惯。
我当时出的每一套笔试卷,题目数量通常控制在5到6道,难度从热身到压轴逐步递进。不会出现一道题卡死全场的情况,但一定有一道题是用来区分"中等水平"和"高水平"的。这道题往往不是考你某个冷门算法,而是把常见算法包装在一个新的场景里。这就回到了"建模能力"这个核心。
1.2 笔试难度梯度是怎么设计的
一套合格的程序员算法笔试卷,通常分为三个梯队:
- 第一梯队是热身题:考察基础数据结构操作,比如数组去重、链表反转、字符串翻转。这类题的正确率要求很高,基本是送分题,用来筛掉"完全没准备"的候选人。
- 第二梯队是核心题:开始考察算法思想,比如二分查找的变种、排序算法的手写、常见DP模型(背包、LIS、编辑距离)。这里能看出候选人有没有系统学过算法。
- 第三梯队是压轴题:往往结合了多个知识点,比如"TopK问题 + 外部排序 + 堆/快排变种",或"图的遍历 + 状态压缩DP"。这类题的完整通过率通常不高,但它起到的是"加分"作用,不是"一票否决"作用。
知道这个梯度有什么用?非常有用。它决定了你的答题策略:如果第一梯队都写不利索,就别死磕第三梯队,先把能拿的分稳稳拿到。很多人笔试挂掉,不是压轴题没做出来,而是前两梯队的小题因为边界条件、复杂度细节丢掉了一堆分。
1.3 那些看起来"超纲"的考点其实有迹可循
热词里出现了"粒子群算法""模拟退火""音频重采样""Rete算法""PID控制"这类看起来和笔试没关系的东西,有些同学一看就慌:"这也要考?"我的判断是:这类偏门算法出现在正式笔试中的概率很低,除非你投的是特定方向的岗位(比如音视频、机器学习、规则引擎)。但它们出现在面试聊资或技术深挖环节的概率不低——面试官可能不问你怎么实现粒子群,但会让你讲讲"启发式搜索和精确搜索的取舍"。
所以我的建议是:把核心经典算法(排序、搜索、动态规划、图论基础、字符串匹配)吃透,做到可以手写、可以讲清复杂度、可以分析边界。至于粒子群、模拟退火这类,你只需要知道它们解决什么问题、和经典算法相比优劣在哪,就足够应付大多数场面了。
2. 高频笔试题型拆解:数组、字符串、链表与树
2.1 KMP的next数组:背模板之前先搞懂那三行回退
热词里恰好有"在KMP算法中,对于模式串p='abacaba',其next数组",这是真题里很常见的考法。很多候选人能把KMP的匹配过程背出来,但一问next数组怎么求就卡壳,或者干脆把next的含义都搞混了。
先说人话:KMP本质上是在做一件事——匹配失败时,模式串别傻乎乎地从头再来,而是跳到已经匹配过的、最长的相同前后缀位置。next数组存的就是"当第i位匹配失败时,模式串应该跳到哪一位"。这个"跳到哪一位"的数学含义是:模式串p[0...i-1]中,最长的相等前缀和后缀的长度。
以p = "abacaba"为例,我带你手算一遍:
next[0]约定为-1(或者说0,取决于写法)。next[1]:看"a",没有真前后缀,为0。next[2]:看"ab",前缀a,后缀b,不相等,为0。next[3]:看"aba",前缀a= 后缀a,长度1;再看前缀ab= 后缀ba,不相等,所以为1。next[4]:看"abac",最长相等前后缀是0。next[5]:看"abaca",前缀a= 后缀a,长度1;前缀abvs 后缀ca不行;abavsaca不行。所以为1。next[6]:看"abacab",前缀ab= 后缀ab,长度2;其他都不行,所以为2。
很多网上的模板会直接给你代码,但最靠谱的记忆方式是理解那个j的回溯:while (j != -1 && p[i] != p[j]) j = next[j];这句话的意思是——新来的字符匹配不上,就利用已经算好的部分匹配信息,把前缀指针往前退,直到能匹配或退无可退。理解了这条回退链,求next数组才不会出错。
我在实际笔试里见过一个高频陷阱:题目定义next[i]为"模式串前i个字符组成子串的最长相等前后缀长度",和图论里某些教科书用的"失配时跳转位置"差了一位。你落笔之前,一定要看清楚题目定义,否则一个for循环下来,整个数组全错。
2.2 排序算法的考察:不只是"背一个快排"
排序算法在笔试卷上出现的频率高到什么程度?高到我几乎可以断言,你投10家公司至少有7家会问排序。但考察方式并不只是"手写快速排序",更多的是:
- 给你一个近乎有序的数组,让你选排序方案并说明原因。这时候插入排序可能比快排更优。
- 让你分析归并排序的空间复杂度为什么是O(n),以及可以怎么优化(原地归并虽然难,但思路可以谈)。
- 让你写堆排序,但要求不用递归。这题能挂掉不少人,因为堆排序的调整逻辑虽然简单,但写起来细节很多。
我自己的建议是:手写快排、归并、堆排、插入、选择、冒泡,目标是在白板或编辑器里一遍写对。不要小看这件事,很多候选人平时在IDE里写了无数遍,但笔试环境没有自动补全、没有调试器、甚至没有编译运行,就写不出来了。这是纯手感的差距,只能靠默写训练来补。
另外要清楚排序算法的稳定性:稳定的有插入、冒泡、归并;不稳定的有选择、快排、堆排。这个知识点在"按多个字段排序"的场景题里会冒出来,比如先按分数从高到低,再按ID从小到大,如果你用了不稳定的排序,第二层的顺序可能被打乱。
2.3 链表与树的题目:指针操作和递归状态的验证
链表和二叉树是手写代码的高频区,因为它们考的是"指针操作"和"递归思维",这两个能力恰恰是很多程序员日常业务开发里很少用的。但它们的解法和代码量都不大,很适合笔试环境。
链表题我最推荐先画图再写码。比如反转链表,你光靠脑子想很容易绕晕,但画一个三个节点的链表,把每一步的next指向标出来,代码就顺理成章了。另一个常见陷阱是"链表的环":用快慢指针判断是否有环不难,但很多人忘了快指针走一步、慢指针走一步的写法在空链表和单节点链表上会空指针异常,这种边界细节就是我前面说的"白丢分点"。
二叉树方面,前序、中序、后序、层序遍历是基础,但笔试里更常考的是它们的变种,比如"最近公共祖先"、"二叉树的最大路径和"、"根据层序遍历结果重建二叉树"。这些题目想考察的核心是:你能不能把递归函数的"返回值定义"想清楚。我见过很多候选人写递归,但说不清楚"这个函数到底返回什么",这就是伪理解。我自己的习惯是:写递归前,先用一行注释写出函数定义,然后保证所有分支都围绕这个定义展开,这样可以避免大量的逻辑混乱。
3. 动态规划与贪心:两类最容易被看穿的"伪解法"
3.1 从爬楼梯到编辑距离:DP状态定义是第一步也是最后一步
动态规划是算法笔试的绝对核心,几乎每套卷子里都会出现。热词里的"贪心算法""动态规划"是最常被混淆的一对,我先给一个直白的判断标准:如果你能证明每一步的局部最优选择不会影响未来选择的空间,那才可以用贪心;否则大概率需要DP。
很多人学DP最大的问题是:看到题目觉得"有点像DP",然后就开始瞎写转移方程。我的经验是,DP题的成败在状态定义,而不在递推公式。状态定义不对,递推公式写得再漂亮也是空中楼阁。
举个经典例子:编辑距离。dp[i][j]被定义为"字符串A的前i个字符转换成字符串B的前j个字符所需的最小操作数"。为什么偏偏是"前i个"和"前j个"?因为这是一种规模描述:把大问题拆成小问题,小问题必须由两个子串的前缀规模来界定。一旦定义好,转移就顺理成章:dp[i][j]可以从dp[i-1][j](删)、dp[i][j-1](插)、dp[i-1][j-1](替换或不操作)三个方向推导。如果没有先想清楚状态定义,直接去凑公式,很容易漏掉某个操作。
还有一类DP题,热词里的"打家劫舍"就是代表——它考的不是"会写转移方程",而是"你能不能把一维问题扩展成二维、三维状态"。"每个房子偷不偷"这种0/1决策,天然适合DP。我在笔试里见过一个变种:房子围成一圈,首尾不能同时偷。解法是把原问题拆成两个子问题:偷第一家不偷最后一家、不偷第一家偷最后一家。没有这个思路,硬写状态会非常痛苦。
3.2 贪心算法为什么"想当然"容易错
贪心算法在笔试里出现的频率也很高,但它的正确性常常经不起推敲。我见过一个很典型的错误示范:题目是"给一堆活动区间,选尽量多的不重叠活动"。很多候选人会说:"我每次选结束时间最早的。"然后直接写循环。这确实是对的,但很少有人能说清为什么对——因为结束越早,给后面留下的时间越多,这个"交换论证"逻辑其实是可以被严格证明的。
但换一道题就不行了。比如"找零钱:有1、5、11面额的硬币,凑出15元的最少硬币数"。贪心的做法是"每次选不大于剩余金额的最大面额",于是选11、1、1、1、1,共5枚。但最优解其实是5+5+5,共3枚。这就是典型的贪心反例:大面额虽然单次"赚得多",但它会压缩后续选择空间。这类题必须用DP:dp[i] = min(dp[i-1], dp[i-5], dp[i-11]) + 1。
所以我在复盘笔试时,养成了一个习惯:任何贪心解法写完后,花一分钟构造一两个反例。构造反例的方法是"想想局部最优会不会锁死全局最优"。如果构造不出来,再默认贪心是对的。
3.3 一个经典反例帮你看清贪心和DP的分界线
再来一个更经典的对比:数字三角形问题——给你一个三角形数字塔,从顶部出发,每次可以走到左下方或右下方,问经过路径上数字和的最大值。
如果你用贪心,每一步都选较大的那个子节点,很可能会错过底部更大的数。比如:顶部是5,左子是99右子是100,你选了100,但99下面全是1,100下面全是1,而99左上角其实是0,这就略过了99下方可能存在的巨大数值。这是贪心失效的典型场景。
DP的解法是从底部往上推:dp[i][j] = triangle[i][j] + max(dp[i+1][j], dp[i+1][j+1])。你看,状态定义是"从(i,j)出发到底部的最大路径和",然后从底部逐层向上计算,时间复杂度O(n²),空间复杂度还可以优化到O(n)。本质上,数字三角形考察的是"你愿不愿意为全局最优放弃局部最优"的思维模式。面试官出这道题,其实是想听到你分析"为什么贪心不行、DP才行"的过程,而不是直接甩一个转移方程。
4. 手撕代码时的实战细节:输入输出、边界条件与复杂度陷阱
4.1 笔试环境下的输入输出:那些没见过的"坑"
很多第一次参加在线笔试的程序员会挂在输入输出上。比如牛客网和LeetCode的模式完全不同:LeetCode已经帮你写好了函数签名,而牛客或公司自研OJ要你自己处理标准输入、拼接输出格式。每年都有候选人因为不熟悉这种模式,把大量时间浪费在"读入"上。
我整理几个常见的坑,先提前排掉:
- 读整行字符串:如果输入包含一行带空格的字符串,用
cin >> s会只读到空格就停,需要用getline或nextLine。 - 多组测试用例:很多题会以"多组输入,每组占一行"的形式出现。我见过不少人在每行处理完后忘了刷新输出,导致答案挤在一起。
- 输出格式:要求每两个数字之间以空格分隔、行尾无多余空格。这个细节非常恼人,但也很容易写好:用循环判断"不是最后一个元素就输出空格",或者先把结果存进数组最后join。
- 输入结束标志:有的题是"读到EOF结束",很多人不会写
while(cin >> n)这个循环条件。
这些坑本身不难,难的是在笔试的高压环境下还要分心去处理。我自己的做法是:考前专门练3-5道牛客网的输入输出练习题,把各种读入模式跑一遍,形成肌肉记忆。
4.2 边界条件:不是"小心就好",而是要有检查清单
边界条件是算法笔试中最可惜的丢分点。一个候选人写了极漂亮的算法思路,结果因为空数组访问越界、i+1操作越界、整数溢出被判错,才是最亏的事。边界问题不能靠"小心"解决,要有一套固定的检查流程。我给自己总结的检查清单如下,每次写完代码后逐项过一遍:
- 数组/字符串为空时,代码行为对不对?
- 数组/字符串只有一个元素时,行为对不对?
- 循环里有没有
i-1或i+1访问?什么时候可能越界? - 输入数值有没有可能等于
Integer.MAX_VALUE或Long.MAX_VALUE?加减乘除会不会溢出? - 递归的终止条件,是覆盖了规模最小的情况,还是只覆盖了"空"?
- 双指针或快慢指针,是否会因为快指针先走到头而提前退出?
- 使用哈希表时,第一个元素和最后一个元素有没有走同一套逻辑?
这套清单看起来普通,但每次笔试都能救我几次。比如二分查找的边界,很多人用while(left <= right)还是while(left < right)全凭记忆,我建议直接在草稿纸上写一个长度为2和长度为1的小数组,把过程走一遍,比死记模板可靠得多。
4.3 复杂度评估:面试官最在意的三个问题
手写完代码,面试官大概率会追问三个问题:
- 时间复杂度是多少?别只说O(n²),要说明哪一层循环导致了n²。
- 空间复杂度是多少?如果用了递归,要算上递归调用栈的深度;如果用哈希表,要说明哈希表存储的元素数量和输入规模的关系。
- 能否优化?时间/空间怎么取舍?这一问是真正的高分区。比如你写了一个哈希表解法,空间O(n),面试官会问你能否在原数组上操作,把空间降到O(1)。再比如你写了递归,他会问"会不会导致栈溢出?怎么改成迭代?"
很多候选人栽在低级的复杂度判断上:比如把for循环里套一个Arrays.sort说成O(n),但实际上排序的复杂度是O(n log n)。再比如你在循环内不断拼接字符串,Java里String是不可变的,每次拼接都是O(len)的新建操作,整体可能从O(n)变成O(n²)。这种细节,面试官一眼就能看出来,但写代码的人常常不自知。
我个人建议,每道题写完,顺手在代码注释里写上复杂度,既方便自己复核,也给面试官一个"我想清楚了"的信号。更重要的是,要能说出"可不可以更优"以及"这个更优方案在什么条件下不成立"——比如快排最坏O(n²),但可以随机化基准来规避,这本身就是很好的加分点。
5. 一套可复用的刷题与复盘方法:从"会做题"到"能讲题"
5.1 一套按"题型大类"而非"题目来源"的分类法
很多人刷题是"今天在题库里随便挑一道,做完就完事",这种方法效率很低。我更推荐先按题型建框架,再往框架里填题。笔试中最常见的题型大类,我列了一个清单:
- 数组与字符串:双指针、滑动窗口、前缀和、哈希表、区间合并、字符串匹配(KMP等)。
- 链表:反转、环检测、合并有序链表、找中点、删除倒数第N个节点。
- 栈与队列:单调栈(下一个更大元素)、用栈模拟队列、表达式求值。
- 树:递归遍历、层序遍历、BST相关、最近公共祖先、树的序列化。
- 堆与优先队列:TopK问题、合并K个有序链表、数据流中位数、堆排序。
- 图:DFS、BFS、拓扑排序、最短路径(Dijkstra、Floyd)、并查集。
- 动态规划:线性DP、背包DP、区间DP、状态压缩DP、树形DP。
- 贪心:区间调度、加油站、跳跃游戏、分发饼干。
- 排序与查找:手写排序、二分查找变种、有序数组中的搜索。
- 数学与位运算:质因数分解、快速幂、异或性质、求平方根、格雷码。
你按这个清单去分配刷题时间,比"今天随机刷一道难题、明天又随机刷一道简单题"要有效得多。我见过一种很常见的备考误区:大量刷难题,但基础题型的熟练度不够。真要上了笔试,难题不一定考,但基础题一定会考。这个清单就是让你先确保基础,再往高处走。
5.2 复盘模板:每道题都值得回答的六个问题
刷题不复盘等于白刷。但复盘不是"把题解看一遍"就算完,而是要用自己的话把逻辑讲清楚。我给自己定了一个六问复盘模板,也分享给你:
- 这道题考察的是哪个/哪几个知识点?
- 我第一眼看到它时,想到了什么思路?这个思路为什么对/为什么不对?
- 最优解的状态定义或算法选择是什么?我为什么没想到?
- 边界条件有哪些?哪些边界是我第一次写的时候漏掉的?
- 如果改变一个条件(比如数据规模变大、数组变为有序、一维变二维),解法会怎么变?
- 这道题和我以前做的哪道题是"换皮不换芯"的?
第3问和第6问最有价值。第3问能帮你发现自己思维盲区,第6问能帮你建立题型迁移能力。比如"第k小元素"可以用堆做,也可以用快排的partition做,还可以在特定条件下用桶排序——这三者都指向同一个问题:"如何在部分有序中找到位置",理解了这一层,你遇到"数据流中位数"就不会慌。
5.3 怎么用"讲解"来检验自己是否真的会了
我在准备笔试的最后阶段,会做一件事:把每道做过的题,假装自己是在给一个刚入门的朋友讲题。不是念答案,而是从题目场景讲起,讲到为什么想到这个解法,再讲到边界和复杂度。如果发现自己讲着讲着卡住了,就说明这道题并没有真正内化。
这个方法特别适合找"假熟悉感"。很多人读完题解觉得"我看懂了",但实际上只是"我看懂了别人的思路",到了笔试场上根本用不出来。讲解的过程能逼你把每一个步骤的"为什么"都补上:为什么这里用栈而不是队列?为什么这个边界是<=而不是<?讲不清楚的地方,就是你的漏洞所在。
我还发现,把题目和热词里那些"版本答案"联系起来讲,效果特别好。比如热词里"数据结构排序算法""贪心算法""dijkstra算法"这些词,如果你能把每个词背后对应的问题、解法、复杂度、适用条件和反例都讲一遍,那你准备的程度已经超过大多数候选人了。毕竟面试官问的不是"你会不会背排序",而是"你懂不懂排序"。懂不懂,一讲就知道。
另外,我强烈建议在正式笔试前做一次完整模拟:限定时间、不开IDE辅助、全程手敲、做完整套题。这种模拟能帮你提前暴露所有环境适应问题——输入输出格式、时间分配策略、心态管理等。我见过很多基础不错的候选人,就是因为在正式考试里被某个输入格式卡了十分钟,导致后面大题没时间做。花一次模拟的代价,可以避免这样的惨剧。
最后再分享一个我自己的习惯:每次收到笔试卷,我会先花三分钟把全部题目扫一遍,按"绝对能拿分"、"可能要花点时间"、"可能做不出来"分成三档,然后按先易后难的顺序答题。别小看这个策略,它能让你在有限时间内拿满所有该拿的分。算法笔试从来不是"谁做出最难的那道题谁赢",而是"谁在有限时间内拿到最高的总分谁赢"。这也是我从出题人视角转成做题人视角后,最想提醒你的一件事。