
算起来2018年牛客模考第一场A套我前后刷了三遍。第一遍是在当时准备秋招考前一周拿它练手结果有一道模拟题卡了很久印象特别深。第二遍是第二年帮学弟改简历顺便带他复盘这套题发现里面的很多思路放到现在依然不过时。第三遍是最近重新把所有代码用自己的语言整理了一遍写这篇博客的素材也就是这么来的。这套牛客编程题的价值不在于“有多难”而在于它的题目结构非常像真实校招笔试考察的都是大家最常用、最该掌握的算法基本功字符串处理、数组操作、经典动态规划、二分查找边界处理。无论你是刚开始准备校招还是已经刷了一段时间想检验水平拿这套题做一次摸底都很合适。下面我按照我的复盘思路把这套题分成几个维度拆开讲重点放在解题思路和踩坑细节上。1. 这套模拟卷在考什么先看清整体设计1.1 题型构成与能力地图牛客的模考一般不会刻意出偏题怪题它的作用是“模拟真实笔试环境”所以题目结构会尽量贴近各大厂笔试的风格。这套2018年一模A套我印象中大致包括这样几类基础输入输出、字符串处理、数组排序与去重、简单模拟、二分查找、动态规划入门、数学思维题。对刚开始准备笔试的朋友来说这套题最大的价值是能帮你快速定位自己的短板。如果你能把这套题的每道题都做到“不看题解、一次通过、边界条件齐全”那说明你已经具备应对大多数基础笔试的能力。如果你有几道题卡壳也没关系正好借这个机会把对应的专题补起来。我从里面抽了几道代表性比较强的题按题目特征做了一张能力对照表方便大家对照自检题目特征核心考点常见失分点字符串循环移位字符串操作、取模运算忘记处理 k 大于字符串长度的情况数组去重保持原顺序哈希表、计数思想用排序后再去重导致顺序错乱寻找数组中出现次数超过一半的数众数思想、抵消算法不检查众数是否存在直接输出结果连续子数组最大和动态规划、贪心初始化错误全负数时结果不对二分查找目标值第一次出现的位置二分边界处理死循环、index越界1.2 为什么这些题型长期不变很多刚开始刷题的朋友会问这都过去这么多年了刷以前的题还有用吗我的观点是基础算法题型的“骨架”变化很小。虽然每年出现的新题在包装上越来越花哨比如把题目套上一个“云平台调度”的外壳或者用“游戏组队”的场景包装但剥开这些外壳内核还是这些经典内容。字符串的索引操作、数组的原地修改、动态规划的状态转移这些是计算机程序设计最底层的思维训练。2018年的牛客模考是这样考到了2025年现在的牛客编程题依旧是这样考。你把这些底层能力练扎实了不管题目外壳怎么变你都能迅速拆解出考点。刷这套老题不是为了预测新题而是为了训练一种稳定的问题拆解能力。2. 字符串和模拟题的复盘最容易拿分也最容易丢分2.1 字符串循环移位一个取模就能解决的题字符串处理是整套题里占比较高的一块。其中“字符串循环左移”这类题很典型题目会要求你把一个字符串循环左移 k 位比如abcdef左移 2 位得到cdefab。很多人第一反应是每次移动一位循环 k 次。这种暴力做法在大字符串时会超时正确做法是先对字符串长度取模然后直接切片拼接def left_rotate_string(s: str, k: int) - str: if not s: return s n len(s) k k % n return s[k:] s[:k]这里有一个非常典型的坑k可能大于字符串长度甚至等于 0。如果不先取模当 k 是 n 的整数倍时切片会拿到完整字符串结果虽然没错但代码逻辑上其实暴露了你对“循环”这个概念理解不到位。取模运算的意义就是让 k 落到[0, n-1]区间内这才是循环移位的正确语义。在 C 里可以用std::rotate做但笔试现场我一般不推荐依赖这个库函数因为很多基础题考查的就是你手动实现切片和交换的能力string leftRotateString(string str, int n) { int len str.length(); if (len 0) return str; n n % len; reverse(str.begin(), str.begin() n); reverse(str.begin() n, str.end()); reverse(str.begin(), str.end()); return str; }三次反转的思路很巧妙反转前 n 个反转后 len-n 个再整体反转。面试的时候如果能把这种写法讲给面试官听印象分会好不少因为它说明你不只是会用 API还理解字符串反转的内部原理。2.2 数组去重别一股脑先排序数组去重是各大笔试平台出现频率极高的题但这套牛客模考里的去重题加了一个限制条件保持原有顺序。我看到不少朋友一上来就排序排序确实能去重但会把原始顺序搞乱。保持顺序的去重最自然的做法是用哈希表记录“已经出现过的元素”def remove_duplicates(nums): seen set() result [] for num in nums: if num not in seen: seen.add(num) result.append(num) return result这套解法的时间复杂度是 O(n)空间复杂度是 O(n)。如果有额外要求“原地修改”你可以改成双指针写法用i维护“结果数组末尾”用j遍历原数组。这里我特别提醒一个点笔试判题系统对输出格式要求很严有时候多一个空格都会判错。如果你是用print( .join(map(str, result)))输出不会有多余空格但如果用循环逐个输出最后一格空格一定要处理掉。很多人在大数据集上能通过反而在输出格式上被扣分非常可惜。2.3 模拟题的基本功把规则翻译成代码模拟题在牛客这套模考里也占了一席之地。模拟题通常是给你一段“题目自己设定的规则”让你原样实现。比如棋盘上有个棋子按某种规律移动或者有几个进程按时间片轮流执行。做模拟题的核心不是算法而是“把自然语言规则精确翻译成代码逻辑”。我踩过的坑是题目里的一句话代码里可能对应三个 if 分支漏掉其中一个样例过不了但往往只差一个很小的条件。所以我在做模拟题时有一个习惯先在草稿纸上把规则拆成 1、2、3、4 步再动键盘。如果题目说“当 A 和 B 同时满足时执行 C否则执行 D”那代码里一定要显式写出if A and B和else不要用两个独立的 if 去表示互斥关系。有一种更隐蔽的问题模拟题里经常涉及“轮数”和“次数”的换算。比如每个回合要做两次操作问第 N 轮结束时的状态。很多人的错误是把轮数当次数循环里少走了一倍。我的建议是模拟题写完后拿题目给的示例手动跑一遍对照着看每一步状态是否一致。这一步确实费时间但能拦住大多数逻辑错误。3. 进阶题型的解题套路动态规划与二分3.1 最大连续子数组和最经典的 DP 入门题这套模考里应该有动态规划而且我印象中考的就是最经典的那类。“给定一个整数数组求连续子数组的最大和”比如[-2, 1, -3, 4, -1, 2, 1, -5, 4]的最大和是4 (-1) 2 1 6。这个题的思路说难不难说简单也不简单。核心是一个状态定义用dp[i]表示“以第 i 个元素结尾的连续子数组的最大和”。那么状态转移方程就是dp[i] max(dp[i-1] nums[i], nums[i])这个转移方程的含义是要么把当前元素接在前面的子数组后面让前面的和继续为当前服务要么前面的和已经是负数接了反而更小不如从当前元素重新开始一个子数组。实现上可以优化成滚动变量只用两个变量交替取值避免开一整个数组def max_subarray_sum(nums): if not nums: return 0 cur nums[0] res nums[0] for i in range(1, len(nums)): cur max(cur nums[i], nums[i]) res max(res, cur) return res我最开始刷这个题时犯了一个错误把res初始化为 0。当数组全是负数时比如[-1, -2, -3]正确答案应该是-1但res初始化为 0 会输出 0。这是非常经典的边界错误也是很多人面试时被追问的点。正确的初始化方式是等于nums[0]循环从第二个元素开始。3.2 二分查找边界条件比你想的更阴险二分查找这类题在牛客这套模考题里也有体现。如果题目是“给定一个有序数组和一个目标值返回目标值第一次出现的位置如果不存在返回 -1”那就要用到“找左边界”的二分写法。最常见的错误写法是int left 0, right n - 1; while (left right) { int mid (left right) / 2; if (nums[mid] target) { right mid - 1; } else { left mid 1; } } return left n nums[left] target ? left : -1;这里我把left right和right mid - 1写在一起是因为找左边界时我们要“把右指针往左压”。很多教程喜欢写right mid那就必须配合while (left right)使用两种写法都对但千万别混用。混用的典型症状是死循环或者数组下标越界。还有一个细节是计算mid时最好写成left (right - left) / 2不要直接写(left right) / 2。虽然现在的编译器和解释器大多不会溢出但有些题目的数组长度很大写了left right有潜在溢出风险。这属于“写代码的好习惯”在笔试里不一定暴露问题但面试官可能会问。我复盘这套题时发现二分查找的难点其实不在“找得到”而在于“找不到时返回值是否正确、数组只有一个元素时是否正确、目标值比最小元素还小时是否正确”。把这些边界想清楚这类题基本就稳了。3.3 双指针技巧的隐藏考点除了二分这套题里还可能有双指针。最典型的场景是“在有序数组里找两个数使它们的和等于目标值”。很多人想到的是两层循环O(n^2)但用双指针可以降到 O(n)def two_sum_sorted(nums, target): left, right 0, len(nums) - 1 while left right: cur nums[left] nums[right] if cur target: return [left, right] elif cur target: left 1 else: right - 1 return [-1, -1]双指针的核心在于利用“数组有序”这个属性让指针每次移动都能缩小搜索范围。如果题目要求返回元素值而不是下标那思路更简单如果要求返回所有组合且不能重复那还得在移动指针时跳过重复元素。这里有个经验如果题目数据范围比较大比如 n 达到 10^4 以上O(n^2) 基本会超时这时要条件反射地想到双指针或者哈希表。牛客测评系统的时间限制一般在 1 到 2 秒Python 写 O(n^2) 在 n 比较大时会非常吃亏。我推荐大家在平时刷题时就要有“估复杂度”的意识拿到题先看数据范围再决定用哪个复杂度级别的算法。4. 容易翻车的边界条件和细节这些坑我都替你们踩过了4.1 输入输出格式的“隐形杀手”后台测评和本地 IDE 最大的区别是测评系统只看你的输出不看你的调试信息。很多人本地跑得好好的一提交就“答案错误”十有八九是输入输出格式有问题。牛客的输入通常是多组数据或者特殊格式。比如有些题第一行是测试用例组数 T接下来 T 行才是数据。如果你只处理了一组数据那 T 大于 1 时就会出错。正确做法是读完整输入按 T 循环处理。在 Python 里我习惯这样处理import sys def solve(): data sys.stdin.read().strip().split() if not data: return idx 0 t int(data[idx]) idx 1 out [] for _ in range(t): n int(data[idx]); idx 1 arr list(map(int, data[idx:idxn])) idx n out.append(str(process(n, arr))) sys.stdout.write(\n.join(out)) if __name__ __main__: solve()统一读入后用索引按顺序取数据比多次input()更稳尤其是数据量很大的时候一次read()比多次input()快得多。4.2 数组越界和空数组问题数组越界是笔试里最常见的运行时错误。做这类题时我给自己定了一个检查清单函数入口先判断数组是否为空访问nums[i1]时确认 i 不会等于 n-1二分查找的 mid 计算是否可能越界递归时是否漏了终止条件。在 C 中越界访问不会立刻崩溃而是可能读到内存里的脏数据导致结果“貌似正常但就是错”。这类 bug 最恶心因为在本地跑小数据可能发现不了。我建议是在自己的测试用例里刻意增加边界情况比如数组长度为 1、目标值不存在、数组全部相同等。4.3 忽略题目给出的数据范围我发现很多刷题的人不看数据范围拿到题直接写暴力解法。这种做法在数据量小的时候没问题但牛客模考的设计就是为了贴近真实笔试数据范围都是精心设计过的暴力解法大概率过不了大数据集。举个例子如果题目说 n 10^5那么你的算法复杂度最好是 O(n log n) 或 O(n)。此时如果你写 O(n^2) 的冒泡或双重循环即使思路正确运行时间也会超限。反过来如果 n 100那 O(n^2) 完全没问题暴力反而更快不容易写错。所以做题的第一步不是写代码而是看数据范围用数据范围反推可接受的复杂度。这里可以给大家一张速查图帮助快速判断数据规模可接受复杂度n 10O(n!)n 20O(2^n)n 100O(n^3)n 1000O(n^2)n 10^5O(n log n)n 10^6O(n)n 10^7O(log n) 或 O(1)这个表不需要死记心里有个大概就行。真实考试时时间限制通常是 1 秒Python 大概能跑 10^7 次简单运算C 能跑 10^8 到 10^9 次。超出这个量级就该考虑优化了。5. 从 2018 年模考到 2025 年这类题为什么会一直存在5.1 考点迭代背后的变与不变我近两年帮人看简历、做模拟面试发现牛客上的编程题风格确实有变化大厂笔试里越来越多地出现“业务场景包装”的题目比如模拟日志解析、设计一个简化版缓存、处理海量数据求 TopK。这类题目看起来很“工程化”但拆到底层还是二叉树、哈希表、堆、滑动窗口这些基础结构。为什么基础题型的生命力这么强因为面试官真正想考察的不是你背了多少题而是你在压力下能不能快速把一个模糊的问题转成清晰的算法。字符串、数组、DP、二分这四个大类几乎是所有算法的载体。你把这几块吃透了遇到任何包装过的新题都能快速“脱衣服看本质”。顺便提一下最近很多人问我关于 2025 年 3 月 Python 一级编程题的事。那次的重点依旧是基础的输入输出、条件判断、循环和简单字符串操作。可以看出不仅是牛客这套老模考就连现在的等级考试都非常重视基本功。所以别老追求刷难题偏题把基础题型练到条件反射才是效率最高的备考方式。5.2 如何用这套 2018 年模考训练实战手感光看题解没用编程能力是在键盘上敲出来的。我建议你用这套模考做一次“全真模拟”严格遵守以下流程第一设定时间。按真实笔试标准给自己限时 90 到 120 分钟一口气做完这套题中间不查资料、不暂停、不跟人讨论。这一步的目的是让你提前适应笔试题量带来的心理压力。第二统一评测。不要自己肉眼判断输出去。把代码提交到牛客题库让评测系统告诉你到底过了几个测试点。很多题你本地感觉“能出结果”但实际漏了边界条件评测系统一测就现原形。第三错题归档。每道错题都要做二次复盘是算法思路错了还是边界漏了还是输入输出格式问题。最好建一个错题文档把每个错误的代码片段、错误原因、正确写法都记录进去。这样做一个月以后你会惊讶地发现自己重复犯的错误类型高度集中。第四限时重做。隔两周后把原来做错的题再限时重写一遍不许看旧代码。如果还能一遍通过这个知识点才算真正掌握了。我给很多人推荐过这种方法反馈都很好。它比盲目刷 300 道题有效得多因为你是在“高质量重复”而不是在“低水平熟练”。6. 实测最有用的三个刷题习惯前面聊了这么多题最后想分享三个我在实际操作中觉得最有用的刷题习惯也算是对这套牛客模考的复盘总结。第一个习惯是“先写注释再写代码”。每次实现算法题之前我先用注释把核心逻辑写出来比如“dp[i] 表示以 i 结尾的最大子数组和”“left 指向结果数组末尾”“这里要取模是因为 k 可能大于 n”。这样代码写到一半不容易乱遇到复杂逻辑也能回头对照。很多人做题时思路是乱的就是因为脑子里没有一个清晰的“步骤清单”注释就是帮助你建立清单的方式。第二个习惯是“用极端用例测自己的代码”。提交前至少测四组数据空数组、只有一个元素、全正数、全负数、最大规模随机数据。不要觉得这浪费时间一道题多花三分钟测边界能帮你减免好几次提交失败。尤其是全负数数组我印象中不知道多少人栽在最大连续子数组和的初始化上。第三个习惯是“别急着看题解”。一道题如果 20 分钟没思路正常人都会想翻题解。我自己的建议是再给自己 10 分钟用最笨的办法先尝试暴力解哪怕复杂度很高先让它能跑出正确结果。暴力解的意义在于让你理解题目的“原始规则”很多时候在写暴力的过程中你会突然发现优化方向。如果直接看题解你就跳过了一段很重要的思考过程。我在实际使用这套 2018 年牛客模考时的体会是它就像一面镜子能照出你哪些地方是真的熟练哪些地方只是“看着眼熟”。所以不管你现在处于刷题阶段的哪一个位置我建议你找个完整的时间段安静地把这套题从头到尾做一遍。做完之后再去补短板效率会高很多。刷题这件事没有太多捷径把经典题型反复练到“肌肉记忆”你在真正的笔试里才能有底气去应对那些没见过的包装。