最近重新把牛客2023模考(四模)的编程题整卷过了一遍,连着三个晚上,一套一套重新手写、跑样例、看题解,越琢磨越觉得这套卷子出的有点东西。如果你现在正处于秋招或者春招的备战期,想找一套和真实笔试手感最接近的题目来练手,四模是一个绕不过去的选择。它不是单纯把几道 hard 堆在一起炫技,而是按真实考试的难度梯度、题型占比、时间压力来设计,做起来很有考场感。这篇文章我不打算把题目答案抄一遍,而是想从这套题里提炼出真正能带走的东西:每类题型的考点拆解、做题时的思考顺序、容易翻车的细节,以及怎么把一次模考的成绩变成后续复习的依据。适合两类人看:一类是刚开始刷牛客,想通过模考摸底的同学;另一类是已经刷了不少题但一到模拟考就发挥不稳的同学。
1. 整体认知:四模这套题到底在考什么
1.1 模考序列里,四模的位置决定了它的难度曲线
牛客的模考一般会按时间线排好几场,四模这场的定位很微妙:它不是第一场那种“给你点信心”的摸底卷,也不是最后一场那种“难度拉满”的劝退卷。它处在整个备战周期的后半段,题型分布、难度比例、题干长度都已经非常接近真实笔试,适合用来做系统性的自我检测。
我当时刷完的最大感受是:这份卷子的选题很有代表性。编程题部分覆盖了笔试里出现频率最高的几类问题,比如数组模拟、字符串处理、链表操作、动态规划、贪心思想,没有出现特别偏门的数据结构,也没有为了难而难的竞赛题,整体出题风格就是“给你一个业务场景,让你用算法把它解决”。这一点很重要,因为真实笔试的编程题不会问你“红黑树怎么实现”,而是会把算法藏在题目描述背后,让你自己识别出来。
另外一点值得说的是时间压力。模考和平时刷题最大的区别就在于倒计时。平时做不出来可以慢慢想,但模考时一道题卡了半小时,后面所有计划就全乱了。四模的编程题数量控制在常规笔试范围内,但每道题都留了“陷阱”,如果你没有提前训练过时间分配,很容易在第二题上耗时过多,导致后面的大题根本没时间动笔。
1.2 和刷题单、每日一题最本质的区别
很多人习惯用题单刷题,比如今天做两道链表,明天做两道动态规划,这种方式的优点是知识体系清晰,缺点也很明显——你永远不知道换一个题型组合之后,自己的临场反应会是什么样。牛客模考这种整卷形式,逼你在一个时间段内面对不同类型的题目,随时切换思维模式。
举个例子,你可能在刷题单的时候动态规划正确率很高,但模考的时候,如果动态规划出现在第三题,前面已经做了两道偏模拟的题目,状态切换不过来,就很容易在状态转移方程卡住。四模这种混合题型设置,会把你这种“隐性短板”暴露出来。所以我的建议是:模考成绩高低都在其次,关键是把模考当成一次全流程演练,包括时间分配、心态调整、遇到不会的题怎么放弃,这些都是在题单里练不到的。
1.3 题型分布与难度梯度参考
根据我对四模卷子的复盘,编程题部分的题型和难度大概可以整理成下面这个表格,方便你对照自己的薄弱项:
| 模块 | 典型特征 | 常见考察方式 | 难度预期 |
|---|---|---|---|
| 数组与模拟 | 题干较长,规则明确,代码量大 | 双指针、前缀和、差分、区间合并 | 中低 |
| 字符串处理 | 思路简单,但边界条件多 | 字符统计、哈希表、滑动窗口 | 低 |
| 链表操作 | 考察对指针/引用的理解 | 反转、找中点、环形链表、合并 | 中 |
| 动态规划 | 状态设计困难,代码量小 | 线性DP、背包类、区间DP | 中高 |
| 贪心与数学 | 需要证明或直觉 | 排序后决策、最大最小值问题 | 中 |
| 搜索与图论 | 部分场次出现,占比不高 | BFS、DFS、连通分量 | 中高 |
从往年经验看,前两道题通常是“保分题”,只要细心就能过;中间一道会卡一部分人,考察某个经典模型的变体;最后一道则是用来区分高分的,往往需要综合运用两到三种技巧。四模也延续了这个规律。因此,刷这套题时不要只关注“做没做出来”,还要记录自己在哪一档题目上花了多少时间。做不出来不可怕,可怕的是习惯性在简单题上翻车。
2. 考点拆解:这套卷子里的四大高频模块
2.1 数组与模拟类:基础题稳拿分的关键
数组模拟类的题目在笔试里几乎是必考,四模也不例外。这类题通常文本量很大,描述一个复杂的规则场景,让你按规则一步一步处理。很多人一看到长题干就发慌,其实这类题反而是最好拿分的,因为它考的不是你懂多少高级算法,而是你能否把规则翻译成代码,并且不犯细节错误。
做模拟题的核心技巧是“拆步骤”。我在刷四模那道区间合并变体题时,第一遍直接上手写循环,结果越写越乱。后来学乖了,先把题目要求的操作按顺序列出来,比如:先读入所有区间,按左端点排序;然后维护当前区间左右边界;遇到新区间时判断是合并还是加入结果集。把逻辑理顺之后,代码就是按步骤一句一句翻译的事。
这里给一个典型的区间合并模板,几乎适用于所有类似场景:
def merge(intervals): intervals.sort(key=lambda x: x[0]) res = [] for interval in intervals: if not res or interval[0] > res[-1][1]: res.append(interval) else: res[-1][1] = max(res[-1][1], interval[1]) return res这个代码很短,但很多人都写不对,问题出在边界情况的处理上。比如区间是[1,4]和[5,6],这两个区间并不重叠,因为左端点 5 大于右端点 4,所以不能合并。但如果题目改成“端点相接也算连续”,那>就要改成>=,这就是看题要抠字眼的地方。模拟题翻车的重灾区,往往就是对“包含/不包含”“等于/大于”这些细节的把握。
2.2 链表问题:不要只会“背模板”
链表在四模里出现在中档题位置,考得很经典:不是单纯让你反转链表,而是和“两个链表找交点”“环形链表判断入口”这种变体结合。这种题平时刷题单的时候很容易被忽略,因为很多人觉得“链表嘛,不就是 next 指针动来动去”,结果到了笔试现场,一紧张就容易绕晕。
链表题最大的敌人不是算法,而是“引用丢失”。我在现场写代码的时候,经常犯的一个错误就是:没有先用临时变量保存下一个节点,就直接修改了当前节点的 next,导致后面的节点找不到了。比如反转链表的核心循环,很多人背了模板能写出来,但一旦题目改成“每 K 个节点一组反转”,就不知道如何把指针关系理清楚。
举一个容易犯错的例子,查找链表中间节点,用快慢指针:
def middle_node(head): slow = fast = head while fast and fast.next: slow = slow.next fast = fast.next.next return slow这段代码看起来简单,但fast and fast.next这两个条件缺一不可。只写fast.next,在链表节点数为偶数时会报错;只写fast,在节点数为奇数时又可能在最后一次循环越界。这些细节,就是“简单题”和“稳拿分”之间的差距。
我刷四模时还有一点体会:链表题一定要养成“画图”的习惯。笔试虽然没有草稿纸的限制,但多数在线 IDE 里你可以自己注释画图。把指针的变化画出来,比盯着代码空想要清晰得多。尤其是涉及 dummy 节点的题,画图之后思路会顺畅很多。
2.3 动态规划:状态定义决定成败
四模的动态规划题不算特别难,但足以过滤掉一部分没准备好的人。它考的不是那种“一看就知道是 DP”的题,而是把一个比较经典的问题包上了一层业务外衣。你需要先看穿它的本质,才能写出正确的状态转移。
以我复盘时遇到的一个“打家劫舍变体”为例,题面描述改成了一排房屋相邻不能选,但其实核心就是经典的线性 DP。做这类题,第一步不是急着写代码,而是想清楚状态代表什么。是dp[i]表示前 i 间房屋能偷到的最大值,还是表示“偷到第 i 间时”的最大值,这两种定义都会导致不同的转移方程:
# dp[i] 表示前 i 间房屋能偷到的最大金额 dp[i] = max(dp[i-1], dp[i-2] + nums[i])状态定义错,后面全错。很多人做 DP 题第一反应是套模板,而不是先理解题目语义,这是一个很普遍的误区。四模的动态规划题如果单纯从代码量看,只有十几行,但状态转移需要想清楚,这恰恰是拉开时间差距的地方。
2.4 字符串与哈希:性价比最高的模块
字符串和哈希的题目代码量小,但很能考基本功。四模里有一道关于“最长无重复字符子串”的变形题,用滑动窗口加哈希表,十分钟内能写完;但如果对窗口收缩的逻辑不熟,也很容易写出死循环。
我一直觉得这类题目是整个笔试里性价比最高的,因为实现思路相对固定,练几遍就能掌握,不像动态规划那样需要大量积累。做字符串题时,一个实用技巧是提前确定字符集范围。比如题目说明是 ASCII 字符,那就可以直接用长度为 128 的数组代替哈希表,速度更快,代码也更简洁:
def length_of_longest_substring(s): last = [-1] * 128 left = 0 max_len = 0 for i, c in enumerate(s): if last[ord(c)] >= left: left = last[ord(c)] + 1 last[ord(c)] = i max_len = max(max_len, i - left + 1) return max_len用数组代替字典,其实是一个很重要的思维转变。笔试环境里 Python 的字典虽然也能用,但常数时间开销更大,在卡常数的题目里可能就差那么一点。字符串处理题还要特别警惕 Unicode 字符和大小写问题,题目说“仅含小写字母”,你就别用大写转换去画蛇添足,按题面来。
3. 我用一套“翻译-设计-优化”流程复盘真实做题过程
3.1 拿到题目后,先做“关键词翻译”
很多人拿到编程题的瞬间就开始敲代码,这不是个好习惯。我在刷四模的时候总结了一个固定的动作:先把题目里的关键词翻译成算法名词。比如看到“连续子数组”,第一反应是“前缀和或滑动窗口”;看到“第 K 大”,想到“堆或快速选择”;看到“最小代价”,想到“动态规划或贪心”;看到“网格中的最短路径”,想到“BFS”。
这个“翻译”过程能帮你快速定位题目的考点,避免在错误的方向上浪费时间。比如四模里有一道题描述很复杂,核心就是“给定一组数,找两数之和等于目标值”,翻译出来之后就变成了一道经典的哈希表题。如果没做这层翻译,可能就会把它当成排序题去处理,绕一大圈。
我把常见的关键词对应关系整理成了一张速查表,方便你参考:
| 题面关键词 | 优先考虑的思路 |
|---|---|
| 连续子数组、窗口、最大/最小长度 | 前缀和、滑动窗口 |
| 两数之和、是否存在、配对 | 哈希表(空间换时间) |
| 第 K 大/小、Top K | 堆、快速选择、排序 |
| 最长公共、子序列、最优策略 | 动态规划 |
| 环、互通块、最短步数 | BFS、DFS、并查集 |
| 区间重叠、合并、覆盖 | 排序 + 贪心/扫描线 |
| 具有某种性质的排列 | 回溯、全排列剪枝 |
这个表不是万能公式,但它能帮你在一开始就站到大概率正确的方向上。别忘了这一步要结合题目本身的约束条件:如果数据范围很小,暴力可能已经够用;只有数据范围大,才需要考虑优化算法。
3.2 写代码前的“三步设计法”
我在现场练习时通常把做题过程分成三步,每一步都有明确的目的。
第一步是写边界条件。很多人习惯最后补边界,但我建议第一遍就把空输入、单元素、极端值这些情况写在注释里,然后让代码从一开始就兼容它们。比如处理链表时,永远先问自己:如果 head 是 None 怎么办?如果只有一个节点怎么办?想清楚再动手。
第二步是写暴力版本。不要一上来就追求最优解,先把能用的解法以最简单的方式写出来,确保逻辑正确。这一步的作用有两个:一是让你对问题本身有更具体的理解,二是万一后面优化时间不够,暴力版本至少能拿到部分分数。四模这种模拟考里,很多人的问题不是不会最优解,而是暴力版本都写不对。
第三步是回头优化。优化的方向通常有两种:要么把重复计算缓存起来,要么改变遍历顺序降低复杂度。以我复盘时遇到的一道题为例,暴力解法是两重循环,数据量一大就超时。这时候我想到用哈希表记录已经见过的元素,把查找从 O(n) 降到 O(1),整体复杂度从 O(n^2) 降到 O(n):
def two_sum(nums, target): seen = {} for i, val in enumerate(nums): need = target - val if need in seen: return [seen[need], i] seen[val] = i return []这个例子看起来简单,但它代表了面试中最常见的一类优化思路:用空间换时间。四模里有一道题几乎就是这个思路的变体,只是把“数字配对”换成了“字符是否满足某种关系”,如果你建立了“翻译”习惯,一眼就能看穿。
3.3 从暴力到最优:同一道题的三种写法
复盘四模时我发现,很多题的得分差异不在于会不会最优解,而在于能否根据数据范围选择合适的解法。以下面这道高频题为例,题目是“给定一个整数数组,返回所有和为 0 且不重复的三元组”。常见有三种层次的写法。
第一种是三重循环暴力枚举,时间复杂度 O(n^3),能解决 n 很小的情况。但在笔试环境下,这种写法基本拿不到满分,只能作为兜底方案。
第二种是先用两层循环确定两个数,然后用哈希表查找第三个数,时间复杂度 O(n^2),空间复杂度 O(n)。这种方案已经能应对大多数数据范围。
第三种是在数组排序后使用双指针,一层循环枚举第一个数,剩下两个数用双指针在有序区间内移动,时间复杂度 O(n^2),空间复杂度 O(1)。思路是固定第一个数,然后用 left 和 right 指针从两端向中间逼近,根据当前和与目标值的大小调整指针位置:
def three_sum(nums): nums.sort() res = [] n = len(nums) for i in range(n - 2): if i > 0 and nums[i] == nums[i-1]: continue left, right = i + 1, n - 1 while left < right: total = nums[i] + nums[left] + nums[right] if total < 0: left += 1 elif total > 0: right -= 1 else: res.append([nums[i], nums[left], nums[right]]) while left < right and nums[left] == nums[left + 1]: left += 1 while left < right and nums[right] == nums[right - 1]: right -= 1 left += 1 right -= 1 return res这段代码里最容易错的是去重逻辑。如果不去重,返回结果里会出现重复的三元组;去重去得不对,又会跳过正确答案。我在四模中看到不少人卡在去重上,说明这个细节值得单独花时间练一次。不要只看题解,动手敲一遍,卡住的地方就是你的薄弱点。
4. 实战排查:我在刷四模时踩过的坑
4.1 超时问题:先估复杂度,再写代码
四模的编程题环境对运行时间有明确限制,我用 Python 刷的时候最大的感受是:Python 虽然写起来快,但同样的算法复杂度,执行效率比 Java 和 C++ 要慢不少。所以写代码之前一定要估算复杂度,避免做无用功。
一个粗略的参考标准是:一秒钟大概能跑一千万到五千万次简单操作。如果你写了一个 O(n^2) 的算法,而数据范围 n 是 10^5,那基本不可能通过。这时候你需要在写代码之前就判断,这个量级应该用 O(n log n) 或 O(n) 的算法,而不是赌数据弱。
我遇到超时问题后,通常先检查这几个点:有没有不必要的重复计算;有没有可以用哈希表替代的线性查找;循环里有没有调用耗时过大的函数。Python 里的in操作对于列表是 O(n),对于集合和字典是 O(1),很多人忽略了这个区别。如果你在一个大循环里写了if x in list,很可能就是超时的根源。还有一个经常被忽视的细节是输入输出,数据量大的时候,建议用sys.stdin.readline代替input(),用列表收集结果再统一输出,避免频繁调用 print。
4.2 边界条件:最容易丢分的隐形杀手
复盘四模的时候,我把每一道题因为边界条件丢分的点整理成了一个清单。每次交代码之前,对着这个清单过一遍,能避免很多无谓的失败。
| 场景 | 必须检查的内容 |
|---|---|
| 链表 | 头节点为空、只有一个节点、两个节点、环 |
| 数组 | 空数组、长度 1、元素全相同、升序降序 |
| 数字运算 | 负数、0、最小值最大值、整数溢出 |
| 字符串 | 空串、单字符、全空格、大小写混合、Unicode |
| 二叉搜索/分治 | 区间只有一个元素、目标值不在区间内、重复元素 |
这些边界条件不需要背,但需要形成条件反射。比如看到树的题目,先问一句“如果是空树怎么办”;看到数组二分的题目,先确认“left 和 right 的初始值是否覆盖了所有情况”。四模里有一道题,我明明算法设计得没有问题,但因为没有处理空输入,被判了运行时错误,第一次提交直接零分。这种丢分非常可惜。
还有一个技巧是:不要只跑题目给的样例,要自己构造几个边界样例。比如最大值、最小值的输入,恰好重叠的区间,刚好形成环的链表等等。你在本地把这些场景测一遍,比提交后反复试错要高效得多。
4.3 Python 环境下的输入输出与调试技巧
很多同学平时刷题用的 IDE 和牛客笔试环境不是一回事,到了模考才会发现输入输出也不顺手。四模的编程题有一些输入是一行一行读入的,如果对input()的机制不熟悉,就容易读到空行。我建议从一开始就适应标准输入模式,特别是用到二维矩阵的时候。
一个常见的输入处理模板是这样的:
import sys def main(): data = sys.stdin.read().split() # 按需解析 idx = 0 n = int(data[idx]); idx += 1 arr = [] for _ in range(n): arr.append(int(data[idx])); idx += 1 # 你的逻辑 print(" ".join(map(str, result))) if __name__ == "__main__": main()这种一次性读入再 split 的方式,比一行一行读入要快,而且不容易被换行符干扰。在牛客这类在线评测平台,提交答案时如果因为输出格式不对被判错,也不会给你看具体原因,所以养成良好的输出习惯也很重要。比如要输出一个列表,很多人会写print(result),结果输出的是 Python 的列表语法,而不是题目要求的空格分隔格式,提交上去直接错完。
关于调试,我强烈建议不要把 print 语句留在最终代码里。笔试环境里频繁 print 会大大拖慢运行时间,尤其在最坏情况下可能导致超时。如果非要用 print 调试,提交之前一定要注释干净。
5. 怎么把四模经验带到真实笔试里去
5.1 模考成绩不等于真实水平,但能定位问题
四模结束后,我看到很多人在讨论分数。我个人觉得,模考分数本身参考意义不大,因为每家公司的笔试出题风格、难度、时间都不同,一次模考的绝对分数说明不了太多。真正有意义的是“你在哪些类型的题目上失分”“你花在每道题上的时间是否合理”“面对不会的题时你的应对方式是什么”。
我做完四模后会做这样一个复盘:统计每道题的状态,一共分成四类——快速 AC、卡了很久 AC、没做出来但看了题解能理解、完全没思路。四类题目对应的复习策略完全不同。前两类说明你已经掌握,保持手感即可;第三类是最有价值的突破口,多看几遍题解,重新默写一遍,基本就能内化;第四类则需要回到基础知识,先把相关章节学扎实再回来刷。
这种分类方式比单纯盯着分数要实用很多。我见过一些人模考拿了不错的分数,结果真实笔试翻车,原因就是对“卡了很久才 AC”的题目没有重视,以为做出来了就代表会了。实际上,笔试是有时间压力的,一分钟和半小时做出来,训练效果天差地别。
5.2 冲刺阶段的刷题计划建议
如果你离真实笔试还有两到三周,我建议把牛客模考的题单当作主线,而不是从头到尾刷几千道题。具体安排可以这样:每天固定花半小时做一道完整的编程题,严格按照笔试时间要求自己,然后留半小时复盘。复盘的重点不是看题解,而是先对照自己的代码,找出是哪一步的想法出了问题。
一周可以安排一次完整的模拟考,用四模或者其他模考卷,把整个流程走一遍,包括心理状态调整和做题顺序选择。平时刷题时也可以刻意训练一个能力:在一道题上卡住超过 20 分钟,立即跳到下一题。很多笔试失败不是因为不会做,而是因为时间分配失衡,简单题没来得及做,难题又没做出来。
如果你用 Python 刷题,还要注意基础语法的熟练度。比如列表推导式、字典的 get 方法、Counter 的用法,这些在笔试中能帮你省下很多时间。不少同学会用 Python 入门编程,先练一些类似“python2025.3一级编程题题目”那种基础题,把语法和环境跑通,再进到牛客模考这类综合环境中检验,这个进阶路径其实很平滑。基础越熟练,越能把注意力集中在算法本身,而不是边写边想语法。
5.3 从模考到真实笔试的心态与策略调整
真实笔试和模考还有一个很大的不同:真实笔试的题目范围更不可控,偶尔会出现完全没见过的题型。这时候最怕的不是不会,而是心态崩了。我在四模中训练出的一个技巧是:遇到没见过的题目,先强制自己花三分钟把题目读两遍,把输入输出示例手算一遍,很多时候思路就在这个过程中冒出来了。
如果三分钟后还没有任何思路,就先跳过,把后面能拿的分拿了再说。笔试的计分方式往往不是按题序给分,先做容易的题,再回头啃硬骨头,是性价比最高的策略。另外不要因为一道题样例能过就觉得万事大吉,样例能过只是最低要求。我复盘四模时发现,很多题目的样例都不是很刁钻,你需要自己构造一些极端的输入,才能真正判断代码是否可靠。
关于做题顺序,我个人的偏好是先做字符串和模拟题,再做链表和贪心,最后做动态规划和搜索。因为字符串和模拟题通常最容易进入状态,能帮你快速建立信心;动态规划这类需要深度思考的问题,放在后面时间充裕时做,即使卡住也不至于影响整场节奏。当然这个顺序因人而异,但原则是一致的:先做有把握的,再挑战不确定的。
6. 复盘之外:我沉淀下来的几个刷题习惯
四模刷完之后,我最大的收获反而不是某道题的解法,而是几个可以复制到任何一套题上的习惯。在这里分享给你,尤其是第一个习惯,我觉得对提升刷题效率帮助最大。
第一个习惯是建立错题类型标签,而不是简单地记录题目名字。比如“这道题我错在边界条件”“这道题我错在状态转移方程写反了”“这道题我错在题意理解偏差”。标签越具体,后续复习越有针对性。我在刷四模时给每道错题贴了标签,然后发现自己在“区间边界”上的错误率特别高,于是集中找了几道相关的题目专项训练,效果比零散刷题好很多。
第二个习惯是尽量用纸笔模拟一个示例的运行过程。不要一上来就写代码,先在草稿纸上用一个小数据把算法流程走一遍,标记出每个变量的变化。这个方法对链表、指针和递归尤其有效。我在做四模的链表题时,先画了链表反转每一步的指针指向,代码写起来几乎没有卡壳。
第三个习惯是每次提交之前做一遍“三分钟代码审查”。第一分钟看边界条件,第二分钟看复杂度和输入输出,第三分钟检查有没有调试用的残留代码。这套流程听起来简单,但它能在关键时刻救你一把,避免很多因为粗心导致的零分。
如果你现在还处于“刷题刷了不少,但一到模考就发挥不出来”的阶段,建议不要急着再刷新题,而是把四模这套卷子重新拿出来,按我上面说的方法重新走一遍。重点不是把题目背下来,而是把做题的思路、时间分配、边界审查这一整套流程练熟。等到真实笔试的时候,你会发现自己对题目的反应速度和处理方式,已经和之前完全不一样了。