← 返回博客列表 Pinterest SDE VO 复盘:三轮 Coding + HM 沟通,开关翻转 BFS / LRU 驱逐回调 / 日志查询设计
Pinterest

Pinterest SDE VO 复盘:三轮 Coding + HM 沟通,开关翻转 BFS / LRU 驱逐回调 / 日志查询设计

2026-06-06

网传 Pinterest 的面试题很难,真做下来其实还好——题目不刁钻,但非常注重代码质量、沟通清晰度和 ownership。整个 Virtual Onsite 由 三轮 coding + 一轮 Hiring Manager 沟通 组成,coding 不追求 trick,而是看你能否写出健壮、可扩展、贴近真实业务的代码。下面把三道真题完整复盘一遍,思路都很有代表性。

一、Pinterest SDE VO 概览

维度 详情
轮次 3 轮 coding + 1 轮 HM competency
形式 远程视频,注重 verbal reasoning
难度 中等,题不偏,但 follow-up 深
考点 图 / 状态管理 / 数据访问接口设计
风格 工程化,coding 常夹带 design 元素

核心提醒:Pinterest 的题目大多和实际系统设计挂钩,变量命名、边界条件、扩展性 是评分重点。系统设计虽然没有单独一轮,但 coding 里经常埋 design 元素,trade-off 要准备好深入讲。

二、题 1:最多翻转 K 个开关的可达性(变种 BFS)

给定一张城市地图,每条边代表一条单向道路,每条道路有一个开关(on/off)。问从城市 A 到城市 B 是否存在一条路径,最多允许翻转 K 个开关使道路变为可通行。

思路转化:经典最短路的状态是「当前节点」,这题把状态扩展为 (当前节点, 已翻转次数)。每遇到一条关闭的道路,如果翻转次数还没超过 K,就 +1 加入队列;遇到开着的道路则不消耗预算。用 visited[node][flips] 去重,避免重复访问同一状态。

from collections import deque

def can_reach(n, edges, A, B, K):
    # edges[u] = list of (v, is_open);is_open=True 表示道路本来就通
    graph = [[] for _ in range(n)]
    for u, v, is_open in edges:
        graph[u].append((v, is_open))

    # 状态 = (节点, 已翻转开关数)
    start = (A, 0)
    visited = {start}
    q = deque([start])
    while q:
        node, flips = q.popleft()
        if node == B:
            return True
        for nxt, is_open in graph[node]:
            cost = 0 if is_open else 1
            nf = flips + cost
            if nf <= K and (nxt, nf) not in visited:
                visited.add((nxt, nf))
                q.append((nxt, nf))
    return False

关键点:面试官特别在意 flip 计数逻辑能否避免重复访问——同一个节点用不同的翻转次数到达是不同状态,不能只按节点去重,否则会漏解或死循环。时间复杂度:O((V+E)·K),空间复杂度:O(V·K)。

三、题 2:带驱逐回调的 LRU Cache

在标准 LRU Cache 的 get / put 之外,要求支持:当某个元素因为容量上限被驱逐时,触发一个 callback 函数。

思路转化:底层结构还是 双向链表 + 哈希表,难点在于「驱逐时机」——只有在 put 导致超容、需要淘汰尾部节点时,才调用 callback,并且要在删除完成后调用,保证回调里读到的状态是一致的。

class Node:
    __slots__ = ("key", "val", "prev", "next")
    def __init__(self, key=0, val=0):
        self.key, self.val = key, val
        self.prev = self.next = None

class LRUCacheWithEviction:
    def __init__(self, capacity, on_evict=None):
        self.cap = capacity
        self.on_evict = on_evict          # 驱逐回调
        self.cache = {}
        self.head, self.tail = Node(), Node()
        self.head.next, self.tail.prev = self.tail, self.head

    def _remove(self, node):
        node.prev.next, node.next.prev = node.next, node.prev

    def _add_front(self, node):
        node.prev, node.next = self.head, self.head.next
        self.head.next.prev = node
        self.head.next = node

    def get(self, key):
        if key not in self.cache:
            return -1
        node = self.cache[key]
        self._remove(node)
        self._add_front(node)
        return node.val

    def put(self, key, val):
        if self.cap <= 0:
            return
        if key in self.cache:
            self._remove(self.cache[key])
        node = Node(key, val)
        self.cache[key] = node
        self._add_front(node)
        if len(self.cache) > self.cap:
            lru = self.tail.prev
            self._remove(lru)
            del self.cache[lru.key]
            if self.on_evict:            # 驱逐后再回调
                self.on_evict(lru.key, lru.val)

