这套2016年的搜狐研发工程师笔试题目,放在2025年的今天来看,表面上是一套“老古董”,实际上我每次给准备校招的学弟学妹做模拟训练时,都会把其中几道题翻出来。原因很简单:它不像现在很多笔试那样偏、怪、堆砌冷门模型,而是老老实实地考基本功,链表、字符串、数组、动态规划、贪心,每一题都在测试“你写代码的时候脑子是否清醒”。
而且这几年大家刷题的语言渐渐从C++转向Python,2025年3月的Python等级考试里也出现了大量与这类经典笔试题同源的编程题。所以这篇文章我会把搜狐2016研发工程师编程题里比较有代表性的几类题目做一次深度拆解,配上C++和Python两种实现思路,把题目背后的考察点、容易踩的坑、笔试时的答题顺序策略一次性讲明白。适合正在准备校招笔试、跳槽机试,或者单纯想把算法基础打扎实的人阅读。
1. 这套2016年的笔试题,为什么现在还值得刷
很多应届生一看题库写着“2016年”,第一反应就是“太旧了,不考了”。这个想法其实会害了你。老牌互联网公司的题库有一个特点:核心题型非常稳定,换汤不换药。搜狐这套2016研发工程师编程题,覆盖的知识点恰好是后面十年笔试的高频区间,与其去刷一堆乱七八糟的新题,不如先把这套题的解题范式吃透。
1.1 搜狐这套题的难度定位
从公开流传的题库来看,搜狐研发岗的笔试编程题通常是2到4道,难度梯度非常明显:第一题基本是模拟或字符串处理,送分题;第二题开始上排序、双指针、二分这类常规算法;最后一道往往落到动态规划或者贪心,用来区分“会写代码的人”和“真正懂算法的人”。
这个梯度设计其实和现在大部分中大型公司的笔试是一致的。所以刷这套题的时候,你应该抱着“模拟真实笔试环境”的心态去练,而不是做一道看一道答案。
1.2 从老题看大厂题型变化
有人会问:现在大厂笔试都爱考什么?我的观察是,基础数据结构题的比例在回升,但提问方式更“工程化”了。比如以前直接问“反转链表”,现在会包装成“实现一个任务调度器中等待队列的逆序输出”;以前问“括号匹配”,现在可能变成“校验某种配置文件的括号是否合法”。
搜狐2016的编程题里就有不少这种“裸题”,当时的裸题到现在变成了各种包装题的内核。如果你连裸题都写不利索,面对包装题基本就是死路一条。反过来,你把裸题的边界情况全部想清楚,包装题只是多花点时间读懂题目而已。
1.3 2025年用Python刷老题的现实意义
结合2025年3月Python等级考试一级编程题的出题方向来看,越来越多初学者开始用Python学算法。Python在笔试中的优势很明显:代码量少、调试快、不容易因为指针和内存问题卡壳。但它的劣势也在这里——太多人用Python的时候不分析复杂度,一上来就全用切片、集合、内置函数,结果面试官一问时间复杂度就懵了。
所以我的建议是:用Python刷搜狐2016这套题时,每道题都强制自己用“基础数据结构 + 显式逻辑”实现一遍,尽量少用黑魔法。比如反转字符串,别直接切片倒序就结束了,老老实实双指针走一遍。这样面试时手撕代码才不会露怯。
1.4 我选取的题目范围说明
由于完整原题的流传版本很多,我在本文中挑选的是多年来被反复讨论、也是最符合搜狐这套题风格的几类代表题型,包括字符串翻转与括号匹配、数组中第K大元素、合并区间、滑动窗口最大值、动态规划与贪心经典模型。
这些题目在公开题库中都能找到对应版本,我会在每道题上标明考察点、完整思路、可用代码和易错点。你可以把它当成一份“高频题型复习地图”,也可以当成一次模拟实战来连刷。
2. 高频题型拆解:字符串与模拟题的易错点
字符串题在笔试里属于“看起来简单,做起来一堆bug”的类型。搜狐这套题里的字符串题难度不高,但非常考验基础是否扎实。我遇到过太多人在这类送分题上丢分:有人是索引没理清,有人是没考虑空串,有人是没注意大小写。下面这两类题目,基本可以代表搜狐2016题单中字符串题的考察风格。
2.1 字符串翻转问题
题目模型:给定一个英文句子,要求将句子中的单词顺序翻转,但单词本身保持原来的字符顺序。比如输入“I am a coder”,输出“coder a am I”。
这是搜狐笔试题单里的经典题型,后续很多公司都出了变体版:有的要求去掉首尾空格,有的要求单词间只保留一个空格,有的要求标点符号跟着单词走。但我建议先把最基础的版本写对。
思路很简单:先整体反转整个字符串,然后遍历字符串,遇到空格或字符串结尾时,反转当前单词。
C++版本:
#include <iostream> #include <string> #include <algorithm> using namespace std; string reverseWords(string s) { // 先反转整个字符串 reverse(s.begin(), s.end()); int n = s.size(); int start = 0; while (start < n) { // 跳过空格 while (start < n && s[start] == ' ') start++; int end = start; while (end < n && s[end] != ' ') end++; // 反转当前单词 reverse(s.begin() + start, s.begin() + end); start = end; } return s; }Python版本:
def reverse_words(s: str) -> str: chars = list(s) n = len(chars) def reverse(left: int, right: int) -> None: while left < right: chars[left], chars[right] = chars[right], chars[left] left += 1 right -= 1 reverse(0, n - 1) i = 0 while i < n: while i < n and chars[i] == ' ': i += 1 j = i while j < n and chars[j] != ' ': j += 1 reverse(i, j - 1) i = j return ''.join(chars)这里最容易犯的错是:整体反转之后,单词内的字符顺序也是反的,很多人就停在这一步,直接返回了。笔试时这种错误特别冤,因为自己本地测试时经常用对称单词“aba”这种,根本测不出问题。建议专门用“I am a coder”这种不对称的用例来验证。
2.2 括号匹配问题
题目模型:给定一个只包含左右小括号的字符串,判断是否为合法括号序列。进阶版会扩展到中括号、大括号,甚至要求返回第一个不匹配的位置。
搜狐2016这类题目的考察核心是“栈”这个数据结构。左括号入栈,右括号出栈并匹配。最后检查栈是否为空。
Python版本:
def is_valid_brackets(s: str) -> bool: stack = [] pairs = {')': '(', ']': '[', '}': '{'} for ch in s: if ch in '([{': stack.append(ch) else: if not stack or stack[-1] != pairs[ch]: return False stack.pop() return not stackC++版本:
#include <iostream> #include <stack> #include <string> using namespace std; bool isValid(string s) { stack<char> st; for (char ch : s) { if (ch == '(' || ch == '[' || ch == '{') { st.push(ch); } else { if (st.empty()) return false; if (ch == ')' && st.top() != '(') return false; if (ch == ']' && st.top() != '[') return false; if (ch == '}' && st.top() != '{') return false; st.pop(); } } return st.empty(); }这个题有两个高频错误点。第一个是忘记在遇到右括号时先检查栈是否为空,如果栈空了还去取栈顶元素,就会导致崩溃或者越界,这属于笔试中的致命伤。第二个错误是在循环结束后忘记检查栈是否为空,输入是“(()”这种左括号多余的情况就会被漏掉。
2.3 模拟计算器表达式
搜狐的题单里有不少“模拟题”,比如实现一个只含加减乘除和括号的表达式求值。这种题不会出现特别复杂的优化,但非常考察对栈和运算符优先级的理解。
我建议的通用做法是:使用两个栈,一个存数字,一个存运算符。遍历表达式时:
- 遇到数字就解析出完整数字,入数字栈。
- 遇到左括号直接入运算符栈。
- 遇到右括号则一直弹出运算符计算,直到遇到左括号。
- 遇到运算符,则先处理栈中优先级不低于当前运算符的运算符,再入栈。
- 最后清空运算符栈。
这个逻辑背后的原理并不复杂。乘除优先级高于加减,所以遇到加减时,前面如果有乘除必须先把它们算完,否则结果全乱。括号则将局部计算强行隔离,优先处理。
笔试时这种模拟题真不建议现场硬刚全功能版本。先把不带括号的加减乘除版本写出来,再考虑括号,一步一步加,而不是一上来就试图写一个三百行的完整求值器。搜狐这类题目的判题用例通常不会卡得太严,能够处理常见表达式就足够拿大部分分数。
3. 排序与数组算法:从暴力到优化的推进过程
数组和排序相关的题目在搜狐2016研发工程师编程题里占比不小。这类题看起来“人人都会”,但区分度恰恰体现在复杂度控制上。下面这三道代表题型,我会从暴力解法讲起,再一步步推到更优解法,帮助大家建立一个完整的优化链条。
3.1 数组中第K大元素
题目模型:给定一个无序整数数组,找出其中第K大的元素。
这道题是我在搜狐题单里见得最多的一类变体题,现在各大公司的题库里依然非常活跃。最直观的解法是排序后按下标取,时间复杂度是O(n log n)。笔试中只要K接近数组长度,这个解法完全够用。
但如果你想展示扎实的基本功,可以用快速选择算法,期望时间复杂度降到O(n)。核心思路借鉴快速排序的partition过程:随机选择一个基准,把数组分成大于基准和小于基准的两部分,然后判断第K大的元素落在哪一部分,只对那部分继续递归,而不是像快排那样两侧都处理。
Python版本:
import random def find_kth_largest(nums, k): target_index = k - 1 left, right = 0, len(nums) - 1 def partition(l, r): pivot_index = random.randint(l, r) nums[pivot_index], nums[r] = nums[r], nums[pivot_index] pivot = nums[r] i = l for j in range(l, r): if nums[j] > pivot: nums[i], nums[j] = nums[j], nums[i] i += 1 nums[i], nums[r] = nums[r], nums[i] return i while left <= right: pos = partition(left, right) if pos == target_index: return nums[pos] elif pos < target_index: left = pos + 1 else: right = pos - 1 return -1这里要注意:第K大指的是从大到小排,第K个位置。很多人在“大”和“小”上反了,导致答案完全错误。笔试时,如果时间紧张,直接用排序解法是更稳妥的方案,因为快选的随机性在极端情况下可能退化到O(n²),虽然概率低,但笔试环境心态紧张时不要给自己找麻烦。
3.2 合并区间问题
题目模型:给定若干区间,把有重叠的区间合并。
这道题在搜狐题单中出现的原因是它考察了两个点:排序和边界处理。很多人想到了排序,但排序规则没定清楚,或者合并条件写错。
正确的做法是先按区间起点升序排序,然后遍历区间,维护当前合并区间的起点和终点。如果当前区间的起点小于等于当前合并区间的终点,说明有重叠,更新终点为两者较大值;否则把当前合并区间加入结果,开始新的区间。
C++版本:
#include <vector> #include <algorithm> using namespace std; vector<vector<int>> merge(vector<vector<int>>& intervals) { if (intervals.empty()) return {}; sort(intervals.begin(), intervals.end()); vector<vector<int>> res; int start = intervals[0][0], end = intervals[0][1]; for (int i = 1; i < intervals.size(); i++) { if (intervals[i][0] <= end) { end = max(end, intervals[i][1]); } else { res.push_back({start, end}); start = intervals[i][0]; end = intervals[i][1]; } } res.push_back({start, end}); return res; }这个题的坑在于区间边界“是否闭合”。如果题目说区间是闭区间,起点等于上一个区间终点时就要合并;如果区间是开区间,则起点等于终点时不合并。搜狐这套题默认闭区间,但有些变体的题目会特意强调边界条件,务必仔细。
3.3 滑动窗口最大值问题
题目模型:给定一个数组和一个窗口大小k,窗口每次向右移动一格,求每个窗口内的最大值。
这道题的暴力解法非常容易想:枚举每个窗口,扫描窗口内的k个元素求最大值,时间复杂度O(nk)。当n和k都很大的时候,必超时。所以搜狐这类题目的主要考察点是单调队列。
单调队列的核心思想:队列中存的是数组下标,并且保证从队头到队尾对应的数组值是单调递减的。这样队头永远是当前窗口的最大值。每次窗口移动时,先去掉掉出窗口的队头下标,再不断从队尾弹出值小于当前元素的元素,然后把当前元素下标入队。
Python版本:
from collections import deque def max_sliding_window(nums, k): dq = deque() res = [] for i, v in enumerate(nums): # 弹出窗口外的元素 if dq and dq[0] <= i - k: dq.popleft() # 从尾部弹出所有小于当前值的元素 while dq and nums[dq[-1]] <= v: dq.pop() dq.append(i) if i >= k - 1: res.append(nums[dq[0]]) return res这里有没有同学会问:为什么从队尾弹出的是“小于等于”而不是“小于”?其实用“小于”也不会错,因为相等值的下标留在队里会导致过期判断更频繁,但用“小于等于”会让队列更紧凑,性能更好。笔试时这两种写法都能通过,我习惯用“小于等于”。
这种题在日常业务的“日志最近K条统计”或“流式数据滑动窗口计算”中都能找到对应场景,属于典型的“代码不长但思路很值钱”的题目。
4. 动态规划和贪心:笔试中最容易翻车的两类题
动态规划和贪心题在搜狐2016研发工程师编程题中属于拉开差距的部分。很多人的问题在于:入门基础还行,但一碰到需要自己推导状态转移方程的题就卡壳。本质上还是练得不够多。下面这两道题,一个侧重状态设计,一个侧重贪心策略证明,恰好是两类题型的代表。
4.1 最长上升子序列
题目模型:给定一个无序数组,求最长严格上升子序列的长度。这里的“子序列”不是“子数组”,不要求连续。
这道题的经典动态规划解法是:定义dp[i]表示以第i个元素结尾的最长上升子序列长度。状态转移方程是:dp[i] = max(dp[j] + 1) 其中 j 从 0 到 i-1,且 nums[j] < nums[i]。
初始时,每个位置的dp[i]都至少为1,因为单个元素本身就是一个上升子序列。最后答案取dp数组的最大值。
C++版本:
#include <vector> #include <algorithm> using namespace std; int lengthOfLIS(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; vector<int> dp(n, 1); int ans = 1; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = max(dp[i], dp[j] + 1); } } ans = max(ans, dp[i]); } return ans; }这个版本的时间复杂度是O(n²),笔试中n在1000以内都没问题。如果n达到10的5次方量级,就必须使用“贪心 + 二分”的优化版本,用tail数组维护当前长度下的最小末尾值。
我建议:笔试中优先写O(n²)的DP解法,因为逻辑清晰、不容易写错。只有明确看到数据范围很大时才去挑战优化版。很多人直接写优化版,边界条件没理清,反而扣分更多。
4.2 股票买卖问题
题目模型:给定一个数组,第i个元素表示第i天的股票价格,允许最多完成一笔交易,求最大利润。
为什么说这类题容易翻车?因为很多人会下意识选择贪心:找到最低点,然后找最高点。但“最低点”和“最高点”不一定能配对,因为最高点可能出现在最低点之前。
正确做法是动态规划思想下的滚动变量法:用一个变量minPrice记录遍历过程中的最小价格,用另一个变量maxProfit记录当前能获得的最大利润。每遍历一天,尝试用当天价格减去minPrice,更新maxProfit。
Python版本:
def max_profit(prices): if not prices: return 0 min_price = prices[0] max_profit = 0 for price in prices: min_price = min(min_price, price) max_profit = max(max_profit, price - min_price) return max_profit这里需要注意什么?注意题目说的是“最多完成一笔交易”,如果改成“可以完成多笔交易”,解法就完全变了,变成贪心:只要后一天价格比前一天高,就累积利润。很多人在考场上没看清“一笔”还是“多笔”,用了错误模型,整道题垮掉。这是我在真实笔试图上见过最可惜的丢分原因。
4.3 动态规划题目的通用解题套路
既然聊到动态规划,不妨把通用流程也整理一遍。我试过很多次,只要按这个顺序走,即便不是特别的DP高手,也能稳定做出中等难度的DP题。
- 第一步:明确状态。问自己:我关心哪些变量?比如最长上升子序列关心“以某个位置结尾时的长度”,股票问题关心“当前天数、手上是否有股票”。
- 第二步:写出转移方程。用文字描述“当前状态从哪里来”,再转成代码。
- 第三步:确定初始化和遍历顺序。dp数组的初始值是什么?从前往后还是从后往前?
- 第四步:确认答案落在哪里。是dp数组的最后一个值,还是dp数组的最大值?
还有一个细节:DP写完之后一定要手动跑一个小用例,在草稿纸上画一遍状态转移表。很多错误光看代码根本发现不了,但一画表就原形毕露。
5. 从真题到练法:怎么利用老题备战招聘笔试
这部分写给正在准备笔试的读者。刷题和实战其实是两码事,很多人平时刷题六六六,一到笔试就翻车,原因往往不是能力问题,而是答题策略出了问题。以下是我在多次模拟笔试和真实笔试中总结出来的经验。
5.1 答题顺序:先易后难,别在第一题死磕
搜狐这套题的难度分布很有代表性,正式笔试时,我强烈建议按照题目的难度评估来决定答题顺序,而不是按照题目给出的顺序。先把所有题都看一眼,找到最有把握的两道题先写。
为什么?因为笔试的评分通常按用例通过率给分,偶尔出现一道和某道题完全没法下手的情况,死磕不出来,后面的送分题又没时间写,这才是最亏的。我个人的策略是:如果一道题超过25分钟还没有明确思路,先标记起来,去做别的题,最后留时间回来写暴力解法,能过多少用例算多少。
5.2 用例设计:笔试判分的背后逻辑
笔试系统的判分逻辑是跑一组一组测试用例,不是看了你的代码觉得“思路正确”就给满分。所以边界用例非常重要。很多人的代码在示例用例上跑得好好的,一提交就挂,绝大多数情况都是败在边界条件上。
怎么练习边界用例设计?每写完一道题,强迫自己问几个问题:
- 输入为空的情况怎么处理?
- 输入只有一个元素的情况怎么处理?
- 数组长度为1,窗口长度为1,怎么处理?
- 全是相同元素怎么处理?
- 数组已经有序/逆序怎么处理?
- 存在负数或0怎么处理?
把这些情况都列出来,再一一验证自己的代码。这个方法比盲目做十道新题都管用。
5.3 代码规范:别因为是笔试就随便乱写
有些读者会觉得,笔试只要答案对就可以了,代码乱一点无所谓。这是很大的误区。笔试系统确实不看代码风格,但你的代码是给自己看的,如果你一边写一边重构,变量名乱起,缩进不对齐,很容易在调试的时候把自己绕晕。
我的建议是:笔试时也用正常的代码规范写。变量名用有含义的英文名,缩进保持一致,关键逻辑旁边写注释。这不仅是给阅卷人看,更是给自己留一条清爽的思路。我自己就吃过亏:有一次笔试时间紧,变量名直接a、b、c、d乱用,结果提交前调试bug时根本分不清哪个变量是窗口左边界,浪费了至少五分钟。
5.4 刷题复盘:把老题的价值榨干
刷搜狐2016这类老题有一个其他题库替代不了的好处:题目数量有限,且每一道都有清楚的考察点,特别适合做专题复盘。
我的复盘方法是:每刷完一套题,花15分钟写一份简单的错题笔记,记三件事:
- 这道题考的核心知识点是什么?
- 我第一次做的时候卡在了哪里?
- 下次遇到类似题型,第一时间应该想到什么解法?
这份笔记不需要多华丽,甚至不需要给别人看。但它能帮你把一套题从“我刷过了”变成“我完全吸收了”。很多人的刷题量很大,但知识体系一团浆糊,原因就是缺少了这一层反思。
5.5 善用Python协助老题复习
2025年3月Python等级考试一级的题目,很多都把经典的数组、字符串操作包装成了“计算题”、“统计题”,核心逻辑和搜狐2016的题目非常接近。这给我们的启发是:用Python刷老题,不必只追求刷完,还可以把同一道题用不同的Python语法特性实现两三遍。
比如反转字符串,你可以用双指针写一次,再用切片写一次。合并区间可以用传统循环写一次,也可以尝试理解一下链式操作的写法。这样做的目的不是炫技,而是让你对语言更熟,笔试时不管用什么语言都能快速输出。
写在最后的一点个人经验
我自己从求职者变成面试官之后,最大的感受是:笔试题目从来不是为了难倒你,而是为了在最短时间内看出你有没有扎实的代码功底。搜狐2016研发工程师编程题虽然年份看起来很久远,但它考察的那些底层能力——逻辑清晰、边界齐全、复杂度有数、代码规范——到现在依然是拿到offer的通行证。
如果你手头正好有几套这种老牌公司的历史真题,别嫌它们旧。找个完整的时间段,关掉手机通知,设定好时间,模拟一次真正的笔试。做完之后认真复盘,把每个卡住的点都搞清楚。连续做三套以上,你会明显感受到自己写题时的状态不一样了。笔试考的不只是知识,更是你在有限时间下的稳定输出能力,这种能力只能靠平时一次次模拟训练喂出来。