投的是 Netflix Member, Commerce & Games Engineering,这周刚面完。整个过程最大的感受就是:奈飞的面试形式和大多数科技公司完全不一样。题目本身不算难,甚至大都是 LeetCode Easy/Medium,但对沟通、思考过程和工程意识的要求异常严格。把 OA 和两轮店面的细节都记下来,希望对准备 Netflix 面试的人有帮助。
Netflix OA:CodeSignal 平台,三道题
奈飞走的是 CodeSignal,题目都是近期的新题,总共三道:一道概率 quiz,两道 coding。
题目 1:条件概率
场景:万圣节,三个朋友各拿到了不同数量的红、蓝、绿三种颜色糖果。给定表格:
- Jack Silver:红 5、蓝 10、绿 2
- Blake Smith:红 20、蓝 4、绿 4
- Carrie Silver:红 12、蓝 6、绿 2
问题:如果你的姓是 Silver,你拿到蓝色糖果的概率是多少?
这题本质上就是考你会不会仔细读题。关键点是"姓 Silver 的人"------题目里有两个:Jack Silver 和 Carrie Silver,Blake Smith 不算。所以计算时只考虑这两个人的糖果:总数是 5+10+2+12+6+2 = 37,其中蓝色是 10+6 = 16,答案 16/37。
很容易出现的错误是:把 Blake 的数据也算进去,导致分母变成 51,答案错误。这题没什么数学难度,就是读题的细心程度。
题目 2:Bucket 文件系统模拟
题意:模拟一个虚拟环境里的简单文件系统,只有两个命令:
- goto <bucket_name>:切换到指定 bucket(题目保证 bucket 存在)
- create <filename>:在当前 bucket 创建一个文件(如果同名文件已存在,什么都不发生)
处理完所有命令后,返回文件数最多的 bucket 名字(题目保证无并列)。
一个例子的执行序列:
goto bucketA
create fileA
create fileB
create fileA # 同名,无效
goto bucketB
goto bucketC
create fileA
create fileB
create fileC
最终结果:bucketA 有 2 个文件,bucketB 空的,bucketC 有 3 个,所以返回 "bucketC"。
解法就是纯粹的数据结构模拟:用嵌套字典 + 集合来追踪每个 bucket 和其包含的文件。集合自动过滤重复,所以即使 create fileA 两次也只会存一份。
def solution(commands):
buckets = {}
current = None
for cmd in commands:
action, name = cmd.split()
if action == "goto":
current = name
buckets.setdefault(current, set())
else: # create
buckets[current].add(name)
return max(buckets, key=lambda b: len(buckets[b]))
这题有个隐藏坑:bucket 名和文件名可能重名(比如 bucket 叫 "bar",文件也叫 "bar"),但因为我们用集合按 bucket 隔离存储,天然没有问题。
数据规模最多 1000 个命令,时间复杂度 O(n),轻松通过。题目考的是你能不能清晰地用数据结构表达业务逻辑,难度不高但要实现清晰。
题目 3:Cyclic Pairs
这题比前两道有意思一点。
题意:cyclic shift 是把一个数末尾的若干位挪到开头。例如 546 的所有 cyclic shift 是:546、654、465。
给一个正整数数组,统计有多少对 (i, j)(其中 i<j),满足两个条件:
- 两数的位数相同
- ai 是 aj 的某个 cyclic shift(或反过来说,它们互为 cyclic shift)
例子:a = 13, 5604, 31, 2, 13, 4560, 546, 654, 456
答案是 5 对:
- 13 和 31(互为 shift)
- 13 和 13(位置 0 和 4)
- 31 和 13(位置 2 和 0)
- 5604 和 4560(互为 shift)
- 546 和 654(互为 shift)
注意 546 和 456 不构成 pair,因为 456 不在 546 的 shift 集合里(546 的 shift 只有 546、654、465)。另外 4560 和 456 也不算,因为位数不同(一个 4 位,一个 3 位)。
朴素的解法是两两比较:对于每一对 (i, j),计算 aj 的所有 cyclic shift,看 ai 是否在其中。但数据规模是 10⁵,这样做是 O(n²) × O(d)(d 是数字位数),会超时。
正确思路是规范化 + 分组计数。核心想法是:一个数和它的所有 cyclic shift 本质上是"等价"的,我们可以给每个等价类选出一个"标准代表"。
最简单的选法是:对一个数 x,计算它的所有 cyclic shift,取其中字典序最小的那个作为 x 的"规范形"(canonical form)。然后统计数组里每个规范形出现了多少次,同一规范形的数两两构成一对。
from collections import Counter
def solution(a):
def canonical(x):
s = str(x)
# 生成所有 cyclic shift,取字典序最小
return min(s[i:] + s[:i] for i in range(len(s)))
# 计算每个数的规范形
cnt = Counter(canonical(x) for x in a)
# 同规范形的数两两配对,数量为 c*(c-1)/2
return sum(c * (c - 1) // 2 for c in cnt.values())
时间复杂度分析:
- 对每个数计算规范形:O(n × d²),其中 d ≤ 10(十进制数最多 10 位)
- 计数和求和:O(n)
- 总复杂度:O(n × 100),10⁵ 的数据完全可以接受
这题的关键洞察是意识到"cyclic shift 的等价性",进而转化成"规范形计数问题"。一旦想到这层,代码就很短了------这是算法题的常见套路。
电面第一轮:系统设计 + Clarification
题目名字非常简洁:"设计 Netflix 首页的去重逻辑"。但这里的"简洁"其实是陷阱------面试官给的需求极其 high level,几乎没有任何具体细节。
面试的对话过程
面试官:"Netflix 首页有很多内容。有时候同一部剧可能会出现在不同的推荐行里。我们希望确保用户看到的首页中,同一部剧不会出现两次。你怎么设计这个系统?"
乍一听是很直白的需求,但其实什么都没说清楚:
- Input 是什么格式?是列表还是矩阵还是什么其他结构?
- Output 要返回什么?是清理后的首页结构,还是去重后的剧名列表,还是别的?
- "去重"的范围是什么?全页去重还是按行去重,还是有其他限制?
- 是否需要保持原有的结构(行列关系),还是可以打乱顺序?
我用了大约 15 分钟和面试官反复确认:
我的理解是:首页是一个列表的列表(list of list),每个子列表代表一行可以横向滑动的推荐栏。每行里有多个剧名。整个首页需要去重,使得任何剧名在整个首页中只出现一次。Output 是清理后的相同结构(list of list)。
面试官确认了这个理解,但补充说:首页实际上可以竖向滑动也可以横向滑动,所以去重的方式要确保无论怎么滑都不会看到重复内容。
解法
思路其实很直接:维护一个 global visited set,记录整个首页里已经看到过的剧名。然后逐行遍历,对每一行里的剧名,只保留那些还没在 visited set 里的,同时把它们加入 visited set。
def solution(homepage):
global_visited = set()
result = []
for row in homepage:
new_row = []
for show in row:
if show not in global_visited:
new_row.append(show)
global_visited.add(show)
result.append(new_row)
return result
代码写完后,面试官做的不是让我优化复杂度(这个已经是 O(n),无法优化),而是把话题转向现实场景。
Production 讨论:系统设计思考
面试官问:"这个设计在 Netflix 真实生产环境里能用吗?如果首页有 10 万部剧呢?或者 100 万?"
这时候本地缓存的 visited set 就成了瓶颈:
- 如果剧库有百万级别,光存储已看过的剧 ID 就得占用大量内存
- 分布式环境下,多个后端服务无法共享这个 set
- 首页去重的逻辑可能在推荐系统、后端 API、前端等多个层次同时发生,需要一个统一的去重机制
于是我们讨论到了分布式解决方案:用 Redis bloom filter。Bloom filter 是一种概率数据结构,特别适合"检查元素是否出现过"这类场景:
- 空间效率极高:百万级数据只需要几 KB 内存
- 查询快速:O(1) 时间复杂度
- 分布式友好:可以放在 Redis 等中央缓存里,多个服务共享
当然 bloom filter 有 false positive 风险(极小概率会说"没看过"的内容其实看过了),但对首页去重这个场景来说,偶尔重复一两个剧比多次询问数据库的成本低得多,这是一个可以接受的权衡。
这段对话让我意识到:Netflix 的"系统设计轮"不是传统的"设计一个分布式系统架构"类面试,而是直接让你从小问题延伸到真实业务问题。面试官不是在测你知不知道什么高深的理论,而是看你能不能:
- 从模糊的需求里问出关键细节
- 写出清晰、可运行的代码
- 自发地思考 scalability 和 production readiness
- 和面试官讨论权衡和优化策略
电面第二轮:Coding,围着去重继续
第二轮的三个 coding 题都围绕去重这个主题,难度从易到难。
题目 1:Contains Duplicate
最直接的版本。给一个列表(这轮的 input 是 show names),判断是否有重复。
def hasDuplicate(shows):
return len(shows) != len(set(shows))
这题本身没什么内容,就是考你能不能快速识别问题的本质并用最简洁的方式解决。面试官这里就是想让你热热手。
题目 2:最长无重复子列表
给一个 show name 列表,返回最长的连续子列表,其中没有重复的元素。这是 LeetCode 第 3 题(最长无重复子串)的列表版本。
例子:"A", "B", "C", "A", "D", "E" → 答案是 4,对应子列表 "B", "C", "A", "D" 或 "C", "A", "D", "E"。
标准的 sliding window 做法:用两个指针 left 和 right,维护一个 window 内的集合,right 指针不断向右扩展,如果遇到重复元素就收缩 left 指针直到没有重复,同时记录最大的 window 宽度。
def lengthOfLongestSubstring(shows):
char_index = {}
max_len = 0
left = 0
for right in range(len(shows)):
if shows[right] in char_index and char_index[shows[right]] >= left:
left = char_index[shows[right]] + 1
char_index[shows[right]] = right
max_len = max(max_len, right - left + 1)
return max_len
时间复杂度 O(n),空间 O(min(n, 26))(对于字符串,字符集最多 26)。这题考的是你对 sliding window 这个经典套路的掌握程度。
题目 3:没有公共字符的 Unique Pair
给一个 show name 列表,找有多少对 shows,它们之间没有公共的字母(character)。返回 pair 的数量。
例子:"ab", "cd", "ac" → 答案是 1,只有 ("ab", "cd") 这一对没有公共字母。
朴素的做法是两两比较:对每一对,检查它们的字符集是否有交集。
def countPairsWithoutCommonChar(shows):
count = 0
n = len(shows)
for i in range(n):
for j in range(i + 1, n):
if not (set(shows[i]) & set(shows[j])):
count += 1
return count
时间复杂度 O(n² × k),k 是平均字符串长度。如果 n 和 k 都不太大,这个做法就足够了。当我现场写完这个解法后,面试官说"可以",没有要求优化。
但在讨论阶段,他提了一句可以用 bitmask 优化。思想是:把每个 show name 的字符信息压缩成一个 26 位的整数(用位掩码表示包含哪些字母),然后两个 mask 按位与(&)为 0 就表示没有公共字母。这样就不需要每次都做集合交集操作了。
def countPairsWithoutCommonChar_optimized(shows):
def char_mask(s):
mask = 0
for c in s:
mask |= (1 << (ord(c) - ord('a')))
return mask
masks = [char_mask(show) for show in shows]
count = 0
n = len(shows)
for i in range(n):
for j in range(i + 1, n):
if (masks[i] & masks[j]) == 0:
count += 1
return count
这个优化的好处是:每次 pair 检查只需要一个 bitwise AND 操作,比集合操作快。这是 LeetCode 318(单词破坏)问题的原型思想。
面试官这里其实就是在观察:你能不能识别出 optimization 的机会,以及知不知道用 bitmask 这样的技巧。但他不强求,因为题目规模不大的话,朴素解法也完全可以。
面试的整体感悟
经过这次面试,我对 Netflix 的筛选标准有了更清晰的理解。看似 easy/medium 的题目,其实考的是一整套工程思维:
-
沟通优先于优化 --- Netflix 更看重你能不能理解真实需求,而不是一上来就写 O(n log n) 的解法。题目模糊是有意的,目的就是看你会不会 clarify。
-
过程胜过结果 --- 即使你的代码有 bug,只要你的思考过程清晰、讨论态度认真,面试官通常会给高分。反过来,代码完美但闷头闷脑地写,分数会很低。
-
边界条件要主动覆盖 --- 写完代码后,不要等面试官问,主动提出空列表、全重复、单元素这些 edge case,并说明你会怎么处理。这样能展现出细致的工程意识。
-
Production sense 很重要 --- 超过一定规模后,问题会升级。本地缓存不行了、得用分布式、得考虑成本和权衡。能自发地想到这些,或者至少在面试官引导下反应快,会显著提升面试分数。
-
不要过度优化 --- 如果题目规模不大,就不必非得用 bitmask 或其他 fancy 的技巧。清晰 + 正确 > 复杂 + 闪闪发光。面试官反而更欣赏"这个规模用简单方案,但我知道如果规模更大可以用 X 优化"这样的回答。
结语
Netflix 的面试让我重新思考了什么叫"好的工程师"。不是能解最难的 LeetCode,而是:
- 敢于问问题
- 能清晰地思考和表达
- 主动考虑现实约束和边界情况
- 知道什么时候简洁胜过聪慧
如果你正在准备 Netflix,与其狂刷 hard 题,不如找个 mock interview 的机会,和面试官一起讨论模糊需求的 clarification、production case 的权衡。这样才能真正理解 Netflix 的筛选标准。
