這次 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