最近在整理面试常见的算法与数据结构题目时,发现150题系列中的36-40题特别能考察候选人的基础功底。这几道题涵盖了二叉树、动态规划、回溯算法等核心知识点,都是大厂面试中的高频考点。作为面试官,我经常用这类题目来评估候选人的编码能力和思维严谨性。
在实际面试场景中,这类题目往往不是单纯考察能否写出正确答案,更重要的是解题过程中的思考方式。比如如何分析问题复杂度、边界条件的处理、代码的可读性等细节,都能反映出一个工程师的真实水平。下面我就结合自己的面试和刷题经验,详细拆解这几道经典题目。
给定一个二叉树和两个节点p、q,找到它们的最低公共祖先(LCA)。最近公共祖先的定义是两个节点在树中的最低共同父节点,且一个节点可以是自己的祖先。
示例:
code复制输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点5和1的LCA是3
最直观的解法是使用递归遍历二叉树:
python复制def lowestCommonAncestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowestCommonAncestor(root.left, p, q)
right = lowestCommonAncestor(root.right, p, q)
if left and right:
return root
return left if left else right
时间复杂度O(n),空间复杂度O(h),h为树高。这个解法利用了后序遍历的特性,先处理子树再判断当前节点。
注意:面试时要特别说明递归终止条件的设置原因,这是面试官常问的点。
另一种思路是记录从根到p和q的路径,然后比较两条路径:
python复制def lowestCommonAncestor(root, p, q):
stack = [(root, [root])]
paths = []
while stack and len(paths) < 2:
node, path = stack.pop()
if node == p or node == q:
paths.append(path)
if node.left:
stack.append((node.left, path + [node.left]))
if node.right:
stack.append((node.right, path + [node.right]))
lca = None
for a, b in zip(*paths):
if a == b:
lca = a
else:
break
return lca
这种方法虽然直观,但空间复杂度较高,适合解释思路但不推荐作为最优解。
给定一个候选数字集合candidates和一个目标数target,找出所有使数字和为target的组合。candidates中的每个数字在每个组合中只能使用一次。
与组合总和I的区别在于:
示例:
code复制输入: candidates = [10,1,2,7,6,1,5], target = 8
输出: [
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
关键点在于去重和剪枝:
python复制def combinationSum2(candidates, target):
res = []
candidates.sort()
def backtrack(start, path, remain):
if remain == 0:
res.append(path.copy())
return
for i in range(start, len(candidates)):
if i > start and candidates[i] == candidates[i-1]:
continue
if candidates[i] > remain:
break
path.append(candidates[i])
backtrack(i+1, path, remain - candidates[i])
path.pop()
backtrack(0, [], target)
return res
时间复杂度O(2^n),最坏情况下需要遍历所有子集。优化点:
实战经验:在面试中要主动解释为什么需要i > start这个条件,这是去重的关键。
给定一个未排序的整数数组,找出其中没有出现的最小的正整数。要求时间复杂度O(n),空间复杂度O(1)。
示例:
code复制输入: [3,4,-1,1]
输出: 2
这道题的难点在于严格的时空复杂度限制,无法使用常规的哈希表方法。
核心思想是利用数组本身作为哈希表:
python复制def firstMissingPositive(nums):
n = len(nums)
# 第一遍遍历,将数字放到正确位置
for i in range(n):
while 1 <= nums[i] <= n and nums[nums[i]-1] != nums[i]:
nums[nums[i]-1], nums[i] = nums[i], nums[nums[i]-1]
# 第二遍遍历找第一个不匹配的位置
for i in range(n):
if nums[i] != i+1:
return i+1
return n+1
时间复杂度分析:虽然有两层循环,但每个数字最多被交换两次,所以总体是O(n)。
给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子能接多少雨水。
示例:
code复制输入: [0,1,0,2,1,0,1,3,2,1,2,1]
输出: 6
最优解法使用双指针,空间复杂度O(1):
python复制def trap(height):
left, right = 0, len(height)-1
left_max = right_max = 0
res = 0
while left < right:
if height[left] < height[right]:
if height[left] >= left_max:
left_max = height[left]
else:
res += left_max - height[left]
left += 1
else:
if height[right] >= right_max:
right_max = height[right]
else:
res += right_max - height[right]
right -= 1
return res
面试中建议先解释暴力思路,再逐步优化到双指针解法。
给定一个可包含重复数字的序列,返回所有不重复的全排列。
示例:
code复制输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
关键点在于剪枝条件的设置:
python复制def permuteUnique(nums):
res = []
nums.sort()
used = [False] * len(nums)
def backtrack(path):
if len(path) == len(nums):
res.append(path.copy())
return
for i in range(len(nums)):
if used[i] or (i > 0 and nums[i] == nums[i-1] and not used[i-1]):
continue
used[i] = True
path.append(nums[i])
backtrack(path)
path.pop()
used[i] = False
backtrack([])
return res
剪枝条件i > 0 and nums[i] == nums[i-1] and not used[i-1]表示:
这种去重方式保证了不会生成重复的排列。
对于递归算法,可以用递归树和主定理来分析。例如回溯算法的时间复杂度通常是O(分支数^递归深度)。
在实际面试中,即使不能立即给出最优解,也要清晰地展示思考过程。我见过很多候选人虽然最终代码有小问题,但因为思路清晰、沟通良好而通过了面试。