1. 算法训练营第15天核心任务解析
今天我们要集中攻克四个经典的二叉树问题:平衡二叉树判断、所有路径遍历、左叶子节点求和以及完全二叉树节点计数。这组题目看似独立,实则层层递进地考察了二叉树的遍历、递归和迭代技巧。作为代码随想录训练营的学员,我建议按照以下顺序攻关:先解决基础的路径遍历(257题),再处理左叶子求和(404题),接着完成节点计数(222题),最后挑战平衡二叉树判断(110题)。这种由浅入深的顺序能帮助我们逐步建立解题信心。
在开始编码前,必须明确每个问题的核心考察点:
- 257题考验深度优先搜索(DFS)的路径记录能力
- 404题需要准确识别左叶子的定义
- 222题则要利用完全二叉树的性质优化计算
- 110题则综合考察递归和高度计算
提示:建议准备一张白纸,在解题前先手动绘制二叉树示例,标注节点关系和遍历顺序,这对理解题意大有裨益。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 257. 二叉树的所有路径详解
2.1 问题重述与示例分析
给定一个二叉树,返回所有从根节点到叶子节点的路径。例如输入:
code复制 1
/ \
2 3
\
5
应输出:["1->2->5", "1->3"]
这个问题的关键在于两点:如何遍历所有路径?如何正确拼接路径字符串?我最初尝试时犯了个错误——在递归过程中直接拼接字符串,导致结果出现重复片段。后来发现更优雅的做法是使用列表暂存路径节点,在到达叶子节点时统一拼接。
2.2 递归解法实现细节
以下是Python的标准解法:
python复制def binaryTreePaths(root):
def dfs(node, path, res):
if not node:
return
path.append(str(node.val))
if not node.left and not node.right:
res.append("->".join(path))
dfs(node.left, path, res)
dfs(node.right, path, res)
path.pop()
res = []
dfs(root, [], res)
return res
几个关键注意点:
- 使用列表而非字符串暂存路径,避免递归回退时的字符串切割问题
- 在叶子节点判断处(not node.left and not node.right)才进行最终路径拼接
- 必须记得做path.pop()进行回溯,这是很多初学者容易遗漏的
2.3 迭代解法与性能对比
虽然递归解法简洁,但面试时可能会被要求给出迭代方案。使用栈实现的DFS迭代版本:
python复制def binaryTreePaths(root):
if not root:
return []
stack = [(root, [str(root.val)])]
res = []
while stack:
node, path = stack.pop()
if not node.left and not node.right:
res.append("->".join(path))
if node.right:
stack.append((node.right, path + [str(node.right.val)]))
if node.left:
stack.append((node.left, path + [str(node.left.val)]))
return res
实测发现,对于深度较大的树,迭代版本可以避免递归栈溢出的风险,但代码稍显复杂。在LeetCode的测试用例中,两种方法的时间复杂度都是O(n),空间复杂度也都是O(n)。
3. 404. 左叶子之和的精准识别
3.1 左叶子的精确定义
这个问题看似简单,但很容易出错。左叶子不是简单的左子节点,而必须同时满足:
- 是某个节点的左孩子
- 自身是叶子节点(无左右子树)
例如在下面的树中:
code复制 3
/ \
9 20
/ \
15 7
只有9是左叶子,15虽然是左节点但不是叶子,20的右节点7虽然是叶子但不是左节点。
3.2 递归解法实现技巧
标准解法需要通过父节点来判断左叶子:
python复制def sumOfLeftLeaves(root):
if not root:
return 0
def isLeaf(node):
return not node.left and not node.right
total = 0
if root.left and isLeaf(root.left):
total += root.left.val
total += sumOfLeftLeaves(root.left)
total += sumOfLeftLeaves(root.right)
return total
这里有个优化点:可以在递归函数中传递is_left标志来避免重复判断:
python复制def sumOfLeftLeaves(root):
def dfs(node, is_left):
if not node:
return 0
if not node.left and not node.right:
return node.val if is_left else 0
return dfs(node.left, True) + dfs(node.right, False)
return dfs(root, False)
3.3 常见错误与边界情况
我在初次实现时踩过这些坑:
- 误将根节点计入结果(根节点不是任何节点的左孩子)
- 忘记处理空树的情况
- 对只有右子树的节点处理不当
测试用例要特别关注:
- 空树
- 只有根节点
- 所有叶子都是右叶子
- 单边倾斜的树
4. 222. 完全二叉树的节点计数优化
4.1 完全二叉树的性质利用
普通二叉树的节点计数可以直接遍历,时间复杂度O(n)。但完全二叉树具有特殊性质:
- 除最后一层外,其他层节点都达到最大数
- 最后一层节点都集中在左侧
利用这个性质,我们可以实现O(log n * log n)的算法:
python复制def countNodes(root):
if not root:
return 0
left_depth = right_depth = 0
left = right = root
while left:
left_depth += 1
left = left.left
while right:
right_depth += 1
right = right.right
if left_depth == right_depth:
return (1 << left_depth) - 1
return 1 + countNodes(root.left) + countNodes(root.right)
4.2 算法复杂度分析
这个解法巧妙之处在于:
- 先计算左右子树的最左深度
- 如果左右深度相等,说明是满二叉树,直接使用公式2^h - 1计算
- 否则递归计算左右子树
每次递归都有一侧可以直接计算,因此时间复杂度为O(log n * log n)。实测在LeetCode上运行时间从普通解法的80ms提升到了60ms左右。
4.3 层序遍历对比实现
作为对比,常规的层序遍历解法:
python复制def countNodes(root):
if not root:
return 0
queue = [root]
count = 0
while queue:
node = queue.pop(0)
count += 1
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return count
虽然代码更简单,但在最坏情况下(比如线性链表状的树)时间复杂度会退化到O(n)。
5. 110. 平衡二叉树的深度判断
5.1 平衡二叉树的定义
平衡二叉树是指:对于任意节点,其左右子树的高度差不超过1。注意这与完全二叉树、满二叉树是不同的概念。例如:
code复制 1
/ \
2 2
/ \
3 3
/ \
4 4
这棵树看起来对称,但由于最下层叶子深度差超过1,所以不是平衡二叉树。
5.2 自顶向下与自底向上解法
初学者最容易想到的是自顶向下的递归:
python复制def isBalanced(root):
if not root:
return True
left_height = height(root.left)
right_height = height(root.right)
return abs(left_height - right_height) <= 1 and \
isBalanced(root.left) and \
isBalanced(root.right)
def height(node):
if not node:
return 0
return 1 + max(height(node.left), height(node.right))
但这种解法存在大量重复计算,时间复杂度O(n^2)。更优的方法是自底向上:
python复制def isBalanced(root):
return dfs(root)[0]
def dfs(node):
if not node:
return (True, 0)
left = dfs(node.left)
right = dfs(node.right)
balanced = left[0] and right[0] and abs(left[1] - right[1]) <= 1
return (balanced, 1 + max(left[1], right[1]))
这个版本每个节点只计算一次,时间复杂度优化到O(n)。
5.3 迭代解法实现
虽然递归更直观,但面试时可能需要迭代实现。使用后序遍历的迭代版本:
python复制def isBalanced(root):
stack = []
node = root
last = None
depths = {}
while stack or node:
if node:
stack.append(node)
node = node.left
else:
node = stack[-1]
if not node.right or last == node.right:
node = stack.pop()
left = depths.get(node.left, 0)
right = depths.get(node.right, 0)
if abs(left - right) > 1:
return False
depths[node] = 1 + max(left, right)
last = node
node = None
else:
node = node.right
return True
这个实现较为复杂,但能避免递归栈溢出的风险,适合深度非常大的树。
6. 四题联动的综合训练价值
这组题目虽然各自独立,但结合起来训练效果极佳。我在反复练习中发现:
- 257题训练了DFS路径记录能力,为其他问题的递归思路打下基础
- 404题强化了对节点特殊属性的判断,这种思维在110题的高度差计算中再次用到
- 222题教会我们利用数据结构特性优化算法,这种思想可以迁移到其他问题
- 110题则综合考验递归和迭代的灵活运用
建议的练习顺序应该是:先独立完成每个题目,再尝试找出四题的共同点。比如都可以用DFS解决,但各自有不同的终止条件和处理逻辑。这种对比训练能快速提升二叉树问题的解决能力。
在实际编码时,我习惯为每个问题准备3-5个测试用例,包括:
- 空树
- 单节点树
- 完全倾斜的树
- 普通平衡树
- 特殊构造的树(如只有左叶子)
这种全面的测试能帮助发现边界条件的处理漏洞。
