1. 整体感知:这套笔试在考什么
聊触宝科技2017秋季校招笔试研发题第三批之前,我先说个总体的判断:这套题目的考察逻辑,和很多公司“海笔”的套路不太一样。它不光是考你会不会写代码,更像是在模拟一个移动互联网研发工程师日常工作中真实遇到的场景——你在触宝做输入法、做电话助手,天天面对的是海量用户输入、文本编码、网络传输、性能优化这些问题,所以笔试题目也围绕这些真实业务场景展开。
触宝这家公司,老一批做移动互联网的人应该都知道,靠输入法和电话助手起家,海外市场做得很大。当时他们招研发,不是说招一个只会刷LeetCode的人,而是要找一个能直接上手业务、能理解移动端产品逻辑的人。所以第三批笔试题目,我在复盘之后感受到三个明显特征:
第一,算法题属于中等偏上的难度,不搞偏题怪题,但考察的思路很细。尤其是字符串处理类题目,出现频率很高,这和输入法业务强相关。第二,计算机基础题覆盖面广,操作系统、网络、数据库、面向对象都有涉及,但不会让你背概念,而是通过场景题来考察你是否真的理解。第三,会有一道系统设计或者逻辑推理类的题目,这道题往往是拉分的关键,考察的是你分析问题的框架和表达思路的能力。
很多同学当时考完出来,第一反应是“题量不大,但每道题都要想很久”。这其实是好现象,说明这套题不是在拼手速,而是在拼思考深度。如果你现在正在准备校招笔试,或者想检验一下自己作为研发工程师的基础是否扎实,这套题值得认真做一遍,而且不能只做一遍,每一道题都值得反过来再琢磨几遍。
2. 核心题型拆解与考点逻辑
2.1 算法题:字符串处理是重头戏
先说占比最大的算法题。第三批的算法题里,字符串操作占了很大比重。我印象里有一道题,大意是要实现一个函数,将给定的字符串按指定规则进行压缩,比如连续相同字符用“字符+出现次数”的方式表示。看起来很简单,但这里面至少有三个坑。
第一个坑是边界条件。空字符串、单字符字符串、字符串末尾刚好是连续字符的结束位置,这些情况都要考虑到。很多同学在写循环的时候,习惯遍历到字符串末尾才处理最后一组连续字符,如果代码里对边界处理不当,就会出现“最后一个字符没被压缩”或者“越界访问”的问题。
第二个坑是压缩后长度的问题。如果压缩后的字符串长度并不比原字符串短,应该返回原字符串。这个条件我在实际面试中问过不少人,至少有三分之一的人会漏掉这个判断。它考察的不仅是编码能力,更是你对题目要求的理解是否完整。
第三个坑是空间复杂度。如果你用的是C++,直接在原字符串上修改或者用一个新的string接收再返回,这两种写法的空间复杂度是不同的。面试官往往会追问:“能不能做到原地压缩?”这是一个很经典的追问,最好提前想清楚原地操作的实现方式。
除了字符串,二分查找、链表反转、二叉树遍历这类基础算法题也会出现。这些题不难,但考察的是你写代码的规范性和异常处理能力。比如链表反转,很多人背了迭代写法就以为万事大吉,但万一题目改成“反转链表中第m到第n个节点”呢?如果你的代码是硬编码在特定场景下,就很难做扩展。
我在复盘这套题的时候有一个强烈感受:它考察的不是你背了多少经典题,而是你能不能把一道基础题写得无懈可击。边界条件、鲁棒性、可读性,这三点比“AC了没有”重要得多。
2.2 基础题:操作系统、网络、数据库一个不落
基础题部分,操作系统和网络的考点非常常规,但设问方式很有讲究。比如操作系统肯定会考进程和线程的区别,但它会换一种问法,让你分析“一个多线程程序里某个线程崩溃了,其他线程是否还能正常运行”,或者“为什么线程之间切换的开销比进程之间切换小”。这就要求你不仅仅记住概念,还要清楚背后的内核机制——比如线程切换不涉及地址空间切换,所以TLB不用失效,这就是开销低的根本原因。
网络方面,TCP三次握手肯定要考。但触宝的考题里,我注意到它更倾向于考察TCP和UDP在真实业务场景下的选型问题。比如输入法里做本地词典的下载,应该用TCP还是UDP?为什么?这时候你不能只回答“TCP可靠、UDP不可靠”,而是要结合实际场景分析——文件下载需要保证完整性和正确性,所以用TCP,并考虑断点续传、校验重传等机制。你会发现,很多网上的答案只停留在概念表面,真正面试官想听的是你结合业务的分析能力。
数据库方面,它不会直接让你写一个很复杂的SQL,而是倾向于考察索引相关的问题。比如创建联合索引时字段顺序如何选择、什么情况下索引会失效。我见过很多同学把“最左前缀”背得很熟,但一遇到“where a = 1 and b > 2 order by c”这样的组合条件,就不知道索引该怎么建了。这道题考察的是你对索引底层结构(B+树)的理解深度,而不只是背规则。
2.3 逻辑分析题:考察问题拆解能力
第三批的题目里有一道逻辑题,具体是让候选人设计一个方案,来估算一个城市里某种共享出行工具的数量。这类题没有标准答案,考察的核心是拆解问题的能力。你不需要给出一个精确的数字,但你的思考路径要清晰、有理有据,同时要能够合理地估算关键参数。
我建议遇到这种题时,先列出关键假设——比如城市人口、使用频率、单次使用时长、日均需求量和车辆数量的关系。然后一步步推导,最后给一个粗略的数字区间。过程中要主动说明“这些数据是估算值,实际可能不同,但方法论是一致的”。面试官要看的不是数字准不准,而是你有没有构建模型的意识,能不能用数学的方式处理一个模糊问题。
3. 实战模拟:几道典型题的完整解析
3.1 字符串压缩题的完整解法
我拿刚才说的字符串压缩题做一个完整解析,模拟笔试时的思考和书写过程。
题目原意大概是:给定一个由小写字母组成的字符串,实现算法将连续重复出现的字符进行压缩,输出压缩后的字符串。如果压缩后的字符串长度不小于原字符串,则返回原字符串。
我的推荐解法是用一次遍历,在扫描过程中记录当前字符和出现次数,遇到不同字符时把之前记录的字符和次数写入结果。核心代码如下:
string compressString(const string& s) { if (s.empty()) return s; string result; int count = 1; for (int i = 1; i <= s.length(); ++i) { if (i < s.length() && s[i] == s[i - 1]) { ++count; } else { result += s[i - 1]; result += to_string(count); count = 1; } } return result.length() < s.length() ? result : s; }这里写i <= s.length()而不是i < s.length(),就是为了在循环结束的时候利用哨兵位统一处理最后一组连续字符,省掉了循环外面重复写一段逻辑。这个技巧很实用,能有效避免边界问题。
再看时间复杂度和空间复杂度。这个算法遍历一次字符串,时间复杂度是O(n),空间复杂度主要取决于结果字符串的长度,最坏情况下(比如所有字符都不相同)压缩后长度为原字符串的两倍,空间复杂度O(n)。
3.2 单链表反转的两种写法
单链表反转也是高频题,这里给出迭代和递归两种写法。
迭代法比较好理解,用三个指针prev、curr、next,每次把当前节点的next指向前一个节点,然后整体向后移动。核心代码如下:
ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* curr = head; while (curr) { ListNode* next = curr->next; curr->next = prev; prev = curr; curr = next; } return prev; }递归法的写法更简洁,但很多人想不明白它为什么能反转。关键在于递归的终止条件和返回值——我们先假设递归函数可以把后面的链表反转好,然后只需要把当前节点的next节点的next指向当前节点,即head->next->next = head,同时让head->next = nullptr,最后返回新的头节点。这个思路本质上是从后往前处理的,画一下递归调用栈会清晰很多。
说实话,笔试过程中我建议优先写迭代法。不是因为递归不好,而是有些同学递归函数里对返回值的处理容易出错,一旦写错,在笔试环境里debug非常浪费时间。迭代法的思路更直观,现场推导不容易出问题。
3.3 估算类逻辑题的答题框架
再展示一下逻辑题的回答框架。假设题目是“估算某城市共享单车的数量”,我的回答结构如下。
第一步:明确目标用户。城市人口假设为1000万,其中10岁到60岁的可用车人群占比约70%,即700万。
第二步:估算活跃用户。这700万人中每天实际使用共享单车的人,假设有10%,也就是70万人会使用共享单车。
第三步:分析使用频率和时长。假设每人每天骑行2次,单次平均骑行20分钟,那每天的总需求时长为70万乘以40分钟,等于2800万分钟。
第四步:计算单车的日可服务时长。单车肯定不是24小时都在被使用,考虑到早晚高峰和其他时段分布,假设每辆单车每天有效使用时间为80分钟,则需要的单车数量约为2800万除以80,等于35万辆。
第五步:加上冗余量。需要考虑车辆损耗、维修停用、区域调度不均等因素,可以加上20%的冗余,最终估算在40万辆左右。
整个思考过程不需要数字有多精确,但每一步假设都要说清楚,推导过程要自洽。如果你能引出“需求时长”和“单车服务时长”两个核心指标,然后建立比率关系,这道题基本就拿下大半了。
4. 笔试过程中的细节技巧与常见问题排查
4.1 时间分配与做题顺序怎么安排
笔试时间一般在一个小时到一个半小时之间,题量为3到5道大题。我在实际做这套题的时候体会很深的一点是,不要按试卷顺序挨个往下做,而是要先花两三分钟把整张卷子快速浏览一遍,标记出哪些题是送分题、哪些题是拉分题。
然后从送分题开始做,先把能拿的分稳稳拿到手。基础题里那些概念性问题,不要长篇大论写一堆,尽量分点表述,每个点一行,让面试官一眼看到采分点。用关键词配合简短解释,比如“进程是操作系统进行资源分配的基本单位,线程是CPU调度的基本单位;同一进程内的线程共享地址空间,而进程之间地址空间相互独立”。这种答法既节省时间,又清晰有力。
算法题的时间占比应该最大,建议留出40%到50%的时间来写算法。如果一道题卡了十五分钟还没有完整思路,果断先跳过去做后面能做的题,等最后再回来补。千万不要在一道题上死磕,笔试是拿分游戏,不是奥数竞赛。
4.2 代码书写规范:面试官一眼看到的东西
笔试和实际项目开发完全不同,你在本地IDE里跑得通不是终点,面试官要的是“一眼能看懂的代码”。我复盘了很多人笔试翻车的案例,最常见的问题有三个。
第一个问题是不写注释或者注释乱写。不写注释会让面试官觉得你没有工程习惯,注释写得牛头不对马嘴更让人头大。好的做法是:函数开头用一两行说明函数功能、输入参数和返回值;算法关键步骤(如“处理边界条件”“循环结束后处理最后一组字符”)要配上简短注释。
第二个问题是变量命名随意。函数参数不要只写a、b、c,变量名要有意义,比如用left、right表示双指针,用count表示计数。这不仅仅是为了好看,更重要的是让你自己的思路更清晰。很多逻辑错误,就是在用有意义的变量名时更容易暴露出来。
第三个问题是忽略异常输入。我见过不少人写链表反转时,完全没有考虑空链表和单节点链表的情况。虽然代码在测试用例上能AC,但在面试官看来这是明显的隐患。笔试时一定要先写一两个边界条件判断,哪怕只是“如果链表为空,直接返回nullptr”,也是加分项。
4.3 常见的思路卡壳点与突破方法
根据我对这套题和其他校招笔试的观察,大家最容易卡壳的几个地方,我整理成了一个速查表。
| 卡壳点 | 原因分析 | 应对策略 |
|---|---|---|
| 字符串连续子串处理 | 边界条件梳理不清晰 | 在草稿纸上画出每个参与比较的指针位置,模拟最后一次循环 |
| 递归返回值设计与递归出口 | 对递归流程理解不透彻 | 先写最小规模用例(比如n=1或n=2),在纸上手推一遍调用过程 |
| 二叉树层序遍历的逐层标记 | 不会区分每一层的边界 | 内层再加一个循环,每次循环开始时记录当前队列长度 |
| 动态规划的状态定义与转移方程 | 不知道从子问题切入 | 先从暴力递归写起,再改造成自底向上的状态转移,不要一上来就套公式 |
| 估算类题目的参数假设 | 担心假设不对不敢继续 | 明确告诉面试官“这是估算,误差会影响结论数量级但不影响方法” |
有一个我印象很深的画面:考场上很多人做动态规划题时对着题目发呆,因为心里默认自己“不会做”。但如果你先写出暴力递归版本,你会发现很多题的递归关系其实很清晰,只是你没有往下走这一步。动态规划从来不是一步到位的,它是由暴力解法优化过来的。
5. 复盘角度:这套题对未来笔试准备的指导意义
5.1 基础能力是永远的第一位
这套题让我最感慨的一点是:它没有刻意设置“偏、难、怪”的题目来刁难人,所有的考点都是课堂上学过的、项目里用过的、社区里讨论过的东西。但它通过业务场景包装,把知识点考察得更深入了。
所以准备校招笔试的时候,不要一上来就狂刷难题偏题,先照镜子检查一下基础知识是否牢靠。比如:你能不能在白板上清晰地写出快速排序的完整过程?能不能解释为什么快速排序最坏情况下时间复杂度是O(n^2)?这些看着简单的问题,往往就是笔试翻车的根源。
我当时为准备笔试,把《数据结构与算法分析》里的每一道例题都手写实现了一遍,不复制不粘贴,然后对照书上的讲解检查自己的思路和细节。这个过程看起来笨,但效果比刷两百道LeetCode还好,因为它逼着你去思考为什么这样写,而不是仅仅“见过这道题的答案”。
5.2 动手写代码的能力要刻意练习
还有一个非常关键的点:笔试环境下写代码的手感和平时在电脑上写代码完全不同。没有IDE的自动补全,不能随时编译运行,连断点调试都做不到。如果你平时依赖IDE太重,很容易在笔试时露怯。
我在准备阶段会刻意用白纸或者不带代码高亮的编辑器来写题,写完以后再复制到IDE里编译运行看结果。这样做最明显的好处是逼自己在写之前就在脑子里把逻辑走通,而不是靠编译器帮自己改错。很多同学平时在IDE里写代码,一个分号没打,编译器立刻提示,但笔试现场没有人帮你提示。
5.3 表达能力同样是笔试的重要组成部分
笔试不等同于写代码。很多线上笔试系统会要求你对编程题给出“解题思路”,这就非常考验书面表达能力。你在纸上向一个从未见过你的人说明你的思路,需要把话说完整。不要只贴代码,面试官想看到的是你的思考过程。
我的建议是:每一道算法题都按照“题目理解—思路设计—复杂度分析—核心实现”的框架来组织答题内容。题目理解部分要复述题目的关键约束条件,思路设计部分可以结合例子说明,复杂度分析部分写明时间和空间两种情况,核心实现部分给出完整代码和必要注释。这样一个四段式的答题结构,让面试官按图索骥,也让你自己不遗漏任何考点。
6. 一道扩展思考题:如何把笔试思路迁移到系统设计中
有些同学会问,这套笔试里没有专门的系统设计题,是不是系统设计就不用准备了?我的看法正好相反。那些估算类逻辑题、那些选型分析题,本质上就是系统设计的雏形。把一道估算题扩展开放,它就变成了系统设计题的背景。
比如估算题里你已经算出了共享单车数量,进一步就能问:如果让你设计一个这套系统的后台,你会怎么设计数据表?用户骑行记录表应该包含哪些字段?订单流水表如何设计才能支撑并发写入?要不要引入消息队列来解耦业务流程?这其实就是从“单点问题”走向“系统性问题”的思考路径。
校招笔试回答这类扩展问题时,可以按照“容量估算—数据模型—核心流程—关键接口—容错与监控”的顺序来展开。先估算系统的规模,再决定数据模型怎么设计;确定数据模型后,核心业务流程就清楚了;接口设计要围绕业务流程做,最后加上容错机制和监控手段。这个框架是我在做了大量系统设计面试准备后总结出来的,在后面参与真实项目评审时也一直沿用。
6.1 容量估算:所有设计的第一步
很多人的系统设计是从画架构图开始的,这其实是本末倒置。架构图应该是容量估算的结果展示,而不是凭空画出来的。你连系统要支撑多大的流量都不知道,怎么判断要不要上缓存?要不要分库分表?要不要引入消息队列?
容量估算的步骤很简单:预估用户量,乘以每个用户的平均请求量,得到系统每秒的访问量QPS,再考虑峰值系数,得到系统的峰值QPS。假设一款产品日活用户200万,每个用户每天发起100次请求,平均每秒请求量约2300次,峰值系数按5倍算,就是每秒约1.15万次请求。有了这个基数,你在设计存储层的时候就知道单库大概率吃不消,需要分库分表,从而引入数据路由和分布式事务方案。
6.2 数据模型与核心流程:把问题变具体
数据模型的设计要围绕业务核心实体展开。比如“用户骑行”这个行为,核心实体就是用户、车辆、订单。用户表关注基本信息,车辆表关注位置和状态,订单表关注骑行时间、费用、支付状态等。这三张表之间的关联关系、索引设计、数据量级、归档策略,都要在笔试答题中交代清楚。
核心流程则要选一个主线来讲,比如“扫码开锁”的完整流程。从用户扫码、请求下单、锁定车辆、开始计费,到骑行结束、结算扣款、释放车辆。这个流程走完,你就能在答题中穿插使用到缓存(锁车状态查询)、消息队列(订单异步处理)、分布式锁(同一辆车不能同时被两个人扫开)等技术点,给面试官展示你的知识广度和深度。
7. 最后说两句个人体会
复盘这套触宝科技2017秋季校招笔试研发题第三批,我自己最大的收获不是某个具体知识点的答案,而是它让我重新审视了“准备校招笔试”这件事。刷题很重要,但比刷题更重要的是理解题目背后的考察意图。你写的每一道字符串压缩题,背后对应的可能是业务里的日志清洗逻辑;你做的每一道链表反转题,背后对应的可能是Android端消息队列的节点维护。当你能把一道算法题和实际业务场景联系起来的时候,这道题才算真正做透了。
我身边不少人当年校招时折在笔试环节,不是代码能力不行,而是不会“考试”——不会分配时间,不会组织答案,不会在有限时间内展示自己最强的部分。这套题提供了一个很好的样本,让你知道一家真实做移动互联网产品的公司,希望招到什么样的研发工程师。技术基础扎实,代码风格规范,能结合业务思考问题,有清晰的表达逻辑——这四个能力,无论你参加哪一年的校招、面试哪一家公司,都是通用的核心竞争力。
如果你正在准备下一场笔试,我建议你把这套题当成一面镜子:先完整做一遍,然后对照答案复盘,找到每一道错题对应的知识点缺口,再针对性地去补基础、练代码、写思路。等到你做这套题不再卡壳的时候,你的笔试能力基本就过关了。
最后分享一个我后来一直沿用的练习方式:每次笔试结束后,不管成绩如何,我都会把整套题重新做一遍,并写一篇思路复盘文档。这样做看起来费时间,但实际上是我成长最快的方式。每道题都当面复盘,才会暴露出很多自以为懂但没有真正弄懂的细节。