统计值等于子树平均值的节点数 图解题解
这道题到底在问什么
- 输入
- root=[54,40,88,30,51,25,95]
- 输出
- 6
- 输入
- root=[1]
- 输出
- 1
最优解:一步一步想明白
- 3记牢这一句:先递归到最底下,再一层层往上把和与个数带回来,在每个节点顺手算一次平均、比一次值。下面从根出发,一路下探到叶子,再自底向上回算。
- 4从根出发向下递归先看清这棵树的形状。后序遍历的规矩是:不急着在半路结算,而是一头扎到最底下的叶子,从叶子开始把子树的和与个数一层层往上抬。你盯住紫色,它代表当前正在处理的节点;绿色代表已经验过、达标的节点;红色代表验过但不达标的节点。现在从根出发往下走。
- 5空树: sum = 0, n = 0正式下探前先说好一个约定:走到空位置,也就是没有节点的地方,直接返回和 0、个数 0,它什么都不贡献。有了这条,叶子的左右孩子都是空,回来都是 0 加 0,叶子的子树和就只剩它自己,个数就是 1,天然成立。这条约定是整个递归能自底向上算对的地基。
- 6先递归它的左子树走到值 54 的节点,它下面还挂着孩子,现在还不能结算。按后序的规矩,得先把它的左子树整个算完、右子树整个算完,拿到两边的和与个数,才轮到它自己。先钻左子树。
- 7先递归它的左子树走到值 40 的节点,它下面还挂着孩子,现在还不能结算。按后序的规矩,得先把它的左子树整个算完、右子树整个算完,拿到两边的和与个数,才轮到它自己。先钻左子树。
- 8值 30 是叶子走到值 30 的节点,它没有孩子,是个叶子。叶子这一步最省事:它的子树就它自己一个,和就是 30,个数是 1。下一帧直接给它结算。
- 9平均 = 30,达标结算值 30 的这个叶子。子树只有它自己,和是 30,个数是 1,平均就是 30 除以 1 等于 30。这个平均正好等于节点自己的值 30,达标,把它标绿,达标节点数变成 1。它向上返回的就是一对数字:和 30、个数 1。
- 10左子树算完,再递归右子树值 40 的左子树整个算完了,左边带回来的是:和 30、个数 1。但现在还不能拍板,右子树还没碰。回到值 40,接着钻它的右子树,把右边那对数字也拿回来。
- 11值 51 是叶子走到值 51 的节点,它没有孩子,是个叶子。叶子这一步最省事:它的子树就它自己一个,和就是 51,个数是 1。下一帧直接给它结算。
- 12平均 = 51,达标结算值 51 的这个叶子。子树只有它自己,和是 51,个数是 1,平均就是 51 除以 1 等于 51。这个平均正好等于节点自己的值 51,达标,把它标绿,达标节点数变成 2。它向上返回的就是一对数字:和 51、个数 1。
- 13和 121 · 个数 3左右子树都回来了。值 40 把两边的和加上自己的值:30 加 51 加 40,等于 121;个数是左边 1 加右边 1 再加自己这一个,等于 3。这就是值 40 整棵子树的和与个数,下一帧拿它算平均。
- 14平均 = 40,达标给值 40 结算。平均值等于和 121 除以个数 3。算出来是 40.33,向下取整拿 40。这个 40 恰好等于节点自己的值 40,达标,标绿,达标节点数变成 3。不管达不达标,它都要照常把和 121、个数 3 向上返回给父节点。
- 15左子树算完,再递归右子树值 54 的左子树整个算完了,左边带回来的是:和 121、个数 3。但现在还不能拍板,右子树还没碰。回到值 54,接着钻它的右子树,把右边那对数字也拿回来。
- 16先递归它的左子树走到值 88 的节点,它下面还挂着孩子,现在还不能结算。按后序的规矩,得先把它的左子树整个算完、右子树整个算完,拿到两边的和与个数,才轮到它自己。先钻左子树。
- 17值 25 是叶子走到值 25 的节点,它没有孩子,是个叶子。叶子这一步最省事:它的子树就它自己一个,和就是 25,个数是 1。下一帧直接给它结算。
- 18平均 = 25,达标结算值 25 的这个叶子。子树只有它自己,和是 25,个数是 1,平均就是 25 除以 1 等于 25。这个平均正好等于节点自己的值 25,达标,把它标绿,达标节点数变成 4。它向上返回的就是一对数字:和 25、个数 1。
- 19左子树算完,再递归右子树值 88 的左子树整个算完了,左边带回来的是:和 25、个数 1。但现在还不能拍板,右子树还没碰。回到值 88,接着钻它的右子树,把右边那对数字也拿回来。
- 20值 95 是叶子走到值 95 的节点,它没有孩子,是个叶子。叶子这一步最省事:它的子树就它自己一个,和就是 95,个数是 1。下一帧直接给它结算。
- 21平均 = 95,达标结算值 95 的这个叶子。子树只有它自己,和是 95,个数是 1,平均就是 95 除以 1 等于 95。这个平均正好等于节点自己的值 95,达标,把它标绿,达标节点数变成 5。它向上返回的就是一对数字:和 95、个数 1。
- 22和 208 · 个数 3左右子树都回来了。值 88 把两边的和加上自己的值:25 加 95 加 88,等于 208;个数是左边 1 加右边 1 再加自己这一个,等于 3。这就是值 88 整棵子树的和与个数,下一帧拿它算平均。
- 23平均 = 69,不达标给值 88 结算。平均值等于和 208 除以个数 3。算出来是 69.33,向下取整拿 69。这个 69 并不等于节点自己的值 88,不达标,标红,达标节点数不加,仍是 5。不管达不达标,它都要照常把和 208、个数 3 向上返回给父节点。
- 24和 383 · 个数 7左右子树都回来了。值 54 把两边的和加上自己的值:121 加 208 加 54,等于 383;个数是左边 3 加右边 3 再加自己这一个,等于 7。这就是值 54 整棵子树的和与个数,下一帧拿它算平均。
- 25平均 = 54,达标给值 54 结算。平均值等于和 383 除以个数 7。算出来是 54.71,向下取整拿 54。这个 54 恰好等于节点自己的值 54,达标,标绿,达标节点数变成 6。不管达不达标,它都要照常把和 383、个数 7 向上返回给父节点。
- 26答案 = 6整棵树验完了。四个叶子 30、51、25、95 各自子树只有自己,平均等于自己,全达标;值 40 的子树平均 40、值 54 的子树平均 54,也都对得上,达标;唯独值 88 的子树平均是 69,不等于 88,不达标,标红。数一数绿色节点,一共 6 个,这就是答案。判定始终只做一件事:在每个节点算子树平均、和它自己的值比一比。
⚠️ 容易写错的地方
✗ 错:每个节点都重新遍历一遍自己的子树来求和
✓ 对:后序一次遍历,自底向上把(和,个数)带回来复用
给每个节点单独遍历子树是 O(n方);后序让子树结果只算一次、逐层复用,才是 O(n)
✗ 错:平均值用浮点数算再比较
✓ 对:按题意用整数除法向下取整,再和节点值比
题目明确平均要向下舍入到整数,浮点会带来精度误差,直接整数除法既准又快
✗ 错:只返回和,不返回个数(或反过来)
✓ 对:必须同时把和与个数两个数一起向上返回
算平均既要总和又要节点个数,少一个父节点就没法在自己这一层继续累加
✗ 错:把空节点当成个数 1 或值 0 参与平均
✓ 对:空节点返回和 0、个数 0,完全不参与
空位置不是节点,若算进个数会把分母撑大、平均算错
完整代码(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 *
from string import *
from operator 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
class Solution:
def averageOfSubtree(self, root: TreeNode) -> int:
def dfs(root) -> tuple:
if not root:
return 0, 0
ls, ln = dfs(root.left)
rs, rn = dfs(root.right)
s = ls + rs + root.val
n = ln + rn + 1
nonlocal ans
ans += int(s // n == root.val)
return s, n
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 averageOfSubtree(TreeNode* root) {
int ans = 0;
function<pair<int, int>(TreeNode*)> dfs = [&]( TreeNode* root ) -> pair<int, int> {
if (!root) {
return {0, 0};
}
auto [ls, ln] = dfs(root->left);
auto [rs, rn] = dfs(root->right);
int s = ls + rs + root->val;
int n = ln + rn + 1;
if (s / n == root->val) {
++ans;
}
return {s, n};
};
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 averageOfSubtree(TreeNode root) {
ans = 0;
dfs(root);
return ans;
}
private int[] dfs(TreeNode root) {
if (root == null) {
return new int[2];
}
int[] l = dfs(root.left);
int[] r = dfs(root.right);
int s = l[0] + r[0] + root.val;
int n = l[1] + r[1] + 1;
if (s / n == root.val) {
++ans;
}
return new int[] {s, n};
}
}复杂度
时间
O(n)
n 是节点总数。每个节点被后序 DFS 恰好访问一次,在节点处做的加法、一次除法、一次比较都是常数操作,总量随节点数线性增长
空间
O(h)
按峰值算,h 是树高。不额外开表,只有递归栈,栈深最多等于从根到最深叶子的层数 h。最坏是退化成链的树,h 达到 n;平衡树时 h 约为 log n
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 统计值等于子树平均值的节点数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题的核心思路一句话怎么说?+
一次后序遍历,每个节点向上返回它子树的元素和与节点个数;在节点处用和除以个数向下取整得到平均,若等于节点自己的值就把答案加一。自底向上,子树结果只算一次、逐层复用,整体线性。
为什么不能自顶向下做?+
自顶向下时,父节点还不知道子树的总和与个数,得先把子树整个遍历一遍才能求平均,这样每个节点都重复遍历自己的子树,退化成 O(n方)。后序自底向上让每棵子树的和与个数只在回溯时算一次,父节点直接拿现成结果拼接,才有 O(n)。
为什么时间是 O(n),空间是多少?+
每个节点在后序遍历中只被处理一次,节点内做常数次加法、一次除法、一次比较,所以时间 O(n)。空间按峰值算只有递归栈,栈深等于树高 h,平衡树约 log n,最坏退化成链时是 O(n)。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 统计值等于子树平均值的节点数 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。