写完后面试官追问了 线程安全(算法面里问这个属于意外):是否需要加锁、是否支持并发访问,以及「如果 callback 执行时间太长会不会卡主流程」。我提到可以把回调丢到异步队列里执行,避免阻塞 put时间复杂度get / put 均摊 O(1)。

四、题 3:日志访问记录查询类设计

输入是日志访问记录 (user_id, action_type, timestamp),实现一个类支持两个方法:countUniqueUsers(start, end) 统计时间区间内的去重用户数;getUserActions(user_id, start, end) 返回某用户在区间内的所有行为。

思路转化:典型的「索引结构 + 区间查询」设计题。按 user_id 建一个索引方便取单用户行为,按 timestamp 排序便于区间二分。getUserActions 直接在该用户的有序列表上二分定位区间;countUniqueUsers 在全局有序事件上扫窗口并用集合去重。

import bisect
from collections import defaultdict

class LogStore:
    def __init__(self):
        self.by_user = defaultdict(list)   # user_id -> [(ts, action)] 有序
        self.events = []                   # [(ts, user_id)] 全局有序

    def record(self, user_id, action_type, ts):
        # 假设按时间近似有序到达,必要时插入保持有序
        self.by_user[user_id].append((ts, action_type))
        bisect.insort(self.events, (ts, user_id))

    def getUserActions(self, user_id, start, end):
        arr = self.by_user.get(user_id, [])
        lo = bisect.bisect_left(arr, (start,))
        hi = bisect.bisect_right(arr, (end, chr(0x10FFFF)))
        return [a for _, a in arr[lo:hi]]

    def countUniqueUsers(self, start, end):
        lo = bisect.bisect_left(self.events, (start, 0))
        hi = bisect.bisect_right(self.events, (end, float("inf")))
        return len({uid for _, uid in self.events[lo:hi]})

面试官非常关心 数据量大时的性能,还追问「能不能只用一个索引结构同时支撑两个方法」——这部分讨论挺深入,本质是在权衡空间换时间。复杂度getUserActions O(log n + k),countUniqueUsers O(log n + m)。

五、第四轮:Hiring Manager Competency

最后一轮不考算法,重点聊 experience 和合作经历。HM 先介绍团队在做的 infra 工作,然后问:

我重点讲了一个异步消息系统从 design 到 rollout 的完整过程,以及如何推动整个团队迁移。整个对话很轻松,但 HM 在评估你的 沟通方式和影响力

六、备考重点

能力 怎么练
状态建模 把「节点」扩展成「节点 + 附加状态」,练 BFS/Dijkstra 变种
接口设计 练带状态管理、数据访问 API 的设计题,想清楚索引结构
Trade-off 表达 每个方案主动说出 2 个备选 + 取舍理由
HM 故事 准备 system 从设计到落地、mentoring、推动变革三类故事

FAQ

Q1:Pinterest SDE VO 一共几轮?

三轮 coding + 一轮 Hiring Manager 沟通。三轮 coding 都注重代码质量和边界处理,HM 轮聊合作经历与影响力,重点评估 culture fit 和 ownership,不考算法。

Q2:Pinterest 的题真的很难吗?

网传很难,实际中等。题目不刁钻、不追求 trick,但 follow-up 很深,而且经常在 coding 里夹带系统设计元素。比起 trick,面试官更看重变量命名、边界条件和扩展性。

Q3:算法面会问线程安全吗?

会,而且常出乎意料。比如做 LRU 时被追问是否需要加锁、callback 太慢会不会阻塞主流程。提前准备「异步处理 / 加锁粒度 / 并发访问」的回答能加分。

Q4:Pinterest VO 怎么准备性价比最高?

多练贴近实际业务的题型,尤其是带状态管理或数据访问接口的设计题。如果想要这三道真题的限时陪练、变种拓展,或需要 VO 辅助 / VO 代面 的实时对接,可以发岗位 JD 先做题型预测再排练习计划。


正在准备 Pinterest SDE VO?

Pinterest 考的是工程化的代码质量和系统思维,不是死刷题。oavoservice 提供 Pinterest / Meta / 头部互联网公司的 VO 全流程陪练:三轮 coding 限时模拟、HM competency 故事打磨、设计题 trade-off 拆解,也支持 VO 辅助 / VO 代面 的实时对接。教练含前大厂资深工程师,熟悉 Pinterest「coding 夹带 design」的评分风格。

立即添加微信 Coding0201获取 Pinterest VO 真题与陪练

联系方式