Google 2012笔试题拆解:从二分查找、动态规划到LRU的算法内功

发布时间:2026/8/29 14:23:27
Google 2012笔试题拆解:从二分查找、动态规划到LRU的算法内功 如果要我给程序员圈子里流传的面试题排个座次Google 2012笔试卷绝对能进前三。它不只是一张卷子更像是一份算法审美的标本题目数量不多但每一道都直指底层能力没有一道是刷题App里随随便便就能背出来的。哪怕你是2025年才入行的开发者只要你准备面大厂这套老卷子依然值得认真过一遍。这套题能帮你解决什么问题说白了就两件事第一检验你写代码的内功到底扎不扎实第二提前感受一下顶级公司是怎么用一道题把会背题和真会写代码区分开的。适合谁看准备算法面试的在校生、跳槽大厂的在职工程师还有那些带团队、需要出笔试题的Tech Lead都能从里面挖出点东西。1. 这份卷子为什么到现在还有人翻出来做很多人一听2012年就觉得过期了。恰恰相反Google的笔试题从来不是为了考某个新框架、新语言特性它考的是那些十年后依然成立的底层逻辑。语言每年都在换但二分、动态规划、贪心、树和图的遍历这些计算机科学的地基从来没变过。1.1 题目的选择思路与区分度2012年的这套卷子题目量不大但区分度做得非常狠。我印象最深的一点是它很少出模板题而是喜欢把经典模型的壳子换掉让你认不出它的真身。比如它不会直接问请二分查找一个数而是把二分藏在找两个有序数组的第K小元素这种场景里。这种出题思路背后其实有一个很实际的需求筛选掉只会背模板的候选人。一个候选人如果只是刷过几百道题但没真正理解时间复杂度的推导、没有形成自己的解题框架遇到换壳的题目就会卡住。而真正写过复杂系统、处理过性能瓶颈的工程师看到这类题会下意识地把问题拆开先想暴力解再想怎么剪枝、怎么降复杂度。Google要的不是做题家是能解决未知问题的人。1.2 到底在考什么四种核心能力我把这套卷子的考察点归纳成四层你在其他公司面试里也会反复撞见能力层考察内容对应的题目特征输入分析是否能快速识别数据规模、边界条件题目往往不给明确的数据范围要自己推断模型抽象能否把实际问题翻译成已知算法模型题面里大量使用迷宫盒子零件等伪装复杂度意识是否清楚暴力解的上限在哪、该怎么优化部分题明确要求比O(N^2)更快工程化输出写的代码是否边界完整、可读、可扩展不只判对错还会看代码风格和命名这四层不是割裂的而是层层递进。我自己出面试题的时候也会按照这个框架来设计先给一道能通过输入分析筛掉一批人的题再在题目里埋一个模型抽象的坑最后用工程化输出看代码习惯。Google的题高明在它把这些层次压缩在一两道题里让你连这题考什么都要想一会儿。2. 三道值得反复咀嚼的原题拆解这套卷子流传出来的题目里有几道几乎成了行业符号后来被LeetCode等平台收录后也一直维持着很高的难度评级。我挑三道最有代表性的逐层拆开讲。2.1 两个有序数组的中位数一眼二分但边界让你怀疑人生题目背景大致是给你两个已经排好序的数组长度分别是m和n要求用O(log(mn))的时间找到它们合并后的中位数。这是Google系题目里极具代表性的一道考点完全在对数时间复杂度这六个字上。最直觉的做法是双指针归并O(mn)面试官不拦你但会追问一句能不能更快到这一步就进入了二分的世界。核心思路是在短数组上二分切割位置。假设我们在第一个数组的第i个位置切一刀那么在合并后的整体里第二个数组的切割位置j必须满足 i j (m n 1) / 2。然后检查边界nums1[i-1] nums2[j] 且 nums2[j-1] nums1[i]满足说明切割位置找对了。调整i的二分方向如果nums1[i-1] nums2[j]说明i切大了要往左移反之往右移。def find_median_sorted_arrays(nums1, nums2): if len(nums1) len(nums2): nums1, nums2 nums2, nums1 m, n len(nums1), len(nums2) left, right 0, m total_left (m n 1) // 2 while left right: i (left right 1) // 2 j total_left - i if nums1[i - 1] nums2[j]: right i - 1 else: left i i left j total_left - i nums1_left_max nums1[i-1] if i 0 else float(-inf) nums1_right_min nums1[i] if i m else float(inf) nums2_left_max nums2[j-1] if j 0 else float(-inf) nums2_right_min nums2[j] if j n else float(inf) if (m n) % 2 1: return max(nums1_left_max, nums2_left_max) return (max(nums1_left_max, nums2_left_max) min(nums1_right_min, nums2_right_min)) / 2这段代码里最值得研究的是循环条件left right和i (left right 1) // 2这个上取整。为什么要上取整因为当m0或者边界情况时避免i陷入死循环。我当年第一次写就因为这里用了下取整导致left永远无法收敛到正确位置调试了一下午。这道题告诉我一个很深的道理二分不是模板是思维。你用left right还是left right取决于你要找的是第一个满足条件的位置还是最后一个满足条件的位置。Google敢把它放在笔试卷里不是指望所有人AC而是想看你在边界上的敏感度。2.2 鸡蛋掉落从暴力递归到动态规划再到数学题目描述是经典的你有一栋N层的楼和K个鸡蛋鸡蛋在某个楼层F以下扔不会碎在F及以上会碎求最坏情况下确定F的最小扔递次数。这道题在当年的卷子里几乎是压轴的存在。它每一层优化都代表一种算法思想的跃迁。最暴力的递归是在每一层扔鸡蛋碎了就去楼主楼下搜索没碎就去楼上搜索取两种情况的最大值再加一。这个复杂度是O(N^2)级别的完全不可行。第二步优化是动态规划。定义dp[K][N]表示K个鸡蛋、N层楼的最小尝试次数。状态转移方程是dp[K][N] 1 min(max(dp[K-1][x-1], dp[K][N-x]))其中x遍历1到N。这个式子讲的是选择在x层扔如果碎了剩下K-1个鸡蛋要搜索x-1层如果没碎K个鸡蛋要搜索N-x层。最坏情况取两者较大值而我们要选一个x让这个最大值最小。时间复杂度是O(K*N^2)仍然太大了。再进阶一层把思路反转给定K个鸡蛋和t次尝试最多能覆盖多少层楼这就变成了一个逆向的动态规划递推式是 dp[t][K] dp[t-1][K-1] dp[t-1][K] 1。等右边的值达到N时t就是答案。时间复杂度降到了O(K * t)而t本身很小因为K2时t的量级在O(sqrt(N))。网上广为流传的二分法解法本质上是一种贪心策略每次尝试都把搜索区间尽量均分。但注意这个只在鸡蛋足够多、不易碎的时候才近似正确。如果K1那就只能从一楼一层层往上试任何二分都是错的。这道题最大的收获不是记住递推式而是学会把问题倒过来想——正向DP算不出来时把状态变成固定资源能覆盖多少范围往往能柳暗花明。2.3 LRU缓存数据结构的综合应用题那道设计一个LRU缓存的题目放到今天依然是系统设计面试的高频考点。题目要求实现一个get和put都是O(1)的缓存容量满了就淘汰最久未使用的key。这不是纯粹的算法题而是一个数据结构选型的综合题。O(1)的get提示你用哈希表O(1)的淘汰提示你需要一个能快速调整顺序的结构。数组做不到中间删除O(1)单纯链表做不到随机访问O(1)答案只能是哈希表加双向链表。class DLinkedNode: def __init__(self, key0, value0): self.key key self.value value self.prev None self.next None class LRUCache: def __init__(self, capacity): self.capacity capacity self.cache dict() self.head DLinkedNode() self.tail DLinkedNode() self.head.next self.tail self.tail.prev self.head def get(self, key): if key not in self.cache: return -1 node self.cache[key] self.move_to_head(node) return node.value def put(self, key, value): if key in self.cache: node self.cache[key] node.value value self.move_to_head(node) else: node DLinkedNode(key, value) self.cache[key] node self.add_to_head(node) if len(self.cache) self.capacity: removed self.remove_tail() del self.cache[removed.key]这里的工程细节不少。头尾哨兵节点的设计避免了删除节点时的空指针判断move_to_head这个操作本质上是两步把节点从链表里摘出来再插到头部。很多人在笔试时用单链表实现结果删除尾部节点时要遍历找前驱直接O(N)就被判死了。这类题在Google试卷里出现的意义是考察候选人设计数据结构的能力。真正的系统里这种哈希表双向链表的组合随处可见比如操作系统的页面置换、数据库的Buffer Pool。笔试只是把工程知识剥掉外框露出最核心的骨架。3. 像Google工程师一样想问题解题的思维链路题目会变但解题的思维链不会。训练这套卷子最有价值的副产品就是养成一套稳定的解题方法论。我会在模拟面试时用这套流程现在也分享出来。3.1 把题目翻译成已知问题看到任何一道题前五分钟不要碰键盘先拿笔在草稿纸上做翻译。比如两个有序数组的中位数翻译过来就是在一个排好序的序列中找到第K小的元素鸡蛋掉落翻译过来是在二元搜索空间里找一个最优的探测策略。翻译完之后你脑中的算法库才能被激活。很多同学直接上来写代码写到一半发现卡住了再回头琢磨题意时间全浪费了。我的习惯是先跟面试官或对自己用一句话复述题意所以我理解这道题是让我在O(log n)时间内找到两个数组的分割点对吗这一步能筛掉题面里百分之八九十的误解。3.2 先写暴力解再谈优化Google的面试官不会因为你一开始给了暴力解就否定你。相反他们更在意你能否从暴力解里看到优化空间。我强烈建议按这条路线走暴力解 - 分析瓶颈 - 剪枝或换数据结构 - 最终解法。举鸡蛋掉落为例暴力递归的瓶颈在于重复子问题的计算所以引入记忆化搜索记忆化搜索的空间开销大所以改成自底向上的DPDP的时间复杂度还是高所以反转思路用覆盖层数做递推。每一个优化步骤都有明确的动机而不是我觉得这里能用一个DP。这套方法论在真实工作中同样好用。线上服务慢先写一个能工作的版本监控耗时再定位到具体瓶颈再决定是加缓存还是改数据结构和算法。直接上手最优解往往容易过度设计。3.3 现场沟通比写出最优解更重要笔试和接下来的面试之间是连续的。Google的流程是笔试通过后会进入多轮onsite面试问题风格一脉相承。而在onsite中边界条件、时间复杂度的口头说明占的权重几乎和代码正确性一样高。我建议在写每一段核心逻辑前先跟面试官讲这里我选择用一个最小堆因为每次只需要取最小的那个元素O(log n)的插入和弹出是合理的。这样即使最后代码有瑕疵面试官也能看到你的思路。反过来你全程沉默地写出一份bug-free的代码在onsite里反而未必拿高分——因为这不像团队协作的样子。4. 刷这套卷子的正确姿势很多人在LeetCode上把题目背下来了以为面试就稳了。我见过太多人拿到面经原题能秒写题目稍微改个条件就完全不会。Google 2012笔试卷的正确打开方式不是刷一遍而是分三遍走。4.1 第一遍限时模拟严格计时90分钟关闭所有IDE提示最好像笔试一样用最朴素的文本编辑器甚至纸笔来写。这一遍的意义是暴露你的裸实力。做完之后立刻对答案但不要看题解只看正确输出。把错题标出来当天晚上复盘问自己三个问题是题意理解错了还是思路不对还是实现细节出bug我自己模拟时会把时间刻意压缩到70分钟给真实考场的紧张感留出冗余。因为真到了面试现场你一边跟面试官聊天一边写代码实际可用时间比想象中少得多。4.2 第二遍专题纵向对比第一遍之后你会看到自己的薄弱点可能链表题特别手生可能DP的遍历顺序总搞错。这时候不要急着刷新题而是把同类题放在一起纵向对比。比如把鸡蛋掉落、戳气球、编辑距离这三道经典DP题放在同一天做。对比什么呢对比它们的状态定义和状态转移方程的推导方式。鸡蛋掉落的状态是鸡蛋数和层数转移靠枚举层戳气球的状态是区间(i,j)转移靠枚举区间内的分割点编辑距离的状态是两个字符串的前缀长度转移靠比较字符。做完对比你会发现DP的难点并不是状态转移方程本身而是状态定义的选择一旦状态定得好方程水到渠成。4.3 第三遍反向出题当你觉得这套卷子的题都会做了就进入最高级的训练法反向出题。把鸡蛋掉落的K改成无限大题目变成什么变成二分搜索的最小次数。把LRU的容量改成动态变化的你要怎么设计把中位数的要求从两个数组改成k个数组合并方式有什么不同这一步训练的正是Google考察的模型抽象能力。你能出的变体越多说明你对原题的理解越深。我出面试题时就是拿经典题改一两个条件候选人有没有做过原题在这时候逃不过我的眼睛。5. 做这套题时我踩过哪些坑最后聊聊实操过程中遇到的实际问题。这些坑我见过无数人反复踩包括我自己。5.1 边界条件的血泪史LRU那道题我第一次用单链表实现删除尾部节点时先遍历找到前驱时间复杂度直接O(N)。考官问我为什么你的get接口不满足O(1)我愣了三秒钟才反应过来。后来我养成了一个习惯所有涉及链表的题先画一个只有两个节点的最小case把所有指针操作在纸上走一遍。这个习惯帮我省了至少十次面试翻车。边界条件这件事没有捷径只有形成条件反射。什么时候判断指针为空什么时候判断数组下标越界这套东西不靠脑子靠肌肉记忆。建议把Google这套卷子里的每一道边界题都用手算的方式跑一遍极端case比如数组长度为0、只有一个元素、两个元素相等的时候。5.2 死磕最优解反而翻车我见过一个候选人鸡蛋掉落用数学法推导出了一个O(1)的公式但推导过程花了将近四十分钟而且中间忘考虑了一个边界case。Final code跑出来只有一个用例不对时间已经不够了。他脑子很聪明但他忘记了一个现实在面试中一个能coding出来的次优解永远好过一个想不出来或者写不完的最优解。我个人的策略是前十分钟如果最优解没有明确思路立刻退回到DP或递归记忆化至少保证在时间截止前有一个能跑通的版本。然后在跟面试官解释时主动说出我知道这里可以用数学优化到O(K log N)但为了在有限时间里保证正确性我先实现这个版本。大部分面试官会认可这个决定。5.3 笔试和工程之间隔着一道鸿沟Google这套笔试卷的价值不仅仅是求职。那些看似复杂的算法在真实的工程里确实有应用场景。比如我在做日志检索系统的时候多个有序日志文件里找第K条匹配记录不就是两个有序数组中位数的变体吗再比如做一个用户行为缓存LRU就是最基础的选择。但笔试卷也有一点跟工程不同工程里你可以查文档、看源码、跟同事讨论笔试必须闭卷。这种差异意味着你不能用我懂思想来替代我能手写。如果你真的想把算法能力转化成工程能力我建议你每个月找一个周末不碰任何IDE补全和搜索引擎手写一个LRU或者红黑树的插入操作。这种刻意练习短期看投入大长期看回报非常可观。最后的体会我现在带团队出笔试题核心框架依然是从Google这套老题里演化出来的。它考的不是某一个语言API而是你能不能把一个模糊的问题拆解成清晰的输入、目标、约束、算法、复杂度的完整链路。这套链路才是这份卷子真正想留给你的东西。