这次 Pinterest 的 VO 一共四轮,从 BQ 行为面到两道 Coding、再到两道系统设计,节奏排得很满,最后顺利拿到了 offer。这篇复盘把每一轮的真实题目、我的思路和代码都整理出来,如果你也在准备 Pinterest 的 onsite,希望这份笔记能帮你把每一轮的重点抓准。需要 VO 辅助或 VO 代面的同学,也可以直接参考文末的联系方式。
面试环境与形式概览
四轮都通过视频会议进行,Coding 用共享编辑器实时手写,系统设计在白板工具上边画边讲。整体感受是:BQ 轮很看重你能不能用数据把 tradeoff 讲清楚,Coding 轮偏重「分步演进 + 边界澄清」,系统设计轮则更在意你有没有先确认需求再动手设计。
| 轮次 | 形式 | 考点 |
|---|---|---|
| Round 1 · BQ | 行为面 + 追问 | STAR 结构、复杂系统简化、UX 与业务指标的 tradeoff |
| Round 2 · Coding | 实时手写 + 讲解 | 棋子可达位置,三步演进、方向表 + 障碍物 HashSet |
| Round 3 · System Design | 白板 + 讲解 | 流式 join + 分组聚合看板、目录批量上传架构 |
| Round 4 · Coding | 实时手写 + 讲解 | 打乱车票的行程重建、Map + Set 找起点 |
节奏建议:Coding 轮先把 clarify 做足再写代码,系统设计轮切忌上来就选型,先确认读写模式再定方案。
Round 1:BQ 行为面
题目
这一轮问了两个问题:
- 讲一次你把一个复杂系统变简单的经历。
- 当用户体验和业务指标发生冲突时,你怎么做取舍?
我的回答(STAR)
第一题我用了优化推荐 feed 去重服务的经历。
- Situation:推荐 feed 的去重服务承担着过滤重复内容的职责,随着流量增长,去重接口的延迟和系统负载都在往上顶,成了链路里的瓶颈。
- Task:我需要在不牺牲去重准确率的前提下,把接口延迟和系统负载压下来。
- Action:我重新梳理了服务架构,把原来一次请求内串行的多次查询改为按维度分层,并引入了针对热点键的缓存策略,让高频重复内容在缓存层就被拦截,减少对底层存储的穿透。
- Result:改造后接口延迟和系统负载都有明显下降。我提前准备了改造前后的数据对比图,面试官很喜欢这种「用数据说话」的方式,还顺着图追问了几个 tradeoff 的细节。
第二题我强调的是:UX 和业务指标冲突时,先量化两边的影响,再看这个决策是短期还是长期。举例说,某个改动可能短期拉高点击但伤害长期留存,我会用 A/B 的分层指标把「短期收益」和「长期体验成本」放在一起比,再和产品一起决定权衡点,而不是只盯单一指标。
复盘要点
BQ 轮最加分的一点,是能把抽象的「我优化了系统」落到具体数字上。提前准备好改造前后的对比图,被追问 tradeoff 时才有据可依。
Round 2:Coding —— 棋子可达位置
面试官是印度裔,口音比较重,但沟通完全没问题。题目分三步演进。
Step 1:只考虑 Queen
给定起点和棋盘大小,Queen 可以沿上下、左右和四条对角线共 8 个方向移动,把所有落在棋盘内的位置都加进结果。
Step 2:加入障碍物
用一个 HashSet 存障碍物坐标。沿某个方向扫描时,一旦碰到障碍物立即停止:Queen 既不能落在障碍物上,也不能越过它。
Step 3:支持多种棋子
通过 piece_type 参数支持不同棋子:Queen 用 8 个方向,Bishop 只用 4 条对角线,Rook 只用上下左右。边界检查、障碍物检查、方向扫描共用同一套代码路径,只是方向表不同。
Python 解法
from typing import List, Set, Tuple
# 每种棋子对应的方向表
DIRECTIONS = {
"queen": [(-1, 0), (1, 0), (0, -1), (0, 1),
(-1, -1), (-1, 1), (1, -1), (1, 1)],
"bishop": [(-1, -1), (-1, 1), (1, -1), (1, 1)],
"rook": [(-1, 0), (1, 0), (0, -1), (0, 1)],
}
def reachable_positions(
start: Tuple[int, int],
n: int,
piece_type: str,
obstacles: Set[Tuple[int, int]],
) -> List[Tuple[int, int]]:
"""计算棋子从 start 出发在 n x n 棋盘上可达的所有位置。
沿棋子对应的每个方向逐格前进,越界或撞到障碍物就停下。
"""
directions = DIRECTIONS[piece_type]
result: List[Tuple[int, int]] = []
r0, c0 = start
for dr, dc in directions:
r, c = r0 + dr, c0 + dc
# 沿该方向一直走,直到越界或遇到障碍物
while 0 <= r < n and 0 <= c < n:
if (r, c) in obstacles:
break # 障碍物不能落也不能越过
result.append((r, c))
r += dr
c += dc
return result
时间复杂度:O(n + b),n 为棋盘边长(每个方向最多走 n 步,方向数为常数),b 为障碍物数量(构建 HashSet 的开销)。 空间复杂度:O(b),用于存放障碍物集合。
复盘要点
三步演进的关键,是让障碍物检查和方向扫描共用一条路径——加障碍物时只在 while 里补一个 break,加棋子类型时只换方向表,主体逻辑不动。面试官很看重这种「加需求不推翻结构」的写法。
Round 3:System Design —— 两道设计题
设计题 1:广告事件聚合看板
已有两个服务分别产出广告事件(adId, servingId)和用户事件(servingId, action 类型 click / impression, region)。要做一个按广告、action 类型和 region 聚合的看板。
本质是一次流式 join + 分组聚合:广告事件先建立 servingId -> adId 的映射,用户事件通过 servingId 补上 adId,再按 (adId, action, region) 聚合。
架构选型:
- Kafka 承接两路事件流。
- Flink 做流式 join 与窗口聚合:一路消费广告事件维护映射,一路消费用户事件做 enrich,再按维度做窗口聚合。
- Redis 存放生命周期较短的
servingId -> adId映射(广告事件通常先于用户事件到达,映射只需短期保留)。 - 聚合结果写入 OLAP 存储(如列式分析库),供看板做多维查询。
要主动讲的点:servingId 映射的 TTL 怎么定、事件乱序 / 迟到怎么用 watermark 处理、聚合是滚动窗口还是滑动窗口。
设计题 2:商品目录批量上传
商家批量上传目录(价格、库存等),单个任务最多 50 万条商品。
我一开始的想法是让客户端 / SDK 把数据切成每 100 行一批推上来,但很快意识到:这把可靠性和任务管理的责任都压到了客户端,不理想。
更好的方案:
- 商家把大文件直接上传到对象存储,服务端返回一个 job ID。
- 用消息队列异步驱动解析、校验、分片、批量写入。
- 客户端凭 job ID 轮询进度和失败详情,并且只需要对失败的部分做重试。
这样客户端只负责上传和轮询,重活都在服务端异步完成,可靠性和可观测性都更好。
踩坑教训:不要在确认读取模式之前就选一个写优化的数据库。应该先确认目录的读取频率、查询模式、更新占比、一致性要求,再决定数据库 / 索引 / 缓存怎么配。
Round 4:Coding —— 行程重建
给定一组打乱顺序的车票,每张票有起点和终点,所有票恰好串成一条完整路线,要求还原出正确的行程顺序。
思路
用一个 origin -> destination 的 Map,再用一个 Set 存所有终点。没有出现在终点集合里的起点就是整条路线的起点,然后顺着 Map 一站一站往下接即可。
Python 解法
from typing import Dict, List, Set, Tuple
def reconstruct_itinerary(
tickets: List[Tuple[str, str]]
) -> List[str]:
"""从打乱的车票中还原完整行程。
先建 origin -> destination 映射,再找唯一的起点,
然后顺着映射逐站接出整条路线。
"""
next_stop: Dict[str, str] = {}
destinations: Set[str] = set()
for origin, dest in tickets:
next_stop[origin] = dest
destinations.add(dest)
# 找起点:不作为任何票终点出现的那个起点
start = None
for origin, _ in tickets:
if origin not in destinations:
start = origin
break
# 顺着映射逐站接出行程
itinerary = [start]
current = start
while current in next_stop:
current = next_stop[current]
itinerary.append(current)
return itinerary
时间复杂度:O(n),n 为车票数量(建映射与遍历各一遍)。 空间复杂度:O(n),用于映射和终点集合。
Follow-up:边界情况
面试官追问了几类异常输入:
- 重复起点:同一个起点出现在多张票上,
origin -> destination会被覆盖,路线不再唯一。 - 环形路线:路线首尾相接,找不到「不在终点集合里的起点」,
while可能死循环。 - 多段互不相连的路线:会有多个起点,无法接成一条链。
- 有多余的车票:部分票用不上,接出的行程比预期短。
我的处理是先和面试官澄清:输入是否保证是一条唯一合法的链? 确认口径后再决定——如果不保证,就要检测异常(比如起点数 != 1、接出的站数 != 票数 + 1),并按约定返回错误、返回空,或报告具体异常,而不是默默给出一条错的路线。
备考建议
- BQ 用数据说话:把「我优化了系统」落到改造前后的对比数字上,并提前想好被追问 tradeoff 时怎么答。
- Coding 分步演进:Pinterest 的 Coding 题常常从简单版本一步步加需求,写代码时就要预留「加障碍物、加类型」的扩展点,避免推翻重写。
- 系统设计先澄清再选型:先确认读写模式、数据量、一致性要求,再谈 DB、缓存和索引,别一上来就选一个写优化的库。
- 主动澄清边界:行程重建这类题,输入是否唯一合法直接决定解法,务必先问清楚再动手。
FAQ
Q1:Pinterest VO 一共几轮,都考什么?
我这次是四轮:一轮 BQ 行为面、两轮 Coding、一轮系统设计(含两道设计题)。BQ 看 tradeoff 表达,Coding 偏分步演进,系统设计看你会不会先确认需求。
Q2:Coding 题为什么喜欢「分三步」这种形式?
它想看你能不能在需求逐步增加时保持结构不崩。棋子那题从 Queen 到加障碍物再到多种棋子,好的写法是共用一条方向扫描路径,加需求只改方向表和加一个 break。
Q3:广告事件聚合看板,为什么用 Flink 而不是批处理?
因为看板需要近实时的多维聚合,事件是持续流入的。Flink 能做流式 join 和窗口聚合,配合 Kafka 承接事件、Redis 存短期映射、OLAP 存聚合结果,整条链路是实时的。
Q4:目录批量上传为什么不让客户端切批上传?
客户端切批会把重试、进度管理、失败恢复都压到客户端,可靠性差。更好的做法是上传大文件到对象存储拿 job ID,服务端用消息队列异步解析、校验、分片、批量写,客户端只轮询进度和失败详情。
Q5:行程重建遇到重复起点或环怎么办?
先和面试官确认输入是否保证唯一合法。如果不保证,就要检测异常:起点数不为 1、接出的站数与票数对不上等,按约定返回错误、返回空或报告异常,不要默默输出错误路线。
正在准备 Pinterest 的 VO? 我们熟悉 Pinterest 四轮的节奏——BQ 的数据化 tradeoff、Coding 的分步演进、系统设计的需求澄清,能提供从模拟到实时的 VO 辅助与 VO 代面,陪你把每一轮打磨到位。
立即添加微信 Coding0201,获取一对一定制备考方案。
联系方式
- 微信:Coding0201
- Email:[email protected]
- Telegram:@OAVOProxy