二叉树递归算法实战:平衡判断与路径收集
1. 二叉树递归算法实战精解作为一名经历过多次算法面试洗礼的老程序员我深知二叉树递归问题在技术面试中的高频出现率。今天要分享的这四个题目——平衡二叉树判断、二叉树路径收集、左叶子节点求和以及完全二叉树节点统计涵盖了二叉树递归应用的经典场景。这些题目看似基础但其中蕴含的递归思维和优化技巧正是区分普通程序员和算法高手的试金石。2. 平衡二叉树判断110题2.1 问题本质与递归思路平衡二叉树的定义是一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。这个定义本身就是递归的——要判断整棵树是否平衡需要先判断其左右子树是否平衡这正是递归应用的完美场景。我常用的递归框架是这样的def isBalanced(root): def getHeight(node): if not node: return 0 left_height getHeight(node.left) right_height getHeight(node.right) if left_height -1 or right_height -1 or abs(left_height - right_height) 1: return -1 return max(left_height, right_height) 1 return getHeight(root) ! -12.2 关键优化点解析这个解法巧妙之处在于将高度计算和平衡判断合二为一。当发现任何子树不平衡时立即返回-1并向上传递避免不必要的计算。这种提前返回的技巧在递归中非常重要可以显著提升效率。注意在面试中面试官常常会追问这个解法的时间复杂度。正确的答案是O(n)因为每个节点只会被访问一次。这与直观认为的O(nlogn)不同需要特别注意解释。2.3 常见错误与调试技巧新手常见的错误包括仅判断根节点的左右子树高度差而忽略了对子树平衡性的检查在计算高度时没有正确处理空节点的情况忘记处理子树已经不平衡时需要立即返回的情况调试时可以添加打印语句输出每个节点的左右子树高度帮助理解递归过程。3. 二叉树所有路径收集257题3.1 递归回溯算法设计收集二叉树所有路径需要采用深度优先搜索(DFS)的策略并在递归过程中维护当前路径。当遇到叶子节点时将当前路径加入结果集。def binaryTreePaths(root): def dfs(node, path): if not node: return path str(node.val) if not node.left and not node.right: res.append(path) return path - dfs(node.left, path) dfs(node.right, path) res [] dfs(root, ) return res3.2 字符串处理优化路径的字符串拼接可以采用多种方式直接字符串拼接如上例使用列表保存路径节点最后用join连接使用StringBuilder类在Java等语言中在Python中第一种方法在少量数据时效率尚可但在路径较长时性能会下降。实际工程中更推荐第二种方法def binaryTreePaths(root): def dfs(node, path): if not node: return path.append(str(node.val)) if not node.left and not node.right: res.append(-.join(path)) path.pop() return dfs(node.left, path) dfs(node.right, path) path.pop() res [] dfs(root, []) return res3.3 非递归实现对比虽然题目要求优先掌握递归但了解非递归的实现也有助于深入理解问题。使用栈实现的DFS版本def binaryTreePaths(root): if not root: return [] res [] stack [(root, str(root.val))] while stack: node, path stack.pop() if not node.left and not node.right: res.append(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 res4. 左叶子节点求和404题4.1 左叶子的精确定义左叶子节点是指是父节点的左孩子且自身是叶子节点没有左右子树。这个定义看似简单但在实际编码时容易出错。关键在于判断时需要通过父节点来判断其左孩子是否为叶子。4.2 递归解法实现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)4.3 易错点分析错误地将所有左子节点都计入结果而忽略了叶子节点的条件在递归时忘记传递当前节点是否是左孩子的信息对空树的处理不完善提示在面试中可以主动询问面试官是否允许修改树节点的结构。如果可以另一种解法是为每个节点添加一个is_left标志但这通常不被允许。5. 完全二叉树节点统计222题5.1 完全二叉树特性利用完全二叉树的特点是除了最后一层其他层的节点都达到最大数量且最后一层的节点都集中在左侧。利用这个特性可以设计出比普通二叉树更高效的节点统计方法。5.2 递归结合完全二叉树特性的解法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)5.3 时间复杂度分析这个解法的时间复杂度是O(logN * logN)分析如下每次递归调用中计算左右深度需要O(logN)时间递归的深度最多为O(logN)层因此总时间复杂度为O(logN * logN)相比之下普通的递归遍历需要O(N)时间这个解法在完全二叉树上效率更高。6. 递归算法通用技巧总结6.1 递归三要素终止条件必须明确定义递归何时结束递归过程如何将大问题分解为小问题返回值明确每一层递归应该返回什么信息6.2 递归优化策略记忆化缓存已计算的结果如斐波那契数列问题尾递归优化某些语言支持尾递归优化避免栈溢出提前返回发现不满足条件时立即终止递归6.3 调试递归程序的方法打印递归深度和参数值使用较小的测试用例手动模拟递归过程添加全局计数器统计递归调用次数绘制递归树帮助理解在实际刷题过程中我发现很多同学对递归存在恐惧心理。其实递归就像洋葱一层层剥开每一层的结构都是一样的。掌握递归的关键是多练习、多画图、多思考终止条件。这四个题目虽然都标注优先掌握递归但理解其迭代解法也同样重要这能帮助我们从不同角度理解问题本质。