
前阵子好几个要参加秋招的学弟学妹跑来找我问搜狗测试工程师的编程题到底考什么、怎么准备。我翻了一下2019年秋招第一场的题目发现一个很有意思的现象单独看每道题都不算难基本都是LeetCode中低档位的题但一旦把“测试工程师”这个身份套上去整个做题思路就变了。搜狗这套题正好是一个很典型的样本它不考偏题怪题考的是你对代码的边界感、逻辑完整度和“会不会给自己留坑”的敏感度。很多同学刷算法题有个通病在编辑器里写得很顺提交也通过一到白板手写就开始丢三落四。搜狗这套题恰恰戳中了这个点。我准备把自己对这套题的复盘拆解开逐题讲清楚思路推导、参考答案、测试用例设计再补一些实际踩坑和面试追问的经验。不管你是准备测试岗还是开发岗这套题的做题逻辑都值得认真过一遍尤其是“先写测试用例再写实现”这个习惯能帮你把面试通过率拉高一截。1. 搜狗测试工程师编程题的出题逻辑与备考思路1.1 测试工程师的编程题到底在考什么很多人以为测试工程师的笔试会比开发岗简单实际不是。搜狗这套编程题给我的感觉是门槛不高但区分度很大。它不会让你写红黑树、手撕AVL旋转也不会让你默写复杂的动态规划状态转移方程而是把一些经典的、贴近工程实现的题目拿出来看你能不能写干净。这类题目有一个共同特征边界条件特别丰富。比如字符串处理题空字符串怎么处理、中间出现连续分隔符怎么处理、字符串特别长怎么办这些都是测试工程师日常最敏感的问题。搜狗出这类题本质上不是在考你会不会某个算法而是在看你有没有测试思维。这也是为什么很多开发背景的同学刷了很多题到了测试岗笔试却拿不到高分因为他们的关注点全在“能不能跑通主流程”而测试岗更看重“你会不会主动想象各种异常场景”。我建议备考测试岗编程题的时候不要再按照开发岗刷题的模式走。不要只看题解里的官方思路而是每做一道题都顺手列出至少八到十个测试用例包括正常输入、边界输入、异常输入。这个习惯既是笔试拿分的关键也是将来做测试工程师的基本功。1.2 应试策略从题目中读出隐藏要求搜狗2019秋招第一场的编程题从题型分布上看大致集中在字符串处理、数组操作、二分思想的应用偶尔还会涉及一点基础的排序或去重。但如果你只盯着算法本身很容易忽略一个信息题面往往很短没有什么花哨的包装这说明出题人想让你把精力放在编码实现和细节处理上。我的做题策略是这样的。首先花两到三分钟把题目读透划出输入范围、输出格式、特殊约定这三个要素。很多人在笔试里写错不是不会写代码而是没有注意“输出要求是什么”比如有的题目要求输出索引有的要求输出字符本身有的要求删除所有符合条件的元素这些细节一旦看错代码写得再好也白搭。其次想清楚再动手。测试岗笔试的时间通常没有开发岗那么紧张但也不宽裕所以更不建议上来就写。我一般会在草稿纸上先写一个“用例矩阵”把输入拆成几类常规输入、最小输入空、一个元素、两个元素、最大输入超长字符串、大数、重复输入、特殊字符输入。想清楚每一类输入应该得到什么结果之后代码逻辑基本就清晰了再动手写会顺很多。2. 字符串处理题删除出现次数最少的字符2.1 题目原型与思路推导这类题在各大公司的测试工程师笔试里反复出现搜狗这场的版本是给定一个只包含小写字母的字符串删除其中出现次数最少的字符如果有多个字符出现次数相同且都是最少则一并删除最后输出删除后的字符串。我第一眼看到题目时脑子里立刻跳出一个想法这题不就是“统计频率 找最小值 过滤拼接”三步走嘛。确实主流程不难但它在测试场景里埋了不少坑。比如删除之后字符串里可能一个字符都不剩这时候应该输出空串还是输出某个约定值如果出现次数最少的是好几个字符是只删一个还是全删这些都是题目里容易含糊的地方也是测试工程师应该主动去澄清的点。思路拆开看一共三步。第一步遍历字符串用字典记录每个字符出现的次数第二步找出最小次数这里有个细节是不要直接对字典values取min然后只删一个字符而是要先把所有“次数等于最小值”的字符收集起来第三步重新遍历原字符串把不在删除集合里的字符拼接成结果。因为题目只涉及小写字母所以也可以用一个长度为26的数组来计数这样在工程上比字典更轻量。用数组的话字符索引可以用ord(c) - ord(a)来算这一步对很多新手来说是最容易卡住的地方。Python里字符转ASCII用ord()整数转字符用chr()别记反了。2.2 参考实现与复杂度分析下面我给出一个比较稳的Python实现这套写法我在面试时也经常用优点是逻辑清晰、不容易手滑。def remove_least_frequent(s: str) - str: if not s: return # 1. 统计频率 cnt [0] * 26 for ch in s: cnt[ord(ch) - ord(a)] 1 # 2. 找出最小出现次数 min_cnt float(inf) for c in cnt: if c 0 and c min_cnt: min_cnt c # 3. 把所有出现次数等于最小值的字符收集到一个set remove_set set() for i in range(26): if cnt[i] min_cnt: remove_set.add(chr(i ord(a))) # 4. 过滤原字符串 res [] for ch in s: if ch not in remove_set: res.append(ch) return .join(res)时间复杂度是O(n)空间复杂度是O(1)因为用了固定长度的数组和set。这里有个很容易被忽视的小点找最小次数的时候cnt数组里大部分是0所以必须加一个c 0的判断否则min_cnt会一直等于0最后程序会把所有没出现过的字符也算进去。笔试现场我一般会先写一版基础实现然后立刻补测试用例。题目里如果没说输入一定是小写字母我会额外做一个校验或者和面试官确认输入范围。如果题目支持任意ASCII字符那用字典会更稳妥数组方案就需要扩大到128或256。2.3 测试用例设计与踩坑经验这道题的测试用例设计我强烈建议按下面的矩阵来思考。用例类别输入示例预期输出检查点空串空输入是否直接返回空串不报错单字符a删掉唯一字符后为空串全部相同aaa所有字符次数相同全部删除常规混合abacaaa出现两次b和c出现一次删除b和c多个最少字符abcd四个字符各出现一次全部删除超长输入a*100000 b*99999仅包含a的字符串是否O(n)完成不超时大小写混入aA取决于题目约定是否区分大小写这里我特别想提一个经验笔试的时候一定要留意题目描述里的“只包含小写字母”这种限定。搜狗这道题如果没有这个限定那数组长度26就不够用了得换成字典。很多同学不是不会字典而是在紧张状态下直接套模板把题目限定给忽略了结果边界用例一测就崩。我通常的习惯是就算题目明说只包含小写字母在代码里也写一个if c in remove_set这种通用逻辑万一输入格式有偏差不至于整个程序崩溃。3. 数组操作题寻找出现次数超过一半的元素3.1 题目原型与两种解法对比搜狗这场还有一道很经典的数组题给定一个非空数组数组里某个元素出现的次数超过数组长度的一半请找出这个元素。这道题的学术名叫“多数元素”LeetCode 169题面试出现频率高得离谱。我见过的解法至少有四种排序后取中间值、哈希表计数、分治、摩尔投票。笔试现场我推荐用哈希表因为思路直白、不容易写错而且对于测试工程师来说“能快速写出正确的代码”比“炫技写最优解”更重要。不过如果你准备充分摩尔投票法是一个很好的加分点。它的核心思想是“同归于尽”候选人初始化为数组第一个元素票数设为1遍历数组如果当前元素和候选人相同票数加1否则票数减1当票数减到0时更换候选人为当前元素票数重置为1。因为多数元素出现次数超过一半所以即使其他所有元素联合起来“抵消”它最后剩下的候选人依然是它。3.2 参考实现与易错点哈希表版本def majority_element(nums): cnt {} n len(nums) for x in nums: cnt[x] cnt.get(x, 0) 1 if cnt[x] n // 2: return x return -1摩尔投票版本def majority_element(nums): candidate None count 0 for x in nums: if count 0: candidate x count 1 if x candidate else -1 return candidate这两个版本有一个关键差异哈希表版本可以在遍历过程中提前返回但需要额外O(n)空间摩尔投票版本空间O(1)但必须遍历完才能确定结果而且它默认题目保证存在多数元素。如果输入不保证存在多数元素摩尔投票跑完还需要再遍历一次验证候选人的出现次数这是很多人在面试追问环节翻车的地方。从测试工程师的角度我更在意“程序会不会因为输入不合法而崩溃”。比如题目说非空数组但实际输入可能传了None元素范围可能是负数可能是浮点数可能是对象。用哈希表写cnt.get(x, 0)对大多数类型都能工作但如果是不可哈希的对象就会报错。所以如果面试官允许我会先确认元素的类型约束或者直接说明“这个解法假设输入是整数数组”。3.3 面试追问与延伸思考面试官在这道题之后很喜欢追问一个问题如果数组中并不存在出现次数超过一半的元素你的代码会怎样这个时候哈希表版本因为提前判断会返回-1摩尔投票版本会返回一个错误候选。正确的做法是加一个验证步骤这是测试工程师思维的典型体现——你不仅要写对主流程还要知道程序在异常输入下会表现出什么行为。再延伸一步如果把“超过一半”改成“超过三分之一”让你找出所有出现次数超过三分之一的元素你会怎么改这个就是LeetCode 229题思路是摩尔投票扩展版维护两个候选人和两个计数器。我在模拟搜狗面试时有时候会自己给自己出这种变体题不是为了押题而是为了训练思维灵活性。测试工程师写代码的机会相比开发会少一些但思维训练不能停。4. 二分思想题有序数组旋转后的查找4.1 题目原型与二分变形的关键搜狗这套题里还出现过一类比较有区分度的题把一个有序数组在某个未知点旋转比如[0,1,2,4,5,6,7]变成[4,5,6,7,0,1,2]给定一个目标值要求判断目标值是否存在于数组中存在则返回索引。这道题的常规解法是二分查找但难点在于“你无法直接判断mid和target谁大谁小”因为数组被旋转过天然的无序点破坏了二分的基本前提。解决思路是虽然整个数组不是有序的但不管怎么旋转从中间切一刀至少有一半是严格有序的。所以二分时先判左半部分是否有序再判目标值是否落在这个有序区间里如果落在就在这一半继续二分如果没落在就去另一半找。这个思路说起来简单但我在面试现场见过太多人栽在等号判断上。比如判断左半部分有序条件是nums[left] nums[mid]这个等号必须要加。等号在处理重复元素时会引发很多复杂的边界问题所以很多面试官会在你写完后又追加一句“如果数组里有重复元素怎么办”这是整道题的最高潮也是最能体现测试敏感度的点。4.2 参考实现与去重处理不考虑重复元素的版本def search_rotated(nums, target): left, right 0, len(nums) - 1 while left right: mid (left right) // 2 if nums[mid] target: return mid if nums[left] nums[mid]: # 左半边有序 if nums[left] target nums[mid]: right mid - 1 else: left mid 1 else: # 右半边有序 if nums[mid] target nums[right]: left mid 1 else: right mid - 1 return -1如果数组里有重复元素例如[1,0,1,1,1]你会发现nums[left] nums[mid]时既不能说左半边有序也不能说右半边有序。此时最简单的处理是把left右移一位跳过重复值再继续循环。这个做法看起来粗暴但实际效果不错最坏情况会退化成O(n)比如数组全是一样元素但多数面试场景里足够用。从测试角度这道题我必测这样一个用例nums [1,0,1,1,1], target 0不用重复处理的话很容易漏掉。另一个必测用例是nums [4,5,6,7,0,1,2], target 3目标值不存在返回-1。还有单元素数组、空数组、旋转后与原数组相同等边界。这些用例想全了代码的正确性才能站得住。4.3 二分题在测试岗面试中的意义说实话测试工程师日常工作中直接写二分查找的机会并不多但搜狗这类大厂为什么还在笔试里考二分我理解是因为二分背后体现的是“对问题规模的敏感度”和“对条件的精确判断力”。一个能写好二分的人通常也能把复杂业务的边界条件梳理得很清楚。所以备考的时候不要把这类题当作纯粹的算法题去背模板而是想一想“为什么这个等号必须加”“为什么这里要判断nums[left] nums[mid]而不是”。当你把每一个分支都想透你就具备了测试思维里很重要的一种能力对条件和边界极其敏感。5. 测试思维如何“反哺”编程题答案5.1 先写用例再写代码的工作流我在前面反复提到测试用例这里展开讲一下实操方法。做题时我会在草稿纸上画一个“输入输出映射表”第一列写输入第二列写预期输出第三列写这个用例的价值判断。这个习惯源自实际测试工作里的用例设计方法论搬到笔试里同样适用而且效果出奇的好。举个例子有一道字符串排序题要求在字符串内部按字符频率降序排列同频率按字典序排列。很多人在动手写代码前已经想好了用Counter和sorted但没想过输入为aaabbc时a出现3次b出现2次c出现1次输出是aaabbc。如果这是我设计的用例排序规则必须清晰先按次数降序再按字符升序。如果先按字符升序再按次数降序出来的结果可能就是aaabbc和bbaaac的区别。不能提前想好写的时候很容易在key函数里把顺序搞反。在工作流上我的顺序是读题、圈出输入输出约定、写用例矩阵、想主流程、写代码、拿用例矩阵逐行验证。这样看起来多了几步但总时长反而快因为不会出现“代码写完才发现理解错了题意”这种返工。5.2 防御式编程永远假设输入会坑你测试工程师平时的工作就是和各种异常输入、脏数据打交道所以笔试写代码时也应该保持这种职业习惯。防御式编程不是说每一步都做一堆无意义的校验而是在关键入口处做合理防护。比如题目给了非空数组但你可以顺手在函数开头写一个if not nums: return -1题目说你只考虑小写字母但你的代码里因为有通用逻辑即使传进来一个大写字符也不至于崩溃。这些习惯在面试官眼里是加分项因为他们看到的不只是一个“能写对题的人”而是一个“写代码时脑子里有测试地图的人”。不过要注意防御式编程不是越多越好。过度校验会让代码变得臃肿在笔试时间有限的情况下反而拖慢速度。我的原则是只保护那些不保护就会导致程序直接崩溃的点比如空输入、None输入、除数可能为0的情况。至于像“目标值不存在”这种结果性异常用返回值或异常去表达不在入口做过多处理。5.3 如何向面试官解释你的思路除了把题写对你可能还需要在面试中口头解释自己的解法。测试工程师面试尤其喜欢追问“这个代码哪里最容易出错”我的建议是不要只说“这里边界条件要考虑一下”而是举一个具体的用例比如“如果输入是空字符串我的代码会直接返回空串避免下一层统计逻辑报错”。这种回答比空泛的“我会注意边界条件”有说服力得多。同一个代码问题面试官会期待测试工程师给出比开发候选人更多的测试细节。如果你能在讲思路的时候自然地提到几个测试用例并且说明每个用例覆盖了哪一类问题你在面试官眼里的专业度会立刻不一样。这也是我为什么反复强调“用例矩阵”的习惯它不只是笔试工具更是面试表达工具。6. 常见问题排查与实操经验补充6.1 现场笔试最容易踩的五个坑我总结了一下历年带人准备测试岗笔试时最常见的坑这里统一列出来。第一个坑是没注意输入数据的规模。有些题目给的数据量很大如果你上来就写O(n^2)的嵌套循环即使答案正确笔试系统也会因为超时给你判失败。第二个坑是输出格式不匹配比如要求输出布尔值你输出了字符串要求输出每个元素占一行你输出空格分隔这些都会被判定为错误。第三个坑是变量名混淆在手写代码时常见比如left、right写混cnt和count同时存在导致误用。第四个坑是忘记处理空输入和单元素输入。第五个坑是太追求简洁而牺牲了可读性面试官想在有限时间里看懂你的代码所以变量名尽量直观注释可有可无但逻辑要一目了然。针对这些坑我现场做题时会做一个“自查10秒”动作代码写完后先不急着提交从头到尾看一遍所有循环边界和if条件有没有明显的等号遗漏再把输入输出代码和题目要求对照一遍。这十秒钟看起来很短但在笔试里常常能救你一命。6.2 一套万能的笔试自检清单这里给出一套我自己用的自检清单每次笔试前我都会在脑子里过一遍。第一输入读取逻辑是否正确是否有多余空格、换行问题。第二输出格式是否和题目完全一致包括大小写、空格、换行、是否保留小数。第三是否覆盖了空输入、单元素输入、最大输入这三种边界。第四是否有潜在的整数溢出问题在C或Java里尤其要注意。第五代码是否因为某些分支漏写return导致函数返回默认值。第六循环是否可能无限循环尤其是二分和while循环。第七是否使用了题目不允许的额外库有些笔试环境不支持第三方库。第八时间复杂度是否满足题目要求的量级。这套清单不针对哪一道题但我做每一道题时都会拿着它逐条核对。养成这个习惯之后你会发现笔试通过率真的会变因为你不再靠“感觉”判断自己写对了没有而是靠一套标准流程去验证。6.3 对于测试工程师的长线积累建议最后说一点长线的东西。笔试只是测试工程师职业生涯里很小的一个路口但笔试准备过程中形成的思维方式比如边界思维、异常思维、用例设计思维会伴随你很长时间。我见过很多从开发转测试、或者校招进大厂的同学真正让他们拉开差距的不是谁多会几个算法而是谁能在拿到需求的第一时间就想到“这里可能会出问题”。这种能力靠刷题刷不出来靠的是每天坚持用测试的眼睛去看代码和需求。所以我给大家的建议是不要考完笔试就把算法题丢到一边。等你真正做了测试工程师你依然需要阅读代码、设计用例、定位问题这些能力的底层都是你在笔试阶段训练过的逻辑分析能力。搜狗这套题的价值不在于押中多少分而在于它用几个经典题目提醒你应该往哪个方向去准备。