二叉树的坡度 图解题解
这道题到底在问什么
- 输入
- root=[1,2,3]
- 输出
- 1(节点 1 坡度 |2-3|=1,两个叶子都是 0)
最优解:为什么这么做
一句话答案:LeetCode 563 二叉树的坡度:某节点坡度=它左右子树和之差的绝对值,答案是全部节点坡度之和。一遍后序 DFS 返回子树和,坡度顺路在每个节点累加,时间 O(n)。
节点坡度和全树坡度,到底在算什么
给一棵二叉树 root。一个节点的坡度,是它左子树所有节点之和、减去右子树所有节点之和,再取绝对值;哪边没有孩子,那边的和按 0 算,空节点坡度是 0。整棵树的坡度,是把每个节点的坡度全加起来。题面例子 root=[1,2,3],根 1、两个孩子 2 和 3 都是叶子:叶子没有子树,坡度都是 0;根的左子树只有 2、和为 2,右子树只有 3、和为 3,坡度 |2-3|=1。三个相加,答案就是 1。
对着每个节点分别求两次子树和,慢在哪
照着定义直接做:遍历每个节点,站在它这里把左子树求一遍和、右子树求一遍和,相减取绝对值。麻烦在于『求某棵子树的和』就得把那棵子树整个走一遍,每个节点都来一次,越靠上的节点被反复累加、重复得越狠。n 个节点最坏要 O(n²),同一批数白白加了许多遍。
一趟后序遍历,怎么把子树和一路带上来
重复求和之所以发生,是因为父节点要的『左右子树和』在处理孩子时就能得到,不必回头重算。把顺序倒过来:先把左右孩子处理完、拿到各自的子树和,再处理当前节点——这正是后序遍历,孩子先把子树和交上来,父节点才开始结算。一个节点的子树和,等于自己的值加左子树和、再加右子树和;孩子把子树和交上来,父节点一个加法就得到。
递归返回子树和,坡度另存一个全局答案累加
写一个 dfs:走到空节点返回 0。否则先递归左孩子拿到左子树和 l、再递归右孩子拿到右子树和 r。此刻两个子树和都齐了,本节点坡度就是 l 和 r 之差的绝对值 abs(l-r),加进全局答案 ans;然后 dfs 返回本节点的子树和 l+r+根值,交给上一层的父节点。
这里的分工要拎清:dfs 向上返回的必须是子树和,不是坡度。父节点算自己的和与坡度,都只认孩子的子树和,用不到孩子的坡度;坡度是每个节点结算后直接累进 ans 的副产品,从不往上传。dfs 若返回坡度,父节点收到的数全错,整棵树跟着崩。
root=[1,2,3] 后序走一遍,ans 怎么变成 1
这棵树三个节点:根 1,左孩子 2,右孩子 3,两个孩子都是叶子。后序先走到最左的叶子 2:左右都是空,l=0、r=0,坡度 |0-0|=0,ans 还是 0,往上交的子树和 0+0+2=2。再处理右孩子叶子 3:同样左右为空,坡度 |0-0|=0,ans 仍是 0,子树和 0+0+3=3。
两个孩子都结算完,轮到根 1:左子树和 l=2、右子树和 r=3 都到手,坡度 |2-3|=1,加进 ans,ans 从 0 变成 1;根往上交的子树和 2+3+1=6,但根没有父亲,6 用不上。全树走完,ans=1,就是答案。
返回错了东西,整棵树的答案跟着废
复杂度上:全树只做一遍后序遍历,每个节点结算一次,求和与算坡度都是常数操作,时间 O(n);额外空间只有递归栈,深度等于树高 h,平衡树 O(log n),链状树最坏 O(n)。
dfs 返回坡度而不是子树和,父节点拿到的数从源头就错;改用前序、孩子没算完就结算父亲,左右子树和根本没到手;坡度忘了取绝对值,左小右大算出负数,把总和拉小。边界反倒省心:空树和单节点树坡度都是 0,没有孩子可比。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这条主线:DFS 返回的是「子树和」,坡度是顺路在每个节点结算时累加的副产品。
- 4栈空,累计坡度 0开局累计坡度是 0。后序 DFS 从根 50 出发,但根的坡度现在算不了,得先知道左右子树各自的和。所以先一路往左下钻,钻到最左下的叶子,再自底向上一个个结算。盯住右边的坡度结算面板,每结算一个内部节点,累计坡度就涨一截。
- 5进入 50递归进入节点 50(紫色)。它还有孩子,左右子树和都不知道,坡度先算不了,所以继续往下钻,先把左孩子那一支彻底搞定。
- 6进入 20递归进入节点 20(紫色)。它还有孩子,左右子树和都不知道,坡度先算不了,所以继续往下钻,先把左孩子那一支彻底搞定。
- 7进入 30递归进入节点 30(紫色)。它还有孩子,左右子树和都不知道,坡度先算不了,所以继续往下钻,先把左孩子那一支彻底搞定。
- 8叶子 80递归走到节点 80(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
- 9子树和 = 80结算叶子 80:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 80。节点变绿表示处理完。
- 10叶子 10递归走到节点 10(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
- 11子树和 = 10结算叶子 10:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 10。节点变绿表示处理完。
- 12坡度 = |80-10| = 70节点 30 的左右子树和都齐了:左边 80,右边 10。本节点坡度 = |80-10| = 70,累加进总答案,累计坡度涨到 70。它往上交的子树和 = 30+80+10 = 120。节点变绿。
- 13叶子 90递归走到节点 90(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
- 14子树和 = 90结算叶子 90:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 90。节点变绿表示处理完。
- 15坡度 = |120-90| = 30节点 20 的左右子树和都齐了:左边 120,右边 90。本节点坡度 = |120-90| = 30,累加进总答案,累计坡度涨到 100。它往上交的子树和 = 20+120+90 = 230。节点变绿。
- 16进入 70递归进入节点 70(紫色)。它还有孩子,左右子树和都不知道,坡度先算不了,所以继续往下钻,先把左孩子那一支彻底搞定。
- 17叶子 60递归走到节点 60(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
- 18子树和 = 60结算叶子 60:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 60。节点变绿表示处理完。
- 19叶子 40递归走到节点 40(紫色),它没有孩子,是叶子。叶子左右两边都是空,子树和都按 0 算,下一帧直接结算。
- 20子树和 = 40结算叶子 40:左右都是空,坡度 = |0-0| = 0,对总答案没有贡献。它要往上交的「子树和」就是它自己 40。节点变绿表示处理完。
- 21坡度 = |60-40| = 20节点 70 的左右子树和都齐了:左边 60,右边 40。本节点坡度 = |60-40| = 20,累加进总答案,累计坡度涨到 120。它往上交的子树和 = 70+60+40 = 170。节点变绿。
- 22坡度 = |230-170| = 60节点 50 的左右子树和都齐了:左边 230,右边 170。本节点坡度 = |230-170| = 60,累加进总答案,累计坡度涨到 180。它往上交的子树和 = 50+230+170 = 450。节点变绿。
- 2370 + 30 + 20 + 60九个节点全部结算完。看右边的坡度面板:五个叶子坡度都是 0,对总和没贡献;真正出力的是四个内部节点,坡度分别是节点 30 的 70、节点 20 的 30、节点 70 的 20、根 50 的 60。把它们相加就是最终答案。
- 24答案 = 18070 加 30 加 20 加 60,正好等于 180,这就是整棵树的坡度。回头看,整个过程只走了一遍后序遍历,子树和一路往上递,坡度顺路累加,一次搞定,时间是 O(n)。
⚠️ 容易写错的地方
✗ 错:让 dfs 返回坡度
✓ 对:dfs 必须返回「子树和」,坡度用全局变量累加
父节点要的是孩子的子树和,不是孩子的坡度,返回错东西全盘崩
✗ 错:用前序去算
✓ 对:必须后序:先有左右子树和才能算父亲
父节点的和与坡度都依赖孩子,孩子没算完父亲就是错的
✗ 错:坡度忘了取绝对值
✓ 对:坡度 = |左 - 右|
题目明确是差的绝对值,漏了绝对值负数会把总和算小
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def findTilt(self, root: Optional[TreeNode]) -> int:
def dfs(root: Optional[TreeNode]) -> int:
if root is None:
return 0
l, r = dfs(root.left), dfs(root.right)
nonlocal ans
ans += abs(l - r)
return l + r + root.val
ans = 0
dfs(root)
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int findTilt(TreeNode* root) {
int ans = 0;
function<int(TreeNode*)> dfs = [&]( TreeNode* root ) -> int {
if (!root) {
return 0;
}
int l = dfs(root->left), r = dfs(root->right);
ans += abs(l - r);
return l + r + root->val;
};
dfs(root);
return ans;
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private int ans;
public int findTilt(TreeNode root) {
dfs(root);
return ans;
}
private int dfs(TreeNode root) {
if (root == null) {
return 0;
}
int l = dfs(root.left), r = dfs(root.right);
ans += Math.abs(l - r);
return l + r + root.val;
}
}复杂度
时间
O(n)
后序遍历每个节点只被访问、结算一次,算和与算坡度都是常数操作
空间
O(h)
只用递归栈,深度等于树高 h;最坏(链状树)退化到 O(n),平衡树则是 O(log n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 二叉树的坡度 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么必须用后序,前序一遍不能把坡度都算出来吗?+
一个节点的坡度依赖它左右子树的和,而子树和又依赖更深处的节点,得先把孩子全部算完才轮到父亲。后序正是先左、右、再根这个顺序,轮到父节点时两个子树和都已备好。前序先碰父亲、那时孩子还没算,左右子树和拿不到,自然算不出坡度。
dfs 到底该返回坡度还是子树和,我总记混。+
返回子树和,坡度另用一个全局变量累加。判断标准很简单:看父节点向孩子要什么。父节点算自己的和与坡度,只需要孩子的子树和,从不需要孩子的坡度。所以往上传的是子树和;坡度是每个节点当场结算、直接加进全局答案的副产品,传上去反而没人用、还会算错。
树很深时递归会不会爆栈,怎么办?+
链状树的深度能到 n,递归栈会压得很深,确实可能爆。可以改成用显式栈模拟的迭代后序遍历,把系统栈换成自己管理的栈;或者给节点带上父指针,记录每个节点已经算好的子树和,自底向上逐个结算。面试时能说清后序的本质是先子后父、并给出一种迭代替代,就够了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 二叉树的坡度 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。