搜狗2020校招后端笔试(第一场)是我秋招季里印象很深的一场。搜狗这个公司,做搜索起家,后来输入法、AI硬件都有布局,所以它的后端笔试天然带着一股"实用主义"的味道——不搞偏题怪题,但每一道题都能感觉到它是在为实际业务筛人。我当时是在牛客网的系统上完成的,全程摄像头监考,两个小时的题量,从选择题到编程题再到设计题,节奏非常紧凑。现在回过头看,这套题的考点覆盖和难度梯度设计得很典型,很适合准备大厂后端校招的同学拿来当模拟卷练手。
这篇复盘我会把整套题的出题逻辑、每道编程题的完整解法、选择题涉及的知识点串讲,以及系统设计题的解题思路全部拆开讲一遍。如果你是正在准备校招的应届生,或者想跳槽大厂后端的社招朋友,这套题值得认真过一遍。我尽量还原当时做题的真实场景和思考过程,有代码的地方给出可直接运行的版本,顺便把我踩过的坑也一并说了。
1. 笔试整体概览与出题逻辑
1.1 题型分布与考点拆解
先说整体结构。搜狗2020校招后端笔试第一场,题型分三块:单选题、编程题、设计题。选择题大概二十道左右,涉及数据结构、操作系统、计算机网络、数据库、C++/Java语言基础这几个方向;编程题三道,由易到难;最后一道设计题,给一个场景让你写方案。总分100分,编程题占比最大,基本上是能不能进面试的分水岭。
从考点分布可以明显看出搜狗的筛选逻辑:基础不牢的,选择题直接刷掉;代码能力不行的,编程题卡死;有工程思维潜力的,设计题拉开差距。这套组合拳其实和大厂后端校招的主流玩法完全一致,并没有因为是搜索引擎公司就考什么冷门算法。唯一带点搜索业务色彩的,是编程题里那道Top K高频词,以及设计题里的短链服务——这两题背后都能隐隐看到搜索业务中"海量数据、高并发、快速响应"的影子。
1.2 难度曲线与时间分配判断
我的判断是,这套题的整体难度在当年大厂校招里属于中等偏上。选择题部分有大概三分之二是基础题,认真复习过408的同学都能拿分,但中间会穿插一两道容易纠结的坑题,比如考你"TCP四次挥手中TIME_WAIT状态出现在哪一端"这类细节。编程题第一题属于热身级别,10分钟之内搞定;第二题是经典链表题,考快慢指针,原理不难但容易在边界条件上翻车;第三题就要动点脑子了,表面是统计词频,实际上考的是堆排序和Top K思想的灵活运用。设计题呢,没有标准答案,但必须写出完整的方案框架。
时间上我建议这样切:选择题控制在40分钟以内,每道题读两遍还拿不准就先标记跳过,别恋战。编程题留80分钟,第一题15分钟,第二题25分钟,第三题30分钟,剩10分钟验证边界和整理设计题提纲。设计题不用写代码,写清楚架构、数据结构、流程和瓶颈分析就够了,时间不宜超过20分钟。这套时间分配我后来复盘觉得是合理的,实际考试时我是选择题花了35分钟,编程题刚好踩线,设计题写得有点赶,如果一开始就能严格按这个节奏来,设计题能写得更从容。
2. 编程题逐题复盘:从字符串到链表的实战拆解
2.1 第一题:最长不含重复字符的子串,别小看滑动窗口
题目大概是这样的:给定一个字符串,找出其中不含有重复字符的最长子串的长度。输入"abcabcbb",输出3,因为"abc"是最长的无重复子串。输入"bbbbb",输出1。这题在LeetCode上是第3题,属于那种"看起来简单,写起来容易出各种小问题"的题目。
我当时的解题思路是滑动窗口加哈希表。核心逻辑是维护一个左边界和一个右边界,右边界不断向右扩展,每遇到一个新字符,就检查它上一次出现的位置是否在当前窗口内,如果在,就把左边界跳到那个位置的下一个字符处,保证窗口内永远没有重复字符。然后每次更新窗口长度,取最大值。时间复杂度O(n),空间复杂度O(n)。
int longestWithoutRepeatingChar(string s) { unordered_map<char, int> lastIndex; int left = 0, ans = 0; for (int right = 0; right < s.size(); right++) { char c = s[right]; if (lastIndex.find(c) != lastIndex.end() && lastIndex[c] >= left) { left = lastIndex[c] + 1; } lastIndex[c] = right; ans = max(ans, right - left + 1); } return ans; }这里有个关键细节我当时差点踩坑:更新左边界时,要加一个判断lastIndex[c] >= left。如果不加这个判断,当遇到一个字符上次出现的位置已经在窗口左边之外时,会把左边界往回拨,导致答案出错。举个例子,"abba"这个字符串,当右指针走到第二个a时,a上次出现的位置是0,但此时的窗口是[2,3],左边界是2,如果直接把left更新成1,窗口就会错误地扩大。所以必须加条件判断,确保只有当重复字符出现在当前窗口内时才移动左边界。
这题其实还有个小变形,如果字符串不是ASCII字符而是Unicode,哈希表的key就要用字符而不是字节。笔试时题目默认是ASCII,但我在面试中就被追问过这个变体,所以提醒大家留意。
2.2 第二题:链表环检测与环入口定位,快慢指针背后的数学原理
第二题是经典中的经典:判断一个链表有没有环,如果有,返回环的入口节点;如果没有,返回null。要求不能使用额外空间。输入是一个链表头节点,输出按要求返回节点。
思路就是快慢指针:快指针每次走两步,慢指针每次走一步。如果链表没有环,快指针会先到达链表尾部,直接返回null。如果有环,两个指针一定会在环内相遇。关键是怎么从相遇点找到环入口。这里有个数学推导:假设链表头到环入口的距离是a,环入口到相遇点的距离是b,环的周长是c。慢指针走了a+b步,快指针走了2(a+b)步。快指针比慢指针多走了a+b步,这个值一定是环周长的整数倍,也就是a+b = n*c。整理一下,从相遇点继续走a步就能回到环入口。所以算法是:相遇后,把一个指针放回链表头,另一个留在相遇点,两个指针每次都走一步,再次相遇的位置就是环入口。
ListNode* detectCycle(ListNode* head) { ListNode* slow = head; ListNode* fast = head; while (fast != nullptr && fast->next != nullptr) { slow = slow->next; fast = fast->next->next; if (slow == fast) { ListNode* ptr = head; while (ptr != slow) { ptr = ptr->next; slow = slow->next; } return ptr; } } return nullptr; }这道题说实话,思路记住了就不难,但很多人在笔试现场会卡在"为什么相遇后再走a步就是入口"这个推导上。我建议备考时不要只背代码,要把数学原理自己推导一遍,因为面试官很可能会顺着这题追问:"如果快指针每次走三步还能相遇吗?"答案是不一定,快慢指针步数差为1才能保证在有限步内相遇,如果快指针走三步,慢指针走一步,在某些环结构下快指针可能会永远跳过慢指针,导致无法检测到环。
实际笔试时,这题还需要处理空链表和单节点无环链表的情况,我见过有人因为没判空导致空指针异常,直接把测试用例挂掉,非常可惜。所以写任何链表题,第一步永远是处理空指针边界。
2.3 第三题:Top K 高频词,搜索引擎业务的缩影
第三题一出来我就笑了,这题太像搜狗会出的题了。题目大意是:给定一个非空的单词列表,返回出现次数最多的K个单词,返回结果应该按单词出现频率由高到低排序,如果两个单词出现频率相同,则按字母顺序排列。
这题考了两个核心点:一是哈希表统计词频,二是Top K的选择算法。最容易想到的做法是把所有单词按频率排序后取前K个,时间复杂度O(n log n),但这不是最优解。当单词总量很大、K很小的时候,更合适的做法是维护一个大小为K的最小堆,遍历词频表,堆满后如果新元素的频率比堆顶大,就弹出堆顶、压入新元素。这样时间复杂度是O(n log K),当K远小于n时,性能优势非常明显。
这题还有一个细节容易翻车:排序规则是频率降序、字母升序。用最小堆的时候,堆顶是"最应该被淘汰"的元素,所以堆顶要放的是"当前K个元素里频率最小、字母序最大"的那个。很多人在堆的比较器上写反,导致输出顺序和预期相反。我当时是这样写的:
struct Node { string word; int count; bool operator<(const Node& other) const { return count > other.count || (count == other.count && word < other.word); } }; vector<string> topKFrequentWords(vector<string>& words, int k) { unordered_map<string, int> freq; for (auto& w : words) freq[w]++; priority_queue<Node> pq; for (auto& p : freq) { pq.push({p.first, p.second}); if (pq.size() > k) pq.pop(); } vector<string> result(k); for (int i = k - 1; i >= 0; i--) { result[i] = pq.top().word; pq.pop(); } return result; }堆里每个元素存储单词和词频两个值。比较器的写法是:频率小的优先出队,频率相等时字母序大的优先出队。这样堆里留下的就是频率最大、字母序最小的那些词。最后从堆里取元素时,由于堆顶是最弱的,所以要倒序填充结果数组,保证输出按频率从高到低排列。
当时我写完这题后在最后的简答题里还顺手提到了:搜索引擎收集用户搜索日志、统计热门搜索词,本质上就是Top K问题的在线版本。这种跨题目联想不扣分,有时候反而会让面试官觉得你有业务sense,我后面在面试时确实被问到了相关内容。
3. 基础选择题考点串讲:不只是背八股
3.1 C++ 与 Java 语言基础高频考点
搜狗后端的选择题在语言基础上考得很细。C++方向我印象比较深的有这么几类:一是虚函数的实现机制,问"含有虚函数的类,其对象模型是什么",答案是对象内部有一个虚表指针,指向虚函数表,虚函数表里存的是函数指针;二是构造和析构的顺序,比如"派生类对象构造时,基类和成员对象的构造顺序";三是智能指针,像是shared_ptr的引用计数是线程安全的,但指向的对象并不线程安全;四是关于const和static的修饰规则,比如静态成员函数不能访问非静态成员变量。
Java方向的考点主要集中在JVM内存模型和垃圾回收。选择题里有一道印象很深:"JVM堆内存中,哪个区域用于存放新建对象"——答案是新生代的Eden区。还有一道关于HashMap在JDK1.8中的变化:底层由数组加链表改为数组加链表加红黑树,当链表长度大于等于8且数组长度大于等于64时转为红黑树。这里有两个前置条件,很多人只记住了8这个阈值,忘了数组长度还必须大于64,笔试时很容易被这种细节绕进去。
我的建议是语言基础这块没有捷径,就是把高频考点做成表格反复过。虚函数、智能指针、JVM内存分区、垃圾回收算法、集合类源码,这几个方向出现频率极高,每个都要做到能默写框架、能讲清楚底层原理的水平。我当时是把这些考点按"是什么、为什么这么设计、有什么坑、怎么答面试追问"四个维度整理成一个文档,考前一周每天过一遍,效果很好。
3.2 操作系统与计算机网络必背知识点
操作系统方向,搜狗笔试考了进程和线程的区别、死锁的四个必要条件、虚拟内存与页面置换算法。死锁那题考的是银行家算法的应用场景,问"系统能避免死锁的调度策略是什么",答案是银行家算法。页面置换那块考了LRU和FIFO的缺页次数对比,给定一个页面访问序列,需要手动模拟计数。这类题不能光记概念,必须亲手在草稿纸上模拟几次,不然考场上算着算着就乱了。
计算机网络方向,选择题密度很高。TCP三次握手和四次挥手几乎是必考的,搜狗也不例外。有一道题是"客户端主动关闭连接后,进入TIME_WAIT状态的是哪一方",答案是主动关闭方,也就是客户端。TIME_WAIT持续时间为2MSL,作用是保证最后一个ACK能到达对端,同时让旧连接中的报文在网络中自然消失。这个知识点我不止一次在笔试里见到,属于网络部分的钉子户。
HTTP相关的题也考了一道:"HTTP 301和302的区别是什么"——301是永久重定向,302是临时重定向。搜索引擎对这个很敏感,因为重定向类型会影响爬虫和索引策略。搜狗是搜索公司,它的笔试里出现这种和业务相关的网络题非常正常,大家在准备这类公司时要注意把基础知识和业务场景挂钩。
3.3 数据库与搜索相关的技术题
数据库部分,索引和事务是两道重头题。索引那道题问的是"InnoDB的默认索引结构是什么",答案是B+树,不是B树,也不是红黑树。为什么是B+树?因为B+树的非叶子节点不存储数据,每个节点可以存储更多索引项,树的高度更低,磁盘IO次数更少;同时叶子节点用指针相连,适合范围查询。这个"为什么"一定要理解,因为选择题只是热身,面试时一定会被追问原理。
事务那道题考的是隔离级别,给了四个场景分别对应哪种隔离级别。比如"一个事务读取到另一个事务已提交的数据,但在当前事务里前后两次读到的数据不一致",这是不可重复读,需要RR级别才能解决。四个隔离级别的区别和各自的并发问题要能流利说出来:读未提交、读已提交、可重复读、串行化,分别解决的问题是脏读、不可重复读、幻读。
作为搜索公司的笔试,还考了一道关于倒排索引的选择题:"倒排索引中,词典的主要作用是什么",答案是记录每个词项对应的倒排列表的位置或指针,用于快速定位。这道题其实很友好,只要知道倒排索引的基本结构都能做对。但如果想加分,可以进一步思考:词典本身用什么数据结构存?哈希表、B树还是跳跃表?不同方案在内存占用和查询效率上有什么权衡?笔试虽然只考选择,但思考深了面试环节会非常占优势。
4. 简答题与系统设计:搜索引擎公司爱考什么
4.1 经典短链服务的后端设计思路
设计题给了一个很常见的场景:设计一个短链接服务,用户可以提交一个长URL,系统返回一个短网址;用户访问短网址时,能302重定向到原始长URL。要求写出核心架构、存储方案、ID生成策略,以及可能遇到的性能瓶颈。
这题我拆成了四层来答。第一层是ID生成:短网址的字符集是大小写字母加数字共62个字符,如果短码长度是7位,能表示62的7次方,大约是3.5万亿个URL,足够用。生成方式我推荐雪花算法,它由时间戳、机器ID、序列号组成,64位整数,转换成62进制就是短码。雪花算法的好处是趋势递增、分布式环境下不冲突、生成效率高。也可以选数据库自增ID加进制转换,但高并发下会有单点压力。
第二层是存储设计:用一个映射表存储短码和长URL的对应关系,主键是短码,字段包括原始URL、创建时间、过期时间、点击次数。为了查询性能,直接用短码作为主键索引即可,不需要额外建索引。但如果要支持按用户查询短链列表,就需要加一个user_id字段并建索引。
第三层是跳转流程:用户访问短网址,服务器解析短码,查存储拿到长URL,返回302重定向响应,浏览器自动跳转到长URL。这里有个小细节要说明:为什么选302而不是301?因为301是永久重定向,浏览器和CDN会缓存,以后想统计点击量或修改目标URL就麻烦了;302是临时重定向,每次都会经过短链服务,便于统计和运营。
第四层是性能与容灾:短链服务读多写少,完全可以加一层Redis缓存,热点短码的请求打到Redis上,减轻数据库压力。如果某个短码被恶意刷量,还需要限流。我当时还加了一句:因为需要记录点击日志用于分析,可以考虑用消息队列异步写入,避免同步写日志拖慢主流程。这一句虽然简单,但能体现对高并发场景的思考。
4.2 一个查询背后的完整流程
除了短链设计,我还把搜索引擎查询的完整流程也梳理了一遍,因为这个是搜狗肯定会关注的方向。题目大概是这样:用户在搜索框输入关键词后,后端系统需要经过哪些步骤才能返回搜索结果?
我按流水线来答。第一步是分词:中文文本没有天然分隔符,需要用分词器把句子切成词项。比如"北京烤鸭"会被切分成"北京"和"烤鸭",也有可能被切分成"北京烤鸭"整体。分词策略直接决定召回结果的准确性,常见做法是正向最大匹配加词典,现在也有基于统计语言模型的分词方案。
第二步是查询分析:除了分词,还要做拼写纠错、同义词扩展、意图识别。用户搜"北京烤鸭店",系统可能要识别出"店"是查询意图的词,和"北京烤鸭"组合成一个完整的查询条件。
第三步是召回:将分词后的词项去倒排索引里查对应的倒排列表,取交集或并集,得到候选文档集合。这一步的性能压力最大,因为倒排列表可能非常长,需要用跳表指针做快速合并。
第四步是排序:对候选文档做相关性打分,传统方法是BM25算法,核心是TF-IDF思想的扩展,同时考虑文档长度归一化。现在搜索引擎还会加一堆业务特征,比如文档质量分、点击率、时效性等,用机器学习模型融合这些特征打分。
第五步是展示:把排序结果返回前端,同时记录用户的点击行为日志,用于后续优化排序模型。整个链路对时延要求极高,搜索引擎后端做到百毫秒级返回,靠的是缓存、索引分片、并行计算等手段。这道题答好了,基本能看出一个人有没有后端系统的全局视野,比单纯背数据结构更能拉开差距。
5. 笔试实战避坑与备考建议
5.1 编码题最容易丢分的5个细节
第一个细节是边界条件。字符串题的空串、单字符串,链表题的空链表、单节点,数组题的越界,这些都是最容易导致用例不过的原因。我建议每道编程题写完,立刻用几个特殊输入自测一遍,用不了30秒,但能救回很多用例分。
第二个细节是返回值约定。题目要求返回长度还是子串本身?要求输出节点还是节点的值?要求升序还是降序?看清楚再动手,别写完了才发现方向错了。
第三个细节是输入输出格式。在线笔试系统通常要求自己写输入输出,而不是像LeetCode那样只写核心函数。有些同学平时刷题习惯了LeetCode的套路,笔试时忘记处理标准输入输出,整道题直接零分。平时练习时就要习惯用cin和cout或者Scanner自己写完整的主函数,避免考场上不适应。
第四个细节是复杂度预估。看到数据范围再去选算法。比如n是10的5次方级别,O(n^2)大概率超时,必须上O(n log n)甚至O(n)的解法。如果题目明确说数据量很小,那暴力法反而最快最稳。
第五个细节是编程语言的选择。搜狗笔试支持多种语言,选自己最熟悉的那个,不要在考场上尝试新语言。C++选手要注意STL容器的时间复杂度,Java选手要注意对象创建的开销,Python选手要注意循环性能,必要时用内置函数。
5.2 时间分配策略与答题顺序建议
整套题的时间分配,我给一个可复用的模板。拿到卷子先花2分钟整体浏览一遍,确认题型和题量。先做选择题,遇到卡壳超过1分钟的先标记跳过去,因为选择题分值一般不大,不值得死磕。编程题就按题号顺序做,先写框架再补细节,不要一上来就追求一次写对。设计题放到最后,但至少要留15分钟,写出框架就不亏,完全不写就很伤了。
我还想强调一个策略:编程题如果卡住了,先把暴力解法写了拿部分分,千万不要空着。很多在线笔试系统是按用例给分的,暴力法也能通过一部分简单用例,拿到30%到50%的分值,这比一道题完全空着要强得多。我见过太多同学因为追求最优解,卡在优化上,最后连暴力分都没拿到。
5.3 从笔试到面试的衔接准备
笔试结束不是终点,而是面试的起点。大厂面试官有个习惯,就是拿你笔试的代码当面试素材。我就在面试中被问过:"你笔试第二题用了快慢指针,能证明一下为什么第二次相遇点就是环入口吗?"或者"第三题你用了最小堆,如果内存放不下所有词频怎么办?"
所以笔试结束到面试开始那段时间,一定要把自己写的代码重新过一遍,确保每一行都能讲出理由。我不会建议你在笔试时写超出自己理解的代码,因为面试官一追问就会露馅。宁可写一个自己能讲清楚但不算最优的解法,也不要抄一个背下来的高级解法,面试官连续追问两轮你就顶不住了。
备考阶段,我个人的做法是刷题时给题目打标签,标注它可能被追问哪些问题,然后自己模拟面试官问自己。比如刷到"最长不重复子串",我就追问自己"如果字符串里有中文怎么办";刷到"LRU缓存",就追问"为什么用双向链表而不是单向链表"。这样笔试面试一次性准备到位,效率比单纯刷题高得多。
最后分享一个小技巧
这套笔试题复盘完,我最想强调的一点是:大厂后端校招笔试,从来没有所谓的"偏题怪题",所有题目都指向同一个目标——考察你有没有扎实的计算机基础、能不能写出干净的代码、有没有系统级思维。搜狗第一场笔试的三道编程题,从滑动窗口到快慢指针再到Top K,每一道都能在LeetCode上找到类似的题,但需要灵活运用;设计题也是常见的短链服务,拼的是思路完整度。
备考冲刺阶段,我特别推荐一个练习方式:按题型分类限时刷题,模拟真实笔试的紧张感。我自己当时是每天上午固定两个小时,用牛客网的模拟笔试功能做一套真题,然后花一小时复盘,把错题和低效解法整理进笔记。坚持一个月,笔试状态肉眼可见地提升。笔试不只是考你会不会,更考你在限时高压下能不能稳定输出,这个能力只能靠模拟训练来培养。