作为全球知名的职业社交平台,LinkedIn (领英) 的工程师招聘流程也自有其特色。整个旅程通常从你的简历开始,LinkedIn 的筛选过程非常看重你的背景是否与职位要求完美契合。听说,简历初筛时就会用到算法来解析,寻找与岗位相关的关键词,而且特别强调技能认证和过往项目的实际影响力展示。
成功通过简历关的申请人,接下来就要面对在线测试 (OA) 了。这主要是考验你的算法功底和解决问题的能力,是进一步评估你的重要基础。过了 OA 这一关的佼佼者,通常会接到一个初步的电话沟通,主要是评估你的文化契合度和沟通能力。
在这之后,就是真正的技术电话面试环节了,通常会有一到两轮。这些面试主要考察你的技术技能和解决问题的能力,常常会涉及实时编程解题或算法问题讨论。别小看电话面试,它可是决定你能否进入 onsite(现场面试)的关键一步!这是你展示技术实力、沟通能力以及你是否适合融入 LinkedIn 文化环境的绝佳机会。
如果电话面试顺利,那么就恭喜你来到了挑战性十足的现场面试环节!LinkedIn 的现场面试强度很高,通常会有四到六轮,而且安排得非常紧凑,可能要持续一整天。面试内容非常有深度,会涵盖技术能力的方方面面:通常包括两轮硬核编程面试来测验你的算法和数据结构功底,一到两轮系统设计面试考察你构建可扩展系统的能力,以及至少一轮行为面试来评估你的文化契合度和软技能。
整个 onsite 过程以其难度和深度著称,目的是全方位地彻底考察候选人在软件工程各个维度的能力。准备好迎接一场硬仗吧!
技术岗类型
软件工程师
岗位职责:设计、开发、测试和维护平台的核心功能、服务和基础设施。
面试考察技术点:数据结构与算法(特别是图的遍历、二分查找、哈希表等)、系统设计(分布式系统、数据库设计、API设计)、编程语言基础和问题解决能力。
数据工程师
岗位职责:负责构建、优化和管理大规模数据处理管道、ETL流程和数据仓库系统,确保数据的高效流入、存储和可用性,为分析和机器学习提供基础。
面试考察技术点:SQL能力、大数据处理框架(如Spark, Hadoop)、ETL概念、数据建模、编程/脚本能力(Python/Scala等)和分布式系统基础。
机器学习工程师
岗位职责:开发、实现、部署和扩展机器学习模型和系统,将算法应用于产品功能,如推荐、搜索排名、内容Feed等。
面试考察技术点:机器学习理论与算法、数据科学基础(统计、特征工程)、编程实现能力(Python及相关库)、模型评估与调优、以及ML系统的设计与部署。
安全工程师
岗位职责:保护领英的用户、数据和系统免受网络威胁,涉及安全架构设计、代码安全评审、漏洞检测和应急响应等。
面试考察技术点:网络安全、应用安全、密码学基础、漏洞分析与防御、安全协议、认证授权、以及系统安全设计。
技术面核心关注领域
在准备领英(LinkedIn)面试时,优先掌握深度优先搜索(DFS)和广度优先搜索(BFS)这两种算法至关重要。一般来说,基本的数据结构和二分查找构成了编程题的基础,这类题目的难度通常适中。而领英面试的独特之处在于,相对更侧重于广度优先搜索(BFS),这可能体现了他们对算法效率和可扩展性在实际场景中的重视。
技术考察点 | 面试中的占比 |
---|---|
杂项(Misc) | 8.2% |
模拟(Simulation) | 1.4% |
双指针(Two Pointers) | 15.1% |
高级数据结构(Adv. Data Structure) | 5.5% |
回溯算法(Backtracking) | 2.7% |
基础数据机构与算法(Basic DSA) | 11% |
二分查找(Binary Search) | 9.6% |
堆(Heap) | 4.1% |
图(Graph) | 2.7% |
动态规划(DP) | 8.2% |
深度优先搜索(DFS) | 17.8% |
广度优先搜索(BFS) | 13.7% |
在领英(LinkedIn),编程面试题难度适中,具有一定的挑战性,但通常达不到像 Google 或 Apple 那样顶尖的难度水平。候选人常会遇到侧重于图的遍历和二分查找的题目。虽然这些题目有难度,但与顶尖 FAANG 公司的题目相比,它们通常更容易入手,在复杂性和可解性之间取得了很好的平衡。
若按题目的难度占比来分,仅有20%为简易题,中等难度题目占比高达65%,剩余的15%为高难度面试题。
面试题与技术考察难度
常见面试题 | 考察技术点 | 难易度 |
---|---|---|
最大堆叠 | 高级数据结构 | 高难度 |
设计认证经理 | 基础数据结构与算法 | 中等难度 |
所有 O`one 数据结构 | 基础数据结构与算法 | 高难度 |
嵌套列表权重和 II | 广度优先搜索、深度优先搜索 | 中等难度 |
最近二叉搜索树值 II | 二分查找、深度优先查找、堆、双指针 | 高难度 |
油漆房 | 动态规划 | 中等难度 |
构建 H2O | 基础数据结构与算法 | 中等难度 |
查找二叉树的叶子 | 深度优先搜索 | 中等难度 |
最短词距离 II | 双指针 | 中等难度 |
机器人在圆圈内 | 模拟 | 中等难度 |
Linkedin OA(在线评估)
这项评估的主要目的,就是想快速了解你的基础编程功底到底扎不扎实,看看你能不能在有时间限制的情况下,运用好核心数据结构和算法,比如他们特别看重的图的遍历和二分查找等等,筛选出具备基本技术能力进入下一阶段的候选人。所以,备考时一定要把这些基础概念和算法弄透彻,尤其要多练练相关的题目,确保思路清晰、代码能跑。考试的时候,务必仔细阅读题目,理解清楚所有要求和限制,同时要特别关注你代码的效率和正确性,别忘了自己简单测试一下边缘情况。
软件工程师
软件工程师面试的核心是考察编程基础、问题解决能力、算法与数据结构功底,以及系统设计能力(尤其在中高级岗位)。他们希望看到你能写出正确、高效且可读性好的代码,并能设计可扩展的系统。
- 考察技术点: 核心数据结构(数组、链表、树、图、哈希表等)、经典算法(排序、查找、递归、动态规划)、图遍历(DFS, BFS)、二分查找、编程语言特性、面向对象设计、系统设计原则(可扩展性、可靠性、性能)、数据库基础。
- 面试题:
- 给定一个图,找出所有的连通分量。(考察图遍历)
- 实现一个函数,接收一个整数数组,返回所有不重叠的合并区间。(考察排序、区间处理)
- 如何设计一个支持高并发读写、能够快速查找某个特定值的分布式缓存系统?(考察系统设计)
- 在一个已排序但被旋转过的数组中查找某个元素。(考察二分查找变种)
- 实现一个 LRU 缓存淘汰策略。(考察链表、哈希表结合应用)
数据工程师
数据工程师面试侧重于大规模数据处理、数据建模、ETL流程设计、数据库知识以及处理分布式数据的能力。他们想了解你如何高效、可靠地构建和管理数据管道。
- 考察技术点: SQL(高级查询、性能优化)、ETL/ELT 设计原则、数据仓库和数据湖概念、数据建模(星型、雪花型模式)、分布式数据处理框架(如 Apache Spark, Hadoop 生态)、编程/脚本能力(Python, Scala)、数据库理论、数据质量与监控。
- 面试题:
- 写一个 SQL 查询,找出过去三个月内购买金额排名前10%的客户,并计算他们的总购买金额。(考察高级 SQL)
- 请描述如何设计一个自动化的 ETL 管道,将用户行为日志从消息队列实时导入数据仓库。(考察 ETL 设计、实时处理概念)
- 在设计一个用于存储社交网络用户关系的数据库时,你会选择哪种模型?解释原因。(考察数据建模)
- 解释 Apache Spark 中的 Transformation 和 Action 的区别,以及 Lazy Evaluation 的概念。(考察分布式数据处理原理)
- 如何处理数据管道中可能出现的 schema drift(数据模式变化)问题?(考察数据工程实践中的挑战)
机器学习工程师
机器学习工程师面试深入考察机器学习的理论基础、算法理解、模型实现能力、特征工程、模型评估与调优,以及将模型投入生产环境的考虑。
- 考察技术点: 经典机器学习算法(线性模型、树模型、聚类、降维等)、深度学习基础(神经网络结构、框架应用)、特征工程、数据预处理、模型评估指标与方法、实验设计、模型部署与监控、编程能力(Python及ML库)、分布式系统基础。
- 面试题:
- 解释偏差(Bias)与方差(Variance)的权衡,以及如何判断模型是欠拟合还是过拟合?(考察 ML 基础概念)
- 在处理一个高度不平衡的分类数据集时,你會使用哪些评估指标?为什么?(考察模型评估)
- 如何为一个大型社交平台设计一个个性化的内容推荐系统?(考察 ML 系统设计、问题抽象)
- 解释梯度下降算法及其变种(如 Adam)的原理和应用场景。(考察优化算法)
- 在将一个机器学习模型部署到生产环境中时,你需要考虑哪些问题来保证其性能和稳定性?(考察模型生产化)
安全工程师
安全工程师面试重点评估候选人在信息安全领域的专业知识、对常见漏洞的理解、安全防御策略的设计能力以及安全事件响应能力。
- 考察技术点: Web 安全(OWASP Top 10)、网络安全(TCP/IP, TLS/SSL, 防火墙)、密码学基础(加密、哈希、数字签名)、认证与授权机制、安全漏洞分析与利用、安全编码实践、操作系统安全、云计算安全、安全架构设计、安全监控与报警。
- 面试题:
- 请解释跨站脚本攻击 (XSS) 和跨站请求伪造 (CSRF) 的原理及防御方法。(考察 Web 安全)
- 描述 TLS 握手过程,并解释其如何确保通信的机密性和完整性。(考察网络安全/密码学)
- 如果在代码评审中发现一个潜在的缓冲区溢出漏洞,你会如何判断其危害性并提出修复建议?(考察漏洞分析与安全编码)
- 在设计一个微服务架构时,如何确保服务间的通信安全?(考察安全架构设计)
- 如果一个用户账号被盗用,作为安全工程师,你的初步调查和响应步骤是什么?(考察安全事件响应)
行为面试 (BQ)
在准备面试时,行为问题是面试官了解你过往经验、评估未来表现的重要途径。这些问题通常可以分为几类:
经典行为问题
这类问题考察你应对基本工作场景的能力。例如:
- 当被问及需要学习新技术来完成项目的经历时,面试官想了解你的快速适应和学习能力。准备一个事例,着重描述你如何评估情况、学习新技术的流程,以及最终的成功结果。可以具体提及使用的工具或资源。
- 关于不同意工作中的某项决定,这类问题旨在考察你的冲突解决技巧。讲述一个尊重各方的故事,说明你如何提出观点,以及为达成解决方案所做的努力。强调你的沟通和在分歧中协作的能力。
- 当被问到如何管理多个截止日期时,要展现你的时间管理和优先级排序能力。解释你如何组织任务、应对潜在障碍,并强调成功按时完成项目。可以提及你使用的工具或方法来跟踪进度。
团队合作行为问题
这类问题侧重于评估你在团队环境中的表现和贡献。例如:
- 描述一次与团队合作完成有挑战性项目的经历,包括项目内容和你在团队中的角色。详细讲述具体事件、应对挑战的策略以及你对团队的具体贡献。突出沟通、解决冲突和角色适应能力。
- 讲述一次帮助解决团队成员之间纠纷的经历,以及你如何处理。着重体现你的调解能力,如何帮助不同观点达成一致。描述你理解各方并寻找互利解决方案的方法,展现你的人际交往和解决问题能力。
- 考虑到领英(LinkedIn)重视通过协作实现变革,可能会问你团队合作如何带来创新解决方案的例子。描述协作如何激发创造力,提及团队成员的多样化输入以及它们如何促成新颖方案。强调不同技能组合的协同作用如何带来突破性创新。
与岗位职责相关的行为问题
这类问题会结合具体岗位要求,考察你的专业技能应用和价值契合度。例如:
- 描述一次使用编程技能解决没有明确解决方案的问题的经历。准备一个需要分析和创造性思维的场景。概述挑战、你的具体行动(包括使用的技术和工具)以及结果。强调任何带来成功的创新方法或优化。
- 如何确保代码的质量和可靠性?讨论你如何使用测试框架、持续集成和代码评审等实践。强调可维护性和可扩展性的重要性,以及你遵循的任何具体方法论,如 TDD 或敏捷开发。
- 领英(LinkedIn)重视变革和创造经济机会。能否分享一个你参与的项目或倡议,与领英的愿景契合?选择一个你产生过显著影响的项目,最好是能以有意义的方式惠及用户的软件改进。解释你的角色、涉及的技术以及项目对用户或利益相关者的影响。这有助于展现你与领英使命和价值观的契合度。
准备这些行为问题时,关键是运用 STAR 原则(Situation, Task, Action, Result)清晰地讲述你的故事,并突出你的核心能力。
面试准备
准备领英的技术岗位面试,首先最核心、最硬的基础功千万要扎实。这意味着你需要对基本的数据结构和算法有深入的理解和熟练的应用能力,比如数组、字符串、链表、树、图、哈希表等经典数据结构一定要烂熟于心。算法方面,特别要花精力去练习图的遍历(DFS和BFS)、二分查找,这些在领英是常考的重点。不仅仅是记住模板,更要理解它们背后的原理和不同场景下的优劣,能分析时间和空间复杂度。多刷一些对应难度区间的题目,培养快速理清思路、将想法转化为可执行代码的能力,并且要注重代码的清晰度和健壮性,能够处理好各种边界条件。记住,扎实的技术功底是你自信应对一切挑战的基石!
除了代码能力,面试官也非常关注你作为一个工程师的综合素质和潜力。这意味着你需要认真准备行为问题——这些问题是为了了解你的协作能力、解决冲突的方式、面对挑战和不确定性时的态度,以及你的学习和成长经历。特别是要思考你过往的项目或经历如何体现出与领英“连接世界专业人士、创造经济机会”愿景的契合度,多准备一些能突出你影响力、团队合作和创新思维的例子。对于更高级别的岗位,系统设计能力也是考察重点,需要展示你设计可扩展、可靠系统的思路。别忘了,清晰、有逻辑的沟通能力贯穿整个面试过程,大胆说出你的想法,与面试官互动,这同样重要。面试不仅仅是技术的较量,更是全面展示你个人能力和职业潜力的过程,带着积极的心态去迎接它吧!
VO面试实录
面试开始,面试官面很直接抛出了一个领英核心业务的问题给候选人:“想象一下,我们拥有海量的领英用户数据,以及他们之间的连接关系。这些关系可以用一个图来表示,其中用户是节点,他们之间的连接是无向边。现在,给定两个用户A和用户B,你需要设计一个算法,找出从用户A到用户B的最短推荐路径。这里的“推荐路径”有一个特殊要求:路径中的每一步连接都必须是一度或二度连接。”
候选人听到这道题,首先想到的是图的最短路径算法,比如广度优先搜索(BFS)。然而,问题中“一度或二度连接”的限制让他意识到这并非简单的最短路径。他最初考虑为每个节点预计算其所有的一度或二度连接,然后再运行BFS。但这种预计算的开销可能很大,而且在图结构频繁更新的情况下效率不高。就在他思路有些凝滞时,辅助团队立即推送了“改良版BFS与动态二度查找”的思路——这道题的核心在于如何高效地处理“二度连接”的限制。我们可以在标准的BFS过程中,每当探索到一个新的节点时,不仅检查其直接邻居(一度连接),还需要检查这些直接邻居的邻居(二度连接)。副设备同步发送了 Python 语言代码框架,重点标注了 BFS 队列、visited 集合以及动态查找二度连接的逻辑:
from collections import deque
def find_shortest_linkedin_path(graph, user_a, user_b):
if user_a == user_b:
return [user_a]
queue = deque([(user_a, [user_a])]) # (当前用户, 当前路径)
visited = {user_a} # 记录已访问的用户,避免重复探索
while queue:
current_user, path = queue.popleft()
# 检查一度连接
for neighbor in graph.get(current_user, []):
if neighbor == user_b:
return path + [neighbor]
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
# 检查二度连接
for second_degree_neighbor in graph.get(neighbor, []):
if second_degree_neighbor == user_b:
return path + [neighbor, second_degree_neighbor]
# 注意:二度连接的路径长度为2,所以只在到达目标时返回
# 否则,如果将所有二度连接也加入队列,可能会导致非最短路径
# 这里的逻辑是确保二度连接只在直接连接到user_b时才被视为有效路径
# 否则,它会作为正常的一度连接在后续BFS迭代中被处理
return None # 没有找到路径
# 示例用法
linkedin_graph = {
"A": ["C", "D"],
"B": ["E", "F"],
"C": ["A", "G"],
"D": ["A", "H"],
"E": ["B", "G"],
"F": ["B"],
"G": ["C", "E"],
"H": ["D"]
}
# 查找从A到B的最短推荐路径
path = find_shortest_linkedin_path(linkedin_graph, "A", "B")
print(f"从 A 到 B 的最短推荐路径: {path}") # 预期输出: 从 A 到 B 的最短推荐路径: ['A', 'C', 'G', 'E', 'B'] 或 ['A', 'D', 'H', 'B'] (需要进一步检查二度连接的逻辑)
# 让我们修正一下二度连接的逻辑,确保它能正确地形成最短路径
# 上面的代码在处理二度连接时,如果直接连接到B,是OK的,但如果二度连接自身也作为新的起点,需要更谨慎
# BFS 的核心是层级遍历,所以二度连接的节点应该在下一层被访问到,而不是直接加入当前层
# 重新修正 find_shortest_linkedin_path 的逻辑,使其更符合 BFS 的层级探索
def find_shortest_linkedin_path_optimized(graph, user_a, user_b):
if user_a == user_b:
return [user_a]
# 队列存储 (当前用户, 当前路径)
queue = deque([(user_a, [user_a])])
# visited 集合存储已访问的用户,避免循环和重复计算
visited = {user_a}
while queue:
current_user, path = queue.popleft()
# 检查一步连接(一度连接)
for next_user_1 in graph.get(current_user, []):
if next_user_1 == user_b:
return path + [next_user_1]
if next_user_1 not in visited:
visited.add(next_user_1)
queue.append((next_user_1, path + [next_user_1]))
# 检查两步连接(二度连接)
# 注意:这里我们是在探索 next_user_1 的邻居,即从 current_user 到 next_user_1 再到 next_user_2
for next_user_2 in graph.get(next_user_1, []):
# 路径长度为 current_user -> next_user_1 -> next_user_2
if next_user_2 == user_b:
return path + [next_user_1, next_user_2]
# 注意:二度连接的节点只有在它们没有被访问过且可以作为下一层探索的起点时才加入队列
# 但根据题意,路径中的每一步都必须是一度或二度连接,这暗示我们只需要检查一次二度连接
# 并且如果二度连接指向B,那么这是一个有效的推荐路径
# 如果二度连接的节点本身需要继续探索,那么它会作为一度连接的“next_user_1”在后续迭代中被处理
return None
# 再次示例用法
path_optimized = find_shortest_linkedin_path_optimized(linkedin_graph, "A", "B")
print(f"从 A 到 B 的最短推荐路径 (优化版): {path_optimized}")
# 再次验证示例图中的路径
# A -> C (1度)
# C -> G (1度)
# G -> E (1度)
# E -> B (1度)
# 路径:A -> C -> G -> E -> B (4步,但每一步都是1度)
# A -> C (1度)
# C 的二度连接:
# C -> G (1度),G 的邻居有 C, E
# C -> G -> E (2度)
# 如果 B 是 E,则路径为 A -> C -> G -> E -> B
# 让我们重新理解“路径中的每一步必须是一度或二度连接”
# 这意味着我们可以从当前节点走到其一度连接,也可以走到其二度连接(通过一个中间节点)。
# 那么 BFS 的步长就不是固定的1了,而是1或2。
# 我们可以修改 BFS,每次出队一个节点时,尝试向其1跳和2跳的邻居扩展。
def find_shortest_linkedin_path_final(graph, user_a, user_b):
if user_a == user_b:
return [user_a]
queue = deque([(user_a, [user_a])])
# visited 存储的应该是用户,而不是路径,因为路径是动态构建的
# 但是为了避免无限循环,我们需要记录已访问的用户
# 这里为了记录最短路径,visited 应该存储 (user, length) 或只存储 user
# 并且一旦找到路径,就应该停止
visited = {user_a}
while queue:
current_user, path = queue.popleft()
# 尝试一步连接(一度连接)
for next_user_1 in graph.get(current_user, []):
if next_user_1 == user_b:
return path + [next_user_1]
if next_user_1 not in visited:
visited.add(next_user_1)
queue.append((next_user_1, path + [next_user_1]))
# 尝试两步连接(二度连接)
for next_user_2 in graph.get(next_user_1, []):
# 确保 next_user_2 不是 current_user 本身,避免无效的二度连接
if next_user_2 == current_user:
continue
if next_user_2 == user_b:
return path + [next_user_1, next_user_2] # 注意这里路径会包含中间节点 next_user_1
# 如果 next_user_2 尚未被访问,并且这是一个有效的新节点
# 我们将其加入队列,但要注意,它是在“两步”之后到达的
# 这意味着 path + [next_user_1, next_user_2] 才是完整的路径
# 这里的 visited 机制需要稍微调整,以确保最短路径
# 传统 BFS 遇到已访问节点会跳过,这里如果通过更长的路径到达已访问节点,也应该跳过
if next_user_2 not in visited:
# 对于二度连接,我们实际上是找到了一个更长的路径,所以如果目标是 user_b 应该立即返回
# 否则,如果将 next_user_2 加入队列,那么它的路径长度是 path 长度 + 2
# 而如果 next_user_2 也可以通过一度连接到达,那么 BFS 会优先找到一度连接的路径
# 这里的关键是确保最短路径,所以只有当 next_user_2 未被访问时才考虑
# 并且需要将 next_user_2 标记为已访问
# 但这样做可能会导致非最短路径,因为如果 next_user_2 之后可以更短的路径到达
# 所以,更好的做法是只在找到目标时才返回二度路径,否则只加入一度连接
pass # 不在这里直接加入二度连接到队列,而是让它们在后续的BFS迭代中作为一度连接被处理
# 核心思想是:BFS 保证了层级最短,如果某个节点可以在当前层通过一度连接到达,那它就是最短的。
# 如果只能通过二度连接到达,那它在下一层才会被考虑。
return None
# 让我们重新思考“路径中的每一步必须是一度或二度连接”
# 这意味着我们可以从 A 到 B 的路径是 A -> X1 -> X2 -> ... -> B
# 其中,A 到 X1 可以是 1 度或 2 度连接
# X1 到 X2 可以是 1 度或 2 度连接,以此类推。
# 这实际上是一个在修改后的图上寻找最短路径的问题。
# 修改后的图,如果 A 和 C 之间有 1 度连接,则边权重为 1。
# 如果 A 和 D 之间有 2 度连接(通过 E),则边权重为 1。
# 这样,BFS 就能正常运行。
# 更清晰的解法是:
# 1. 构建一个新的图,其中如果两个用户之间存在1度或2度连接,则在它们之间添加一条边。
# 2. 在新图上执行标准的 BFS。
# 但面试官期望的是在原图上直接解决,所以我们需要在 BFS 中动态处理。
# 最优的 BFS 处理方式是:当从 current_user 扩展时,我们可以到达:
# a) current_user 的一度连接
# b) current_user 的一度连接的一度连接(即 current_user 的二度连接)
# 让我们用更简洁的 BFS 逻辑来解决:
def find_shortest_linkedin_path_final_optimized(graph, user_a, user_b):
if user_a == user_b:
return [user_a]
# 队列存储 (当前用户, 当前路径)
queue = deque([(user_a, [user_a])])
# visited 集合记录已访问的用户,避免重复探索和循环
visited = {user_a}
while queue:
current_user, path = queue.popleft()
# 探索一度连接
for direct_friend in graph.get(current_user, []):
if direct_friend == user_b:
return path + [direct_friend]
if direct_friend not in visited:
visited.add(direct_friend)
queue.append((direct_friend, path + [direct_friend]))
# 探索二度连接 (通过 direct_friend 作为中间人)
for friend_of_friend in graph.get(direct_friend, []):
# 确保 friend_of_friend 不是 current_user 本身,避免无效的二度连接
if friend_of_friend == current_user:
continue
if friend_of_friend == user_b:
# 如果找到目标,返回路径。这里的路径会包含中间人
return path + [direct_friend, friend_of_friend]
# 如果 friend_of_friend 尚未访问,且不是 user_b,则将其作为新的探索点加入队列
# 这里的路径长度是 path.length + 2,所以要确保它比已访问的路径更短
# 但在 BFS 中,我们通常只加入一度连接,让 BFS 自动处理层级
# 这里的关键是:如果 friend_of_friend 是一个全新的、未访问的节点,且我们希望它成为下一跳的起点
# 那么它应该被加入队列,但其“成本”是2步。
# 传统的 BFS 每次只走一步。要满足“一步或两步”,需要调整 BFS 的步进
# 这可以用一个修改过的距离数组或将 (node, distance) 放入队列来解决。
return None
# 最终的,最能体现面试意图的解法:
# 我们将 BFS 的“步”定义为“一度或二度连接”。
# 这意味着从当前节点 X,我们可以到达 X 的直接邻居 Y (1步),或者 X 的直接邻居 Z 的直接邻居 W (2步)。
# 这里的“步”不是指路径中的边数,而是指“推荐关系”的跳数。
def find_shortest_linkedin_path_correct(graph, user_a, user_b):
if user_a == user_b:
return [user_a]
# 队列存储 (当前用户, 当前路径)
queue = deque([(user_a, [user_a])])
# visited 集合存储已访问的用户,防止循环和重复探索
# 这里存储的是用户,因为我们关心的是用户是否被访问过,而不是通过哪条路径访问
visited = {user_a}
while queue:
current_user, path = queue.popleft()
# 1. 探索一度连接 (一步推荐)
for friend_1 in graph.get(current_user, []):
if friend_1 == user_b:
return path + [friend_1]
if friend_1 not in visited:
visited.add(friend_1)
queue.append((friend_1, path + [friend_1]))
# 2. 探索二度连接 (通过 friend_1 作为中间人,两步推荐)
for friend_2 in graph.get(friend_1, []):
# 避免 friend_2 是 current_user 本身(即 C-A-C 这种无意义的二度连接)
if friend_2 == current_user:
continue
if friend_2 == user_b:
# 返回的路径包含中间人 friend_1,符合推荐路径的语境
return path + [friend_1, friend_2]
# 如果 friend_2 是一个新的、未访问过的节点,并且它可以通过“两步”推荐到达
# 我们将其加入队列,作为下一步探索的起点。
# 这里的关键是确保 BFS 能够按层级找到最短路径
# 也就是说,如果 friend_2 已经被更短的路径访问过,就不要再用更长的路径访问
# 但这里是特殊的“一步或两步”跳跃,所以需要更谨慎。
# 最简单且符合 BFS 精神的做法是:如果 friend_2 尚未被访问,就加入队列。
# BFS 自然会确保最短。
if friend_2 not in visited:
visited.add(friend_2)
queue.append((friend_2, path + [friend_1, friend_2])) # 路径包含了中间节点
return None
# 再次示例用法
linkedin_graph_example = {
"A": ["C", "D"],
"B": ["E", "F"],
"C": ["A", "G"],
"D": ["A", "H"],
"E": ["B", "G"],
"F": ["B"],
"G": ["C", "E"],
"H": ["D"]
}
path_final_corrected = find_shortest_linkedin_path_correct(linkedin_graph_example, "A", "B")
print(f"从 A 到 B 的最短推荐路径 (最终修正版): {path_final_corrected}")
# 期望输出:['A', 'D', 'H', 'B'] 或者 ['A', 'C', 'G', 'E', 'B']
# 让我们手动追踪 A -> B:
# A 出发
# 1. 1度连接: C, D
# - C: path=['A', 'C']
# - C 的 1度连接: A, G
# - G: path=['A', 'C', 'G']
# - G 的 1度连接: C, E
# - E: path=['A', 'C', 'G', 'E']
# - E 的 1度连接: B, G
# - B: return ['A', 'C', 'G', 'E', 'B'] (这是 4 步 1度连接)
# - C 的 2度连接 (通过 G):
# - G 的 1度连接是 E。所以 C 可以通过 G 到 E。路径 ['A', 'C', 'G', 'E']
# 如果 E 是 B,就返回 ['A', 'C', 'G', 'B']
# 修正:题目是“路径中的每一步必须是一度或二度连接”,这意味着我们不是在找最短的1度边路径,而是最短的“推荐跳数”。
# 也就是说,A->B 可能是一次二度连接,比如 A->X->B。那么路径就是 [A, X, B],跳数是1。
# 这就意味着,在 BFS 中,我们每探索一个节点,可以到达它的直接邻居(1跳),也可以到达它的二度邻居(1跳)。
# 所以,我们 BFS 的队列里存储的应该是 (user, current_recommendation_path),而不是 (user, current_edge_path)。
def find_shortest_linkedin_path_by_hops(graph, user_a, user_b):
if user_a == user_b:
return [user_a]
# 队列存储 (当前用户, 推荐路径)
queue = deque([(user_a, [user_a])])
visited = {user_a} # 记录已访问的用户
while queue:
current_user, current_recommendation_path = queue.popleft()
# 1. 探索一步推荐 (一度连接)
for friend_1 in graph.get(current_user, []):
if friend_1 == user_b:
return current_recommendation_path + [friend_1]
if friend_1 not in visited:
visited.add(friend_1)
queue.append((friend_1, current_recommendation_path + [friend_1]))
# 2. 探索两步推荐 (二度连接,通过 friend_1 作为中间人)
for friend_2 in graph.get(friend_1, []):
# 避免无效的二度连接 (如 A-C-A)
if friend_2 == current_user:
continue
if friend_2 == user_b:
# 如果找到目标,返回路径。路径中包含中间人
return current_recommendation_path + [friend_1, friend_2]
# 如果 friend_2 尚未被访问,将其加入队列
# 注意,这里我们是在 friend_1 的循环里处理 friend_2,这意味着 friend_2 的“跳数”和 friend_1 相同
# 因为它们都是从 current_user 出发的一跳或二跳
# 这里的 visited 机制需要确保最短路径。
# 如果 friend_2 已经通过更短的“推荐跳数”被访问,则跳过
if friend_2 not in visited:
visited.add(friend_2)
# 关键:这里的路径表示从 user_a 到 friend_2 的推荐路径
# 路径中包含中间人 friend_1,这符合“推荐路径”的描述
# 如果 friend_2 是通过二度连接到达的,那么它相当于从 current_user “跳跃”了一次
queue.append((friend_2, current_recommendation_path + [friend_1, friend_2]))
return None
# 再次示例用法
path_by_hops = find_shortest_linkedin_path_by_hops(linkedin_graph_example, "A", "B")
print(f"从 A 到 B 的最短推荐路径 (按跳数): {path_by_hops}")
# 让我们手动推导 A -> B:
# Start: A, Path: [A]
# Queue: [(A, [A])]
# Pop (A, [A])
# 1. 1度连接: C, D
# - C: path=[A, C]
# - C not in visited, add C to visited, Queue.append((C, [A, C]))
# - 2度连接 (通过 C):
# - C 的邻居: A, G
# - A == current_user, skip
# - G: friend_2=G. G not in visited, add G to visited, Queue.append((G, [A, C, G]))
# - D: path=[A, D]
# - D not in visited, add D to visited, Queue.append((D, [A, D]))
# - 2度连接 (通过 D):
# - D 的邻居: A, H
# - A == current_user, skip
# - H: friend_2=H. H not in visited, add H to visited, Queue.append((H, [A, D, H]))
# Queue: [(C, [A, C]), (D, [A, D]), (G, [A, C, G]), (H, [A, D, H])] (顺序可能不同)
# Pop (C, [A, C])
# 1. 1度连接: A, G
# - A == current_user, skip
# - G: visited. G already visited, skip
# - 2度连接 (通过 A): (不会发生,因为 A 是 current_user)
# - 2度连接 (通过 G):
# - G 的邻居: C, E
# - C == current_user, skip
# - E: friend_2=E. E not in visited, add E to visited, Queue.append((E, [A, C, G, E]))
# Pop (D, [A, D])
# 1. 1度连接: A, H
# - A == current_user, skip
# - H: visited. H already visited, skip
# - 2度连接 (通过 A): (不会发生)
# - 2度连接 (通过 H):
# - H 的邻居: D
# - D == current_user, skip
# - 这里没有 B,所以不直接返回
# Pop (G, [A, C, G])
# 1. 1度连接: C, E
# - C: visited. C already visited, skip
# - E: E not in visited, add E to visited, Queue.append((E, [A, C, G, E]))
# - 2度连接 (通过 C):
# - C 的邻居: A, G
# - A == current_user, skip
# - G == current_user, skip
# - 2度连接 (通过 E):
# - E 的邻居: B, G
# - B: friend_2=B. B == user_b. RETURN [A, C, G, E, B]
# 此时发现 B,返回 ['A', 'C', 'G', 'E', 'B']
# 这就是我们期望的最短推荐路径。
根据我们的提示,候选人迅速抓住了问题的核心——“每一步必须是一度或二度连接”意味着这不是简单的最短路径,而是在定义了新的“步长”后的最短路径问题。他清晰地阐述了如何通过修改标准的广度优先搜索(BFS)算法来实现这一目标:在 BFS 遍历过程中,每当从队列中取出一个用户时,不仅要探索其直接连接的用户(一度连接),还要探索其直接连接的用户的直接连接用户(二度连接),并将这些新发现且未访问过的用户加入队列。他强调了 visited 集合在避免重复探索和确保找到最短路径中的关键作用,并演示了如何动态构建包含中间节点的推荐路径。
候选人进一步解释说,这种方法巧妙地将“一度或二度连接”的约束融入到 BFS 的每一步探索中,保证了找到的路径是最短的“推荐跳数”路径。面试官对此表示高度认可,并赞赏候选人能够将抽象的图论算法与领英的实际业务场景紧密结合的思维能力。