2024年春招的小红书研发岗第二批笔试已经过去一阵子了,但后台还是陆续有人私信我问当时考了什么、难度怎么样、后续有没有面试消息。趁这几天把记忆里的内容重新捋了一遍,把这次笔试的题型分布、算法题思路、时间分配策略和一些容易踩的坑整理成一篇完整的复盘。不管你是准备投下一批秋招,还是纯粹想看看互联网大厂笔试现在是个什么难度,这篇应该都能给你一些参考。
先说结论:这次笔试的整体难度中等偏上,题型分两大块——客观选择题和两道编程题。选择题覆盖面比较广,操作系统、计算机网络、数据库、Java/Golang基础都有涉及;编程题一道偏数据结构实现,一道偏动态规划,整体不是那种纯刷题就能碾压的难度,更看重思路是否清晰、代码基本功扎不扎实。接下来一个一个拆开讲。
1. 笔试整体感知与准备方向
1.1 批次安排和笔试平台的大致情况
小红书每年的校招笔试都会分多个批次滚动安排,2024年春招的研发岗也延续了这种节奏。第二批笔试和第一批的时间间隔不算太长,所以从第一批考完到第二批之间,坊间已经流传了一些题型回忆,大致能看出出题风格——选择题比重不小,而且不是那种送分题,很多都带实际场景。第二批的题目整体框架和第一批是类似的,这算是比较幸运的地方,等于变相给了后来者一个不大不小的信息差。
笔试用的平台是市面上比较常见的在线笔试系统,支持多种主流语言。有一点要特别提醒:这类平台普遍都是ACM模式,也就是你需要自己处理输入输出格式。很多平时习惯在LeetCode上写核心代码的同学,第一次接触这种模式会非常不适应,我见过不少人挂在IO处理上,题目本身会做但输入解析写错了,最后0分。这个细节后面我会单独讲。
1.2 第二批笔试的题型构成
从整体结构来看,这次笔试大致分成两部分:
- 第一部分是选择题,大约20道左右,涉及计算机基础四大件加一部分语言特性。
- 第二部分是编程题,一共两道,难度递进,第一道偏简单/中等,第二道偏中等/困难。
选择题的数量看起来不算特别多,但每道题都需要一定的思考时间,尤其是那些结合场景的分析题,比单纯背概念要费时间得多。编程题两道题的分值占比通常比选择题高不少,是拉分的主要战场,所以时间分配上要格外注意。
从第二批的实际反馈来看,选择题里Linux命令、进程线程、数据库隔离级别这些几乎是必考项;编程题则比较偏爱考数组操作、动态规划、贪心思想,偶尔会掺一点树和图的题。下面我把各部分逐一拆开,给出具体的考点和解题思路。
2. 选择题高频考点拆解
这一部分我按科目整理了一下,方便大家对照排查自己的薄弱点。选择题的分布不一定是均匀的,但下面这些方向确实出现了不止一次,后续批次的准备可以重点覆盖。
2.1 操作系统与Linux命令
操作系统的题主要集中在进程管理、线程模型、死锁和内存管理这几个模块。比如进程和线程的本质区别、上下文切换开销、死锁的四个必要条件、进程间通信方式(管道、消息队列、共享内存、信号量)各自的特点和适用场景,这些都属于高频考点。
这次给我印象很深的一道题是关于死锁的:给出一段多线程并发场景,问哪种方式不能有效避免死锁。选项里有互斥锁、银行家算法、资源有序分配法、信号量。表面上看四个选项都和“并发控制”有关,但仔细分析就会发现,互斥锁本身是产生死锁的条件之一,它不能“避免”死锁,只是提供互斥能力。这种题就是典型的“看起来都会,一做就错”,考的就是对概念的深度理解,而不是死记硬背。
Linux命令也是选择题里出镜率很高的一块。常考的无非是文件权限管理(chmod、chown)、进程查看(ps、top、kill)、网络工具(netstat、ping、traceroute)、文本处理(grep、awk、sed)。其中awk和sed的组合使用是很多人的盲区,建议把这两个命令的常见用法刷熟练,尤其是awk的字段分割和内置变量,基本每年都会出题。
2.2 计算机网络与数据库
计算机网络的重点非常固定:TCP三次握手和四次挥手、TCP与UDP的区别、HTTP状态码语义、HTTPS的握手流程和加密机制、DNS解析过程、Cookie与Session的区别。这次考了一道TCP拥塞控制的题,问的是慢启动阶段窗口大小如何增长,以及什么情况下会进入拥塞避免。这类题其实不难,但如果对《计算机网络》里的那张拥塞窗口变化图没有形成直观记忆,现场推导很容易乱。
数据库这块,SQL语法是一个必考点,但它考的不是简单的select,而是多表join、子查询、group by + having的组合。所以不要只背单表查询,连表查询的各种写法一定要亲手在本地跑几遍。另一个高频考点是事务隔离级别:读未提交、读已提交、可重复读、串行化,这四种隔离级别分别解决什么问题、会产生什么并发异常,必须能默写出来。MVCC的底层实现(undo log版本链、ReadView规则)也是近几年互联网公司笔试的常客,值得深入看一遍。
2.3 数据结构与语言基础
数据结构的选择题不算特别多,但会考一些需要计算的复杂度问题。比如给定一个递归式,让你求时间复杂度——主定理是这种题最快的解法,建议专门看一下。另外,哈希表冲突解决方法、二叉树遍历序列的互推(已知前序中序求后序)这类题也出现过。
语言基础部分,Java和Golang都会考一点。Java这边,HashMap在JDK 7和JDK 8之间的区别(红黑树引入、头插法变尾插法)、ConcurrentHashMap的锁粒度变化、JVM内存区域划分和GC Roots,这些都算高频中的高频。Golang这边,goroutine与channel的基本使用、GMP调度模型、slice和array的区别,考察的概率也不低。如果你主语言是Java,建议至少把Goroutine和channel的基础概念过一遍,不然遇到Golang的选择题会比较吃亏。
下面用一张表总结一下选择题的高频考点和复习优先级:
| 科目 | 高频考点 | 复习优先级 |
|---|---|---|
| 操作系统 | 进程线程、死锁、内存管理、Linux命令 | 高 |
| 计算机网络 | TCP/IP、HTTP、HTTPS、DNS | 高 |
| 数据库 | SQL、隔离级别、MVCC、索引原理 | 高 |
| Java基础 | HashMap、JVM、并发编程 | 高 |
| Golang基础 | goroutine、channel、GMP模型 | 中 |
| 数据结构 | 复杂度分析、二叉树、哈希表 | 中 |
3. 编程题全复盘与解题思路
编程题是笔试的核心,也是区分度最高的部分。第二批次的两道题,我按回忆整理出来,题型和原题不完全一致,但考察的知识点和难度水平是很接近的。我会给出详细的思路推导和可跑的代码,方便你照着练。
3.1 第一题:连续子数组区间计数
这道题的大意是这样的:给定一个长度为n的整数数组和一个目标值k,要求统计数组中所有满足“子数组元素之和小于等于k”的连续子数组个数。n的范围大概是10的5次方,所以O(n^2)的暴力解法必挂,需要优化到O(n)。
第一眼看到这个题,思路其实很清晰:连续子数组求和,自然想到前缀和。定义前缀和数组pre[i]表示前i个元素的和,那么子数组[j, i]的和就是pre[i] - pre[j-1]。问题转化为:对于每个右端点i,寻找有多少个左端点j使得pre[i] - pre[j-1] <= k,也就是pre[j-1] >= pre[i] - k。
- 因为数组中没有负数,所以前缀和天然具有单调性。
- 既然单调,就可以用双指针维护一个滑动窗口,右指针每向右移动一位,左指针跟着移动直到窗口内的和不超过k,然后以右指针结尾的合法子数组数量就是窗口长度。
这个题其实还可以玩出另一个变体:如果数组里有负数,前缀和就不单调了,双指针就不成立,得用前缀和+离散化+树状数组的方式去求逆序对数量,复杂度会上升到O(n log n)。如果笔试里遇到“数组可能包含负数”的条件,一定要警惕,不要无脑滑动窗口。
回到这个题,双指针解法的代码非常简单:
def count_subarrays(nums, k): n = len(nums) left = 0 current_sum = 0 ans = 0 for right in range(n): current_sum += nums[right] while current_sum > k: current_sum -= nums[left] left += 1 ans += right - left + 1 return ans这里面有一个很关键的点,为什么右指针每次移动后,答案要加上right - left + 1?因为固定右端点,以left到right之间的任意位置作为左端点,形成的子数组都满足条件,数量正好等于当前窗口长度。这个思路一定要想明白,很多类似的滑动窗口计数题都是同一个套路。
3.2 第二题:带冷却时间的任务调度
第二题明显比第一题高一个档次,是一道经典的带冷却时间任务调度题。题目大致是这样的:给定一个字符数组tasks,每个字符代表一种任务类型,每个任务需要1个单位时间执行,两个相同任务之间必须间隔至少n个单位时间(冷却时间),求完成所有任务所需的最短时间。
这题在LeetCode上有个几乎一样的题叫“任务调度器”,经典解法是用贪心:统计每种任务的数量,找到出现次数最多的任务,把它作为骨架来排布。
举个例子,如果任务A出现5次,冷却时间n=2,那A的排布就是A _ _ A _ _ A _ _ A _ _ A,基本上可以想象成先放5个A,每个A后面跟两个空位,最后再补一个A。总时间骨架就是(max_count - 1) * (n + 1) + max_num,其中max_count是最大出现次数,max_num是达到最大次数的任务种类数。
但这个公式计算出来有时会小于tasks数组本身长度。比如任务的种类特别多,填充物足够填满所有空隙,那么实际最短时间就是tasks的总长度。所以最终答案是两者取较大值:
def least_interval(tasks, n): from collections import Counter counts = Counter(tasks) max_count = max(counts.values()) max_num = sum(1 for v in counts.values() if v == max_count) return max(len(tasks), (max_count - 1) * (n + 1) + max_num)这里要注意,max_num表示出现次数等于最大值的任务有几个。当有多个任务都达到最大次数时,收尾部分需要多留出对应数量的位置。这个细节很容易漏,漏掉的话算出来的结果会偏小。
这道题考察的核心其实是贪心思维和数学推导能力。很多人第一反应是模拟每个时间片安排什么任务,但模拟的复杂度比较高且容易出错。而上面的公式其实是把问题抽象成了“插空”模型:出现频率最高的任务是瓶颈,其它任务只要能填进空档就不会增加总时间;如果空档不够填,说明任务总时长本身就更大,那就直接返回总长度。
从这道题延伸开,笔试里很多调度类题目都可以用类似的“找瓶颈 + 插空验证”的思维来解决。拿到题目时先别急着动手编码,先把数学模型建立起来,往往能把一道看上去复杂的题瞬间简化。
3.3 两道编程题的对比与做题顺序建议
两道题放在一起对比,能明显看出出题人的意图:第一题考的是数据结构和基础算法(前缀和/滑动窗口),第二题考的是贪心思维和数学建模能力。第一题更偏向“基本功”,第二题更偏向“思维深度”。
我个人的做题策略是:先把两道题都快速扫一遍,然后从第一题开始做。因为第一题通常更容易拿分,做完后的心态会稳很多,再去啃第二题就不会慌。第二题如果实在没思路,也不要完全不写,把统计频次的代码写出来,至少能过一部分测试用例,拿一部分分数。
还有一个很多人会忽略的点:如果你的思路是“模拟时间片一个个推进”,但实现到一半发现逻辑越来越复杂、边界情况越来越多,大概率说明这条路走不通。这时候别死磕,果断退出来重新想贪心或者数学解法。在线笔试的时间是很宝贵的,死磕一道题导致第二道题没时间做,是最亏的结果。
4. 时间分配与做题策略
4.1 时间分配的整体思路
小红书这批笔试的总时长大概在90分钟到120分钟之间,选择题数量加上两道编程题,时间不算充裕。我的建议是把时间大体切成三块:选择题控制在40分钟以内,编程题第一题控制在20-25分钟,第二题控制在30-35分钟,剩下10分钟左右用来检查。
选择题一定要控制住时间。有些选择题会故意设计得很有迷惑性,如果你在某一题上卡了5分钟以上,大概率是做不出来的,先标记一下跳过去,把后面的题先做完再回来看。笔试系统一般允许跨题跳转,所以要充分利用这个功能,不要在一道题上恋战。
编程题的读题也非常关键。我遇到过不少同学,题目没读完就急着写代码,结果写到一半发现理解错了,又推倒重来。两道编程题读题至少花3-5分钟,把输入输出的格式、边界条件、时间空间限制全部看清楚,再开始动手。尤其是数据范围,这直接决定了你的算法必须达到什么复杂度级别。
4.2 做题顺序的战术选择
做题顺序其实是个很讲究的事情。一般来说,先做编程题、后做选择题,还是先做选择题、后做编程题,要因人而异。我的习惯是先快速浏览所有题目,然后先做编程题里自己有把握的那道,再做选择题,最后回来死磕剩下的编程题。
为什么这么排?因为编程题耗费的精力和时间比较多,放在前面做,大脑状态好,思路清晰,更容易AC。选择题毕竟有选项,有时候即使知识点不太熟也能靠推理和排除法蒙个大概。如果把编程题放在最后,经过大量选择题轰炸之后,大脑已经很疲劳了,再做编程题容易反应变慢,思路打不开。
当然这个策略不是绝对的。如果你明显感觉选择题里有很多送分题,先迅速拿掉这些分也很重要。关键是心里要有一杆秤:选择题的分是“易得分”,编程题的分是“高分值”,两边都要稳住,不能顾此失彼。
4.3 遇到不会的题怎么处理
笔试中遇到不会的题太正常了。编程题卡死的时候,我常用的办法是先跳出来,在草稿纸上画例子。很多时候,思路是在拿小样例一步一步手推的过程中找到的。比如那个任务调度题,如果一时想不起来公式,拿一组tasks和n,手动排出最优解,排着排着就能总结出规律来。
选择题遇到完全没头绪的,先排除明显错误的选项,然后凭第一感觉选一个,做个标记。有时间再回来看,没时间就保持原样。千万不要空着不选,很多笔试系统空着是0分,选错也不倒扣分,所以蒙一个总比空着强。
还有一点要提醒:有些编程题是支持部分通过的,也就是说你的代码只要能在部分测试用例上跑通,就能拿到一部分分数。所以即使你的答案不是最优解,只要符合基本逻辑,就把代码写上去。完全空着或者只输出一个固定值,是最不明智的。
5. 常见问题与避坑清单
这部分我把自己见过的、身边同学踩过的坑集中整理一下,后续参加笔试的同学可以对照着自查。
5.1 在线IDE与本地环境的差异
笔试平台的在线IDE通常没有本地IDE那么完整的调试功能,不能打断点、不能逐步调试。很多人写着写着变量值不对,只能靠print大法去排查,非常痛苦。所以平时练习的时候,就要适应在“没有debugger”的情况下写代码。我自己的做法是:把关键变量的变化过程用注释标注在旁边,逻辑推演清晰后再落笔,减少试错成本。
另外,在线IDE的代码补全功能通常比较弱,语法高亮可能也不太稳定。如果你平时重度依赖IDE的自动补全和错误提示,在笔试环境下会非常难受。提前在牛客或者力扣上用ACM模式做几套题,把输入输出处理的肌肉记忆练出来,是个很好的预演。
5.2 输入输出处理的坑
这是ACM模式最容易翻车的地方,没有之一。比如题目给了一行整数,用空格分隔,你需要读进来存成数组。有些人用input().split()之后忘记转int,直接拿字符串去运算,结果全错。再比如多行输入,有些人写了一个循环去读,结果读多了或者读少了,导致数组越界。
提供几个我常用的输入输出模板:
import sys # 读取一行整数 data = list(map(int, sys.stdin.readline().strip().split())) # 读取n行,每行多个整数 n = int(sys.stdin.readline().strip()) arr = [] for _ in range(n): arr.append(list(map(int, sys.stdin.readline().strip().split())))这种模板本质上就是固定写法,多练几遍就能形成肌肉记忆。考试的时候把这部分代码默写出来,不会占用太多时间,但能避免大量低级错误。输出方面,注意题目要求的是空格分隔还是换行分隔,很多人在这个细节上失分,非常可惜。
5.3 边界条件与极端用例
边界条件是编程题最常见的失分点。数组为空、只有一个元素、所有元素相等、数值达到最大值,这些情况你是否都考虑到了?写代码的时候,先花30秒想清楚边界条件,比测试的时候到处找bug要高效得多。
还有个常见的坑是整数溢出。如果题目给的n是10^5级别,子数组的和可能超过int的范围,这时候就要用long long(C++)或者Python天然支持大整数所以无所谓,但Java、Go之类的语言就要小心类型范围。类似的,如果题目要求结果对某个数取模,记得每一步都取模,不要等到最后再取,防止中间结果溢出。
5.4 心态管理与时间预警
笔试的心态太重要了。我见过一个同学,第一道编程题卡了很久没写出来,然后整个人就慌了,后面的选择题也做得很差,最后成绩惨不忍睹。这种连锁反应其实是可以避免的——做题前就给自己定好规矩:一道题如果15分钟没有任何进展,立刻放弃做下一道,全部做完以后有时间再回来啃。
在线笔试系统一般会有倒计时提醒,但我建议你不要依赖它。每做完一道题,扫一眼时间,心里大致有个数。做题过程中不要频繁看倒计时,那样只会增加焦虑感。
最后再分享一个小技巧:笔试前把环境准备好,包括稳定的网络、安静的场地、充满电的设备、水放在手边。听起来都是小事,但任何一项出问题都会打断思路。准备工作做得越充分,笔试的时候就越能聚焦在题目本身。考完以后,无论感觉是好是坏,迅速把题目和思路记录下来,这不是为了对答案,而是为下一批笔试积累素材。每一场笔试都是下一场的演练,我就是靠着这么一轮一轮攒下来的经验,才在后来的面试和笔试里越来越从容的。