二叉树中的最大路径和
给定一棵非空二叉树,路径由相邻节点组成,每个节点最多经过一次。路径不必经过根节点,也不必从叶子节点开始或结束。
例如,在下面的二叉树中,最大路径是 15 → 20 → 7,最大路径和是 15 + 20 + 7 = 42。
text
-10
/ \
9 20
/ \
15 7
一、解题思路
当前节点必须先得到左右子树的计算结果,所以采用"左子树、右子树、当前节点"的后序遍历。
递归函数 maxGain(root) 的返回值不是整棵树的最大路径和,而是"从当前节点出发,只向一侧延伸时,能够返回给父节点的最大路径和"。
对于当前节点,左、右子树的贡献分别为 leftGain 和 rightGain。如果某个贡献是负数,连接这棵子树只会使路径和变小,因此通过 Math.max(0, 子树贡献) 将它舍弃。
一条路径如果在当前节点处结束,可以同时连接左右两侧,所以用 root.val + leftGain + rightGain 更新全局变量 maxSum。
但当前节点向父节点返回时,路径还要继续向上连接,不能同时保留左右两侧,否则路径会产生分叉。因此只能返回 root.val + Math.max(leftGain, rightGain)。
以节点 20 为例,左右子树分别返回 15 和 7。经过节点 20 的完整路径和是 15 + 20 + 7 = 42,所以用 42 更新 maxSum;而节点 20 返回给父节点时,只能选择较大的一侧,因此返回 20 + max(15, 7) = 35。
两种结果的职责不同:
text
root.val + leftGain + rightGain
用于更新整棵树的最大路径和
root.val + max(leftGain, rightGain)
用于返回给父节点继续计算
全局变量 maxSum 必须初始化为 Integer.MIN_VALUE。如果整棵树都是负数,最大路径和应当是其中最大的节点值,而不是 0。
二、完整代码
java
public class BinaryTreeMaximumPathSum {
static class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
/**
* 遍历过程中发现的最大完整路径和。
*/
private int maxSum;
public int maxPathSum(TreeNode root) {
maxSum = Integer.MIN_VALUE;
// 启动后序遍历,递归过程中会更新 maxSum。
maxGain(root);
return maxSum;
}
/**
* 返回从当前节点出发,只向一侧延伸时,
* 能够提供给父节点的最大路径和。
*/
private int maxGain(TreeNode root) {
if (root == null) {
return 0;
}
int leftGain = Math.max(
0,
maxGain(root.left)
);
int rightGain = Math.max(
0,
maxGain(root.right)
);
// 完整路径可以同时连接左右两侧。
int currentPathSum =
root.val + leftGain + rightGain;
maxSum = Math.max(
maxSum,
currentPathSum
);
// 返回父节点时只能选择一侧。
return root.val + Math.max(
leftGain,
rightGain
);
}
public static void main(String[] args) {
/*
* -10
* / \
* 9 20
* / \
* 15 7
*/
TreeNode root = new TreeNode(-10);
root.left = new TreeNode(9);
root.right = new TreeNode(20);
root.right.left = new TreeNode(15);
root.right.right = new TreeNode(7);
BinaryTreeMaximumPathSum solution =
new BinaryTreeMaximumPathSum();
int result = solution.maxPathSum(root);
System.out.println("最大路径和:" + result);
}
}
程序输出:
text
最大路径和:42
每个节点只计算一次,所以时间复杂度为 O(n);递归调用栈的最大深度等于树高,所以空间复杂度为 O(h)。
三、压栈、出栈与返回父节点
程序首先进入 maxPathSum(-10),将 maxSum 初始化为最小整数,然后调用 maxGain(-10)。此时 maxPathSum 暂停,等待整棵树递归结束。
text
栈顶
┌──────────────────────────┐
│ maxGain(-10) │
├──────────────────────────┤
│ maxPathSum:等待递归结束 │
└──────────────────────────┘
栈底
maxGain(-10) 先计算左子树,因此调用 maxGain(9)。节点 -10 的栈帧不会消失,而是留在栈中等待左子树返回。
text
栈顶
┌──────────────────────────┐
│ maxGain(9) │
├──────────────────────────┤
│ maxGain(-10):等待左结果 │
├──────────────────────────┤
│ maxPathSum:等待递归结束 │
└──────────────────────────┘
栈底
节点 9 的左右子树都是空节点,所以 leftGain = 0、rightGain = 0。经过节点 9 的完整路径和为 9 + 0 + 0 = 9,因此将 maxSum 更新为 9;随后,节点 9 向父节点 -10 返回 9 + max(0, 0) = 9,并从调用栈中弹出。
text
栈顶
┌──────────────────────────┐
│ maxGain(-10) │
│ leftGain = 9 │
├──────────────────────────┤
│ maxPathSum:等待递归结束 │
└──────────────────────────┘
栈底
maxSum = 9
得到左子树结果后,maxGain(-10) 调用 maxGain(20) 计算右子树。节点 20 又先调用 maxGain(15),因此栈中依次保存根节点和正在等待结果的父节点。
text
栈顶
┌──────────────────────────┐
│ maxGain(15) │
├──────────────────────────┤
│ maxGain(20):等待左结果 │
├──────────────────────────┤
│ maxGain(-10):等待右结果 │
├──────────────────────────┤
│ maxPathSum:等待递归结束 │
└──────────────────────────┘
栈底
节点 15 没有孩子,所以经过它的完整路径和是 15,maxSum 从 9 更新为 15。节点 15 向父节点 20 返回 15,然后出栈。
回到节点 20 后,leftGain = 15。接着调用 maxGain(7) 计算右子树:
text
栈顶
┌──────────────────────────┐
│ maxGain(7) │
├──────────────────────────┤
│ maxGain(20):等待右结果 │
├──────────────────────────┤
│ maxGain(-10):等待右结果 │
├──────────────────────────┤
│ maxPathSum:等待递归结束 │
└──────────────────────────┘
栈底
节点 7 没有孩子,所以向父节点 20 返回 7,然后出栈。此时节点 20 已经得到 leftGain = 15 和 rightGain = 7。
text
栈顶
┌──────────────────────────┐
│ maxGain(20) │
│ leftGain = 15 │
│ rightGain = 7 │
├──────────────────────────┤
│ maxGain(-10):等待右结果 │
├──────────────────────────┤
│ maxPathSum:等待递归结束 │
└──────────────────────────┘
栈底
经过节点 20 的完整路径和是 15 + 20 + 7 = 42,所以 maxSum 更新为 42。节点 20 向父节点 -10 返回时只能选择一侧,因此返回 20 + max(15, 7) = 35,然后出栈。
回到根节点 -10 后,左右子树的返回值分别是 9 和 35。
text
栈顶
┌──────────────────────────┐
│ maxGain(-10) │
│ leftGain = 9 │
│ rightGain = 35 │
├──────────────────────────┤
│ maxPathSum:等待递归结束 │
└──────────────────────────┘
栈底
maxSum = 42
经过根节点的完整路径和是 9 + (-10) + 35 = 34,小于当前的 42,所以 maxSum 不变。
根节点还会计算单边返回值 -10 + max(9, 35) = 25,但根节点没有父节点,所以这个返回值没有被使用。maxGain(-10) 出栈后,程序回到 maxPathSum。
text
栈顶
┌──────────────────────────┐
│ maxPathSum │
│ maxSum = 42 │
└──────────────────────────┘
栈底
最后,maxPathSum 返回 maxSum,结果为 42。
整个过程只有一条数据链:
text
子节点返回单边贡献
↓
父节点组合左右贡献
↓
更新全局最大路径和
↓
父节点再返回自己的单边贡献
maxGain 的返回值供父节点继续计算,maxSum 记录遍历过程中出现的最大完整路径,maxPathSum 在递归结束后返回最终结果。