1. 问题背景与理解
今天我们来聊聊LeetCode第1372题"二叉树中的最长交错路径"。这是一道中等难度的二叉树题目,但它的解法思路非常有意思,能帮助我们深入理解二叉树的遍历和状态管理。
所谓"交错路径",指的是路径中相邻节点的方向交替变化。比如,如果当前节点是从左子节点过来的,那么下一个节点就必须是从右子节点过来的,反之亦然。我们的任务就是找到这样一条最长的路径。
举个例子,考虑这样一棵二叉树:
code复制 1
/ \
2 3
/ \ \
4 5 6
/ \ \
7 8 9
在这棵树中,最长的交错路径是[2,5,7]或[2,5,8],长度都是2(路径长度定义为边的数量)。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路分析
2.1 暴力解法与优化方向
最直观的想法可能是对每个节点都进行一次完整的遍历,记录所有可能的交错路径,然后找出最长的。但这种暴力解法的时间复杂度会达到O(n²),对于较大的树显然不够高效。
我们需要寻找一种能在单次遍历中完成计算的方法。这提示我们可能需要使用深度优先搜索(DFS)或广度优先搜索(BFS),并在遍历过程中维护一些状态信息。
2.2 关键观察点
- 每个节点的交错路径长度取决于它的父节点是从哪个方向来的
- 对于任意节点,我们需要知道:
- 如果是从左子节点过来的,那么当前节点的右子节点可以继续交错路径
- 如果是从右子节点过来的,那么当前节点的左子节点可以继续交错路径
- 我们需要在遍历过程中同时跟踪两个方向的最长路径
2.3 递归关系建立
我们可以定义两个辅助函数:
dfs(node, isLeft, length):表示从某个方向(isLeft)到达当前节点时,已经形成的交错路径长度
递归关系可以表示为:
- 如果是从左子节点过来的(isLeft=true),那么:
- 向右走时可以继续交错路径:
rightLength = dfs(right, false, length+1) - 向左走时需要重置路径:
leftLength = dfs(left, true, 1)
- 向右走时可以继续交错路径:
- 如果是从右子节点过来的(isLeft=false),同理
3. 具体实现方案
3.1 递归实现
python复制class Solution:
def longestZigZag(self, root: TreeNode) -> int:
self.max_len = 0
def dfs(node, is_left, length):
if not node:
return
self.max_len = max(self.max_len, length)
if is_left:
dfs(node.right, False, length + 1)
dfs(node.left, True, 1)
else:
dfs(node.left, True, length + 1)
dfs(node.right, False, 1)
dfs(root.left, True, 1)
dfs(root.right, False, 1)
return self.max_len
3.2 迭代实现
对于不喜欢递归或者担心栈溢出的情况,我们也可以用迭代的方式实现:
python复制class Solution:
def longestZigZag(self, root: TreeNode) -> int:
if not root:
return 0
max_len = 0
stack = [(root.left, True, 1), (root.right, False, 1)]
while stack:
node, is_left, length = stack.pop()
if not node:
continue
max_len = max(max_len, length)
if is_left:
stack.append((node.right, False, length + 1))
stack.append((node.left, True, 1))
else:
stack.append((node.left, True, length + 1))
stack.append((node.right, False, 1))
return max_len
3.3 时间复杂度分析
无论是递归还是迭代实现,我们都只对每个节点访问了常数次(确切地说是2次),因此时间复杂度是O(n),其中n是树中节点的数量。空间复杂度在最坏情况下(树退化为链表)也是O(n)。
4. 优化与变种思考
4.1 记忆化优化
虽然我们的解法已经是O(n)时间复杂度,但如果我们想进一步优化常数因子,可以考虑记忆化。不过对于这个问题,记忆化的收益不大,因为每个节点的计算已经是最优的了。
4.2 其他遍历方式
我们也可以尝试用BFS来解决这个问题。BFS的实现思路与DFS类似,只是使用队列代替栈:
python复制from collections import deque
class Solution:
def longestZigZag(self, root: TreeNode) -> int:
if not root:
return 0
max_len = 0
queue = deque([(root.left, True, 1), (root.right, False, 1)])
while queue:
node, is_left, length = queue.popleft()
if not node:
continue
max_len = max(max_len, length)
if is_left:
queue.append((node.right, False, length + 1))
queue.append((node.left, True, 1))
else:
queue.append((node.left, True, length + 1))
queue.append((node.right, False, 1))
return max_len
4.3 类似问题扩展
掌握了这个问题的解法后,可以尝试解决一些类似的变种问题:
- 找出所有最长交错路径而不仅仅是长度
- 计算交错路径的数量而非最大长度
- 在普通图中而非二叉树中寻找最长交错路径
5. 常见错误与调试技巧
5.1 初始条件设置
一个常见的错误是初始条件的设置。注意根节点本身不算在路径内,我们需要从根节点的左右子节点开始计算。这也是为什么在代码中我们分别对root.left和root.right进行初始调用。
5.2 路径长度定义
另一个容易混淆的是路径长度的定义。题目中定义为边的数量而非节点的数量。例如,路径A->B->C的长度是2而不是3。
5.3 空节点处理
在处理空节点时,我们需要小心不要增加路径长度。这也是为什么在代码中我们有if not node: continue的判断。
5.4 测试用例建议
为了验证代码的正确性,建议准备以下测试用例:
- 空树
- 单节点树
- 完全左斜或右斜的树
- 题目中的示例树
- 较大的随机生成的树
6. 实际应用场景
虽然这个问题看起来是纯算法题,但类似的路径搜索技术在现实中有很多应用:
- 网络路由:寻找满足特定跳转条件的网络路径
- 游戏AI:在游戏地图中寻找符合特定移动模式的路径
- 数据压缩:在树形结构中寻找可压缩的重复模式
- 生物信息学:分析DNA序列或蛋白质结构中的特定模式
理解这类问题的解法,可以帮助我们在面对更复杂的树形结构问题时,快速找到解决方案的思路。
7. 个人解题心得
在实际解决这个问题时,我最初尝试了暴力解法,很快就遇到了性能问题。通过分析问题特性,我发现关键在于在遍历过程中维护状态信息。这种"携带状态的遍历"是解决许多树形结构问题的通用技巧。
另一个收获是对路径长度定义的理解。在树形问题中,明确长度是节点数还是边数非常重要,这直接影响边界条件的处理。我建议在解题时,先用小例子手动计算验证理解是否正确。
最后,对于递归解法,我发现在处理树形结构时,先明确递归函数的含义(参数表示什么,返回值表示什么)非常重要。这比直接开始写代码更能保证逻辑的正确性。
