在大多数公司,软件工程师面试都遵循一个熟悉的流程:招聘人员电话、一到两轮编码、面向中级和高级岗位的系统设计轮,以及行为面试轮。下面按轮次整理了 20 道软件工程师面试题及答案。发布前,每段代码示例都已用成千上万组随机输入与暴力解法进行对比测试。
每一轮考察什么
| 轮次 | 常见形式 | 考察重点 |
|---|---|---|
| 招聘人员初筛 | 电话沟通 20 到 30 分钟 | 背景、求职动机、薪资范围、流程安排 |
| 编码 | 在共享编辑器中进行 45 到 60 分钟 | 解题能力、代码整洁度、测试意识、边写边讲 |
| 计算机基础 | 穿插在编码轮中,或作为单独一轮 | 你是否理解自己每天使用的系统 |
| 系统设计 | 在白板或文档上进行 45 到 60 分钟 | 权衡取舍、规模化能力、清晰表达 |
| 行为面试 | 30 到 45 分钟 | 主人翁意识、协作能力、如何应对失败 |
应届生面试以编码和行为面试为主。资深岗位会增加系统设计,并期待你对过往项目给出更深入的回答。关于招聘人员电话,请参阅我们的电话初筛面试题。
编码题
每一轮编码面试中,动手打字前先说明思路,最后给出时间和空间复杂度,并用一个小例子进行测试。复杂度部分可以参考我们的大 O 速查表。
1. 两数之和:返回相加等于目标值的两个数的下标
把每个数字及其下标存入哈希表。每读到一个新数字,就检查它的补数是否已经在表中。
def two_sum(nums, target):
seen = {}
for i, x in enumerate(nums):
if target - x in seen:
return [seen[target - x], i]
seen[x] = i
return []
print(two_sum([2, 7, 11, 15], 9)) # [0, 1]
时间复杂度 O(n),空间复杂度 O(n)。暴力解法要检查每一对数字,时间复杂度为 O(n²)。面试官通常希望你先提到这个解法,再进行优化。
2. 有效括号:检查每个括号是否按正确的顺序闭合
把左括号压入栈中。遇到右括号时,栈顶必须是与之匹配的左括号。
def is_valid(s):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for ch in s:
if ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False
else:
stack.append(ch)
return not stack
print(is_valid("([]{})"), is_valid("(]")) # True False
时间复杂度 O(n),空间复杂度 O(n)。最后一行很关键:像 “((” 这样的字符串会在栈中留下元素,因此它不是有效的括号。
3. 合并重叠区间
先按起始时间排序,然后要么延长最后一个已合并的区间,要么开始一个新区间。
def merge_intervals(intervals):
merged = []
for start, end in sorted(intervals):
if merged and start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return merged
print(merge_intervals([[1, 3], [2, 6], [8, 10], [15, 18]]))
# [[1, 6], [8, 10], [15, 18]]
由于排序,时间复杂度为 O(n log n)。可以先问面试官,像 [1, 2] 和 [2, 3] 这种首尾相接的区间是否应该合并。本版本会将它们合并。
4. 反转链表
遍历一次链表,让每个节点指向它的前一个节点。
class ListNode:
def __init__(self, val, next=None):
self.val = val
self.next = next
def reverse_list(head):
prev = None
while head:
head.next, prev, head = prev, head, head.next
return prev
时间复杂度 O(n),额外空间复杂度 O(1)。如果面试官觉得这种多重赋值写法太取巧,可以改写成三行,借助一个临时变量。清晰比简短更重要。
5. 返回出现频率最高的 k 个元素
先用哈希表统计次数,再用堆取出次数最大的 k 个。
from collections import Counter
import heapq
def top_k_frequent(nums, k):
counts = Counter(nums)
return [x for x, _ in heapq.nlargest(k, counts.items(), key=lambda p: p[1])]
print(top_k_frequent([1, 1, 1, 2, 2, 3], 2)) # [1, 2]
时间复杂度 O(n log k)。面试官可能追问:按频率进行桶排序可以把复杂度降到 O(n)。
6. 设计一个 get 和 put 都是 O(1) 的 LRU 缓存
你既需要快速查找,又需要按最近使用顺序快速调整位置。哈希表加双向链表可以同时满足这两点。在 Python 中,OrderedDict 正好就是这种组合。
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.data = OrderedDict()
def get(self, key):
if key not in self.data:
return -1
self.data.move_to_end(key)
return self.data[key]
def put(self, key, value):
if key in self.data:
self.data.move_to_end(key)
self.data[key] = value
if len(self.data) > self.capacity:
self.data.popitem(last=False)
有些面试官会要求你自己实现链表。先说明结构:哈希表指向链表节点,最近使用的节点位于一端,淘汰时从另一端移除。
7. 带障碍物网格中的最短路径
当每一步的代价都相同时,广度优先搜索可以找到最短路径。
from collections import deque
def shortest_path(grid):
rows, cols = len(grid), len(grid[0])
if grid[0][0] or grid[rows - 1][cols - 1]:
return -1
queue = deque([(0, 0, 0)])
seen = {(0, 0)}
while queue:
r, c, steps = queue.popleft()
if (r, c) == (rows - 1, cols - 1):
return steps
for dr, dc in ((1, 0), (-1, 0), (0, 1), (0, -1)):
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and not grid[nr][nc] and (nr, nc) not in seen:
seen.add((nr, nc))
queue.append((nr, nc, steps + 1))
return -1
print(shortest_path([[0, 0, 0], [1, 1, 0], [0, 0, 0]])) # 4
时间和空间复杂度均为 O(rows × cols)。把单元格加入队列时就标记为已访问,而不是出队时才标记,否则同一个单元格可能会被多次入队。
8. 找出第一个坏版本
版本 1 到 n 在某个点之前都是好的,之后都是坏的。二分查找可以用 O(log n) 次检查找到第一个坏版本。
def first_bad_version(n, is_bad):
lo, hi = 1, n
while lo < hi:
mid = (lo + hi) // 2
if is_bad(mid):
hi = mid
else:
lo = mid + 1
return lo
这个模式适用于任何只会变化一次的是非判断问题,这类题目很常见。如果想找覆盖这些模式的题目清单,请参阅 Blind 75。
计算机基础题
9. 进程和线程有什么区别?
进程是一个正在运行的程序,拥有自己独立的内存空间。线程运行在进程内部,共享该进程的内存。线程的创建和切换开销更小,并且可以直接共享数据,但正是这种共享使得我们需要锁,也会出现竞态条件。进程之间相互隔离,因此一个进程崩溃不会拖垮其他进程。
10. TCP 和 UDP 有什么区别?
TCP 会先建立连接,并通过重传和流量控制保证数据有序、可靠地送达。UDP 发送的是彼此独立的数据包,不保证送达和顺序,但开销更小。网页、API 和文件传输适合使用 TCP。实时语音、视频和游戏更适合 UDP,因为迟到的数据包本来就没有用。想深入了解,可以看我们的计算机网络面试题。
11. 在浏览器中输入网址并按下回车后会发生什么?
浏览器先通过 DNS 把域名解析为 IP 地址,然后建立 TCP 连接,对于 HTTPS 还要进行 TLS 握手。接着发送 HTTP 请求,服务器(通常位于负载均衡器之后)返回响应。浏览器解析 HTML,请求其中引用的 CSS、JavaScript 和图片,然后渲染页面。面试官选哪一部分,你就深入讲哪一部分。
12. 哈希表是如何工作的?它的时间复杂度是多少?
哈希函数把每个键映射到数组中的一个桶。平均情况下,查找、插入和删除的时间复杂度都是 O(1)。当两个键落入同一个桶时,哈希表通过链表法或探测其他槽位来处理冲突,并在太满时扩容。如果大量键发生冲突,最坏情况为 O(n)。
13. 什么时候应该选择 SQL 数据库而不是 NoSQL 数据库?
当数据具有关系性、需要连接查询和事务,并且结构相对稳定时,选择 SQL,例如订单、支付和用户账户。当访问模式简单、数据结构多变,或者需要把写入扩展到多台机器上时,选择文档或键值存储。很多系统会同时使用两者。面试官想听的是权衡取舍,而不是一个非此即彼的结论。通常位于其上层的 API 部分,可以参阅我们的REST API 面试题。
系统设计题
14. 设计一个短链接服务
先明确需求:每天新增多少链接、读取量有多大、链接是否会过期、是否允许自定义别名。然后讲清楚 API(创建和跳转)、短码的生成方式(用 62 进制编码的计数器,或带唯一性检查的随机码)、存储(从短码到原始网址的键值存储),以及针对热门链接的缓存,因为读请求远多于写请求。如果时间允许,最后再讲数据分析和滥用防护。我们在系统设计指南中完整讲解了这个设计。
15. 设计一个限流器
先确定限流在哪里执行(API 网关层,按用户或按 API 密钥),以及客户端超出限制后会发生什么(返回 HTTP 429 并附带重试时间)。常见的选择是令牌桶:每个客户端有一个桶,按固定速率补充令牌,桶中最多存放一定数量的令牌,每个请求消耗一个令牌。它允许短时间的突发流量,同时保证平均速率不超过限制。如果有多台服务器,可以把计数器放在 Redis 等共享存储中,让每台服务器看到的计数保持一致。想要更多练习,请参阅我们的系统设计面试题。
行为面试题
每个故事都使用清晰的结构:背景、你做了什么、结果如何。有数据的话尽量写出具体数字。
16. 请介绍一下你自己
用两分钟讲述你的职业经历,最后说明你为什么想要这个岗位。提及一两个有具体成果的项目。我们关于如何介绍自己的指南里有示例。
17. 讲一个你修复过的棘手 bug
示例回答:“我们的结账服务每天会出现几次超时,而且没有规律。我在日志中加入了请求 ID,发现每次超时都来自同一条数据库查询,而这条查询只在购物车超过 50 件商品时才会执行。查询的连接字段缺少索引。我加上索引后,慢查询的耗时从大约 4 秒降到 50 毫秒以下,并且为该查询的延迟加了告警,以便下次能更早发现问题。”
18. 讲一次你和同事意见不一致的经历
示例回答:“一位同事想用新框架重写我们的通知服务,而我认为上线前风险太高。我没有在会上争执,而是建议我们各自把成本和风险写下来。最后我们达成一致:上线前先修复当前代码中最严重的两个问题,把重写安排在下个季度。版本按时上线了,两个月后我们带着完整的测试方案完成了重写。”
19. 讲一个失败的项目
选一个你负责过其中一部分的真实失败案例。说明哪里出了问题,如果重来会怎么做,之后又做了哪些改变。面试官看重的是诚实和学习能力,而不是完美的履历。
20. 你为什么想来我们这里工作?
把公司产品或工程工作中的一个具体点,与你自己的经历联系起来。“我用你们的 API 已经两年了,希望能参与平台方面的工作”,比“你们是一家很棒的公司”要好得多。
可以问面试官的问题
- 团队里的人一般一周是怎么过的?
- 工作是如何规划的?优先级由谁决定?
- 这里的代码审查和部署是怎么运作的?
- 如果入职后的前六个月做得好,会是什么样子?
- 团队目前在攻克的最难的技术问题是什么?
更多内容请参阅面试结束时可以问的问题。
如何备考
- 选定一门语言,所有练习题都用它来做。Python 最常见,因为代码简短。关于语言本身的内容,可以参阅我们的Python 面试题。
- 按题型练习,而不是比谁做得多:哈希表、双指针、滑动窗口、二分查找、BFS 和 DFS、堆,以及动态规划。
- 计时并出声做题。无声做出正确答案,得分反而低于清楚地讲出来的答案。
- 用你面试时会用的编辑器练习。很多公司使用 CoderPad,它提供免费的沙盒环境。
- 准备五个来自工作经历的故事,让它们能灵活套用到大多数行为面试题上。
- 如果应聘中级或高级岗位,至少做几次完整的系统设计练习,并请人对你进行追问。
出声练习编程面试
YesToTheOffer 会转录你的回答,方便你回看自己是如何讲解解法的。截图编程辅助支持 0.2 倍速。30 分钟免费,无订阅。



