← 返回博客列表 Pinterest VO 四轮复盘:BQ + Coding + 系统设计
Pinterest

Pinterest VO 四轮复盘:BQ + Coding + 系统设计

2026-08-10

这次 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 行为面

题目

这一轮问了两个问题:

  1. 讲一次你把一个复杂系统变简单的经历。
  2. 当用户体验和业务指标发生冲突时,你怎么做取舍?

我的回答(STAR)

第一题我用了优化推荐 feed 去重服务的经历。

第二题我强调的是: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) 聚合。

架构选型:

要主动讲的点:servingId 映射的 TTL 怎么定、事件乱序 / 迟到怎么用 watermark 处理、聚合是滚动窗口还是滑动窗口。

设计题 2:商品目录批量上传

商家批量上传目录(价格、库存等),单个任务最多 50 万条商品。

我一开始的想法是让客户端 / SDK 把数据切成每 100 行一批推上来,但很快意识到:这把可靠性和任务管理的责任都压到了客户端,不理想。

更好的方案:

  1. 商家把大文件直接上传到对象存储,服务端返回一个 job ID
  2. 消息队列异步驱动解析、校验、分片、批量写入。
  3. 客户端凭 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:边界情况

面试官追问了几类异常输入:

我的处理是先和面试官澄清:输入是否保证是一条唯一合法的链? 确认口径后再决定——如果不保证,就要检测异常(比如起点数 != 1、接出的站数 != 票数 + 1),并按约定返回错误、返回空,或报告具体异常,而不是默默给出一条错的路线。


备考建议


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获取一对一定制备考方案

联系方式