作为一名算法工程师,二叉树相关的问题是我们日常工作中最常遇到的题型之一。今天我想和大家分享四道LeetCode上经典的二叉树题目:平衡二叉树判断、左叶子节点求和、二叉树所有路径以及完全二叉树节点计数。这些题目不仅考察了我们对二叉树基本操作的掌握程度,也考验了我们优化算法的能力。
在实际面试中,这四类问题出现的频率非常高。我遇到过不少候选人,虽然能写出基本解法,但在时间复杂度分析和优化思路上往往表现不佳。通过这篇文章,我将从递归和迭代两种思路出发,详细解析每道题目的解题思路,并分享一些我在刷题和面试中总结出来的实用技巧。
平衡二叉树的定义是:一个二叉树每个节点的左右两个子树的高度差的绝对值不超过1。这个定义看似简单,但在实现时需要考虑很多边界条件。
我们先来看递归解法。递归的核心思想是"自底向上"计算每个节点的高度,并在计算过程中检查平衡性:
python复制class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
def height(node: Optional[TreeNode]):
if not node:
return 0
left_height = height(node.left)
if left_height == -1:
return -1
right_height = height(node.right)
if right_height == -1:
return -1
if abs(left_height - right_height) > 1:
return -1
return max(left_height, right_height) + 1
return height(root) != -1
这个解法有几个关键点需要注意:
提示:在实际面试中,面试官可能会要求解释为什么选择后序遍历。这是因为我们需要先知道子树的高度才能判断当前节点是否平衡。
虽然递归解法简洁优雅,但在实际工程中,我们有时需要考虑用迭代法来避免栈溢出的风险。迭代法的实现思路是模拟后序遍历的过程:
python复制class Solution:
def isBalanced(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
stack = [(root, False)]
heights = {}
while stack:
node, visited = stack.pop()
if not node:
continue
if not visited:
stack.append((node, True))
stack.append((node.right, False))
stack.append((node.left, False))
else:
left_h = heights.get(node.left, 0)
right_h = heights.get(node.right, 0)
if abs(left_h - right_h) > 1:
return False
heights[node] = max(left_h, right_h) + 1
return True
迭代法的几个要点:
左叶子节点的定义是:它是父节点的左子节点,且它自己没有子节点。这个问题考察的是我们对树遍历和节点条件判断的能力。
先看易理解的递归写法:
python复制class Solution:
def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
def getSumOfLeftLeaves(node: Optional[TreeNode]) -> int:
if not node:
return 0
left_sum = 0
if node.left and not node.left.left and not node.left.right:
left_sum += node.left.val
left_sum += getSumOfLeftLeaves(node.left)
left_sum += getSumOfLeftLeaves(node.right)
return left_sum
return getSumOfLeftLeaves(root)
这个解法采用前序遍历的顺序,在访问每个节点时检查其左子节点是否是叶子节点。如果是,就将它的值加入总和。
简化写法更简洁,但逻辑相同:
python复制class Solution:
def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
left_sum = 0
if root.left and not root.left.left and not root.left.right:
left_sum = root.left.val
return left_sum + self.sumOfLeftLeaves(root.left) + self.sumOfLeftLeaves(root.right)
迭代法使用栈来模拟递归过程,采用深度优先搜索的策略:
python复制class Solution:
def sumOfLeftLeaves(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
ans = 0
stk = [root]
while stk:
node = stk.pop()
if node.left and not node.left.left and not node.left.right:
ans += node.left.val
if node.right:
stk.append(node.right)
if node.left:
stk.append(node.left)
return ans
这里有个小技巧:虽然左右子节点压栈顺序不影响最终结果,但按照右左顺序压栈可以保持与前序遍历一致的处理顺序。
这个问题要求我们找出从根节点到所有叶子节点的路径。迭代法采用深度优先搜索,并在栈中存储节点和到该节点的路径:
python复制class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
if not root:
return []
paths = []
stack = [(root, str(root.val))]
while stack:
node, path = stack.pop()
if not node.left and not node.right:
paths.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 paths
这种方法的优点是直观易懂,缺点是当树很深时,字符串拼接操作可能会比较耗时。
递归解法更符合问题的本质,代码也更简洁:
python复制class Solution:
def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
def getPaths(node: Optional[TreeNode], path: str, paths: List[str]) -> None:
if node:
path += str(node.val)
if not node.left and not node.right:
paths.append(path)
else:
path += "->"
getPaths(node.left, path, paths)
getPaths(node.right, path, paths)
paths = []
getPaths(root, "", paths)
return paths
递归法的几个注意点:
最直观的解法是遍历整棵树并计数,这里以层序遍历为例:
python复制from collections import deque
class Solution:
def countNodes(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
dq = deque()
ans = 0
dq.append(root)
while dq:
n = len(dq)
ans += n
for _ in range(n):
cur = dq.popleft()
if cur.left:
dq.append(cur.left)
if cur.right:
dq.append(cur.right)
return ans
这种方法的时间复杂度是O(n),适用于任何二叉树,但没有利用完全二叉树的性质。
完全二叉树的特点是除了最后一层外都是满的,且最后一层节点尽可能靠左。我们可以利用这个性质来优化算法:
python复制class Solution:
def countNodes(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
left_depth = self.getDepth(root.left)
right_depth = self.getDepth(root.right)
if left_depth == right_depth:
# 左子树是满二叉树
return (1 << left_depth) + self.countNodes(root.right)
else:
# 右子树是满二叉树
return (1 << right_depth) + self.countNodes(root.left)
def getDepth(self, node):
depth = 0
while node:
depth += 1
node = node.left
return depth
这个优化算法的关键点:
注意:位运算(1 << h)比2^h计算更快,这是常用的优化技巧
在实际面试中,面试官通常会期望候选人先给出O(n)解法,然后再优化到O(log n * log n)。因此掌握这两种解法都很重要。