出现次数最多的子树元素和 图解题解
这道题到底在问什么
- 输入
- root=[5,2,-3]
- 输出
- [2,-3,4](三个和各出现 1 次,全是最多)
最优解:为什么这么做
一句话答案:LeetCode 508 出现次数最多的子树元素和:父节点的和依赖孩子,用后序遍历给每个节点算出子树和、边算边用哈希表计数,最后挑出现次数最多的那些和返回,时间 O(n)。
每个子树都有一个和,要数哪个和最常见
给一棵二叉树,每个节点都能框出一棵以它为根的子树;把这棵子树里所有节点值加起来,就是这个节点的子树和。叶子也算一棵子树,它的和就是自身那个值。全树 n 个节点凑出 n 个子树和,其中难免有重复。题目问的是:哪些子树和出现的次数最多,把它们全都返回,顺序不限。题面例子 root=[5,2,-3],三个节点各给一个和,答案是 [2,-3,4]。
对每个节点各扫一遍子树求和,n 大了就吃力
求一个节点的子树和,得把它底下整棵子树都加一遍。可要是每个节点都各自这么加一次,靠上的节点会重复走下面一大片:根节点扫全树、根的两个孩子又各扫自己那半,同一个叶子被反复加进它每一个祖先的和里。n 个节点最坏叠到 O(n²) 量级的重复遍历,节点一多就慢得明显。
父亲的和等着孩子先算出来
重复全出在一件事上:父节点的和,等于它自己的值,加左子树的和,再加右子树的和。也就是说孩子的子树和只要算一次,父亲拿来直接用就行,用不着重新下去扫一遍。这就要求处理顺序是先孩子后父亲——两个孩子的和都递归求出来了,父节点才拿着它们把自己这棵子树的和拼起来,这种走法叫后序遍历。每个节点只在轮到自己时被加一次,重复就没了。
递归返回子树和,哈希表边算边记一笔
让递归函数 dfs 干两件事:算出当前节点的子树和 s = 节点值 + dfs(左) + dfs(右),并把 s 返回给父亲;同时在一张按值计数的哈希表 cnt 里给 s 记一笔,也就是 cnt[s] 加一。空节点返回 0,正好当作递归到底的边界。
整棵树跑完,cnt 里存的就是每个子树和各出现了几次。取其中最大的次数 best,再把所有出现次数正好等于 best 的和收集起来,就是答案。这里要当心可能有好几个和并列 best,得一个不落全返回。
拿题面的 root=[5,2,-3] 从叶子算到根
这棵树三个节点:根 5,左孩子 2,右孩子 -3,两个孩子都是叶子。后序先下到左孩子 2,它没有孩子,dfs(左)、dfs(右) 都返回 0,子树和 s = 2 + 0 + 0 = 2,cnt[2] 记 1。再到右孩子 -3,同样是叶子,s = -3,cnt[-3] 记 1。最后回到根 5,它的和 s = 5 + 2 + (-3) = 4,cnt[4] 记 1。三个和 2、-3、4 各出现 1 次,最大次数 best = 1,于是三个都是答案,顺序不限,题面给的就是 [2,-3,4]。
写成前序就全错,还有两个必踩的坑
最致命的是遍历顺序:把后序写成前序或层序,父亲还没拿到孩子的和就抢先结算,得到的全是错值。另一个坑是只返回出现次数最多的那一个和——本题并列第一很常见,漏掉几个直接判错。还有别忘了叶子也是一棵子树,它的和就是自身,同样要记进 cnt,少记这一笔次数就可能数偏。
复杂度上,后序把每个节点访问、结算恰好一次,记哈希和最后扫表都是均摊 O(1),时间 O(n)。空间看两处:哈希表最多存 n 个不同的和,递归栈深等于树高,链状树最坏也到 O(n)。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住这条主线:先下钻到叶子,再自底向上结算每个子树和,边算边往哈希表记一笔。
- 4栈空,哈希表空开局哈希表是空的。后序 DFS 从根节点 50 出发,先一路下钻到最左下的叶子,再自底向上一个个结算子树和。盯住右边这张计数表,它会随着结算逐行长出来。
- 5进入 50递归进入节点 50(紫色)。它还有孩子,子树和现在算不了,得先把左右孩子的子树和都求出来,所以继续往下钻。
- 6进入 20递归进入节点 20(紫色)。它还有孩子,子树和现在算不了,得先把左右孩子的子树和都求出来,所以继续往下钻。
- 7进入 30递归进入节点 30(紫色)。它还有孩子,子树和现在算不了,得先把左右孩子的子树和都求出来,所以继续往下钻。
- 8叶子 10递归走到节点 10(紫色),它没有孩子,是叶子。叶子的子树就是它自己一个人,下一帧直接结算。
- 9子树和 = 10节点 10 的左右子树和都齐了,结算:子树和 = 10。把 10 记进哈希表,它是第一次出现,计数记 1。节点变绿表示已经处理完。
- 10叶子 40递归走到节点 40(紫色),它没有孩子,是叶子。叶子的子树就是它自己一个人,下一帧直接结算。
- 11子树和 = 40节点 40 的左右子树和都齐了,结算:子树和 = 40。把 40 记进哈希表,它是第一次出现,计数记 1。节点变绿表示已经处理完。
- 12子树和 = 30 + 10 + 40 = 80节点 30 的左右子树和都齐了,结算:子树和 = 30 + 10 + 40 = 80。把 80 记进哈希表,它是第一次出现,计数记 1。节点变绿表示已经处理完。
- 13叶子 -10递归走到节点 -10(紫色),它没有孩子,是叶子。叶子的子树就是它自己一个人,下一帧直接结算。
- 14子树和 = (-10)节点 -10 的左右子树和都齐了,结算:子树和 = (-10)。把 -10 记进哈希表,它是第一次出现,计数记 1。节点变绿表示已经处理完。
- 15子树和 = 20 + 80 + (-10) = 90节点 20 的左右子树和都齐了,结算:子树和 = 20 + 80 + (-10) = 90。把 90 记进哈希表,它是第一次出现,计数记 1。节点变绿表示已经处理完。
- 16进入 80递归进入节点 80(紫色)。它还有孩子,子树和现在算不了,得先把左右孩子的子树和都求出来,所以继续往下钻。
- 17叶子 40递归走到节点 40(紫色),它没有孩子,是叶子。叶子的子树就是它自己一个人,下一帧直接结算。
- 18子树和 = 40节点 40 的左右子树和都齐了,结算:子树和 = 40。把 40 记进哈希表,这个和之前出现过,计数涨到 2。节点变绿表示已经处理完。
- 19叶子 10递归走到节点 10(紫色),它没有孩子,是叶子。叶子的子树就是它自己一个人,下一帧直接结算。
- 20子树和 = 10节点 10 的左右子树和都齐了,结算:子树和 = 10。把 10 记进哈希表,这个和之前出现过,计数涨到 2。节点变绿表示已经处理完。
- 21子树和 = 80 + 40 + 10 = 130节点 80 的左右子树和都齐了,结算:子树和 = 80 + 40 + 10 = 130。把 130 记进哈希表,它是第一次出现,计数记 1。节点变绿表示已经处理完。
- 22子树和 = 50 + 90 + 130 = 270节点 50 的左右子树和都齐了,结算:子树和 = 50 + 90 + 130 = 270。把 270 记进哈希表,它是第一次出现,计数记 1。节点变绿表示已经处理完。
- 23最大次数 = 2九个节点全部结算完,计数表也满了。现在扫一遍这张表,找出现次数最大的那个。可以看到 10 和 40 都出现了 2 次,其余的和都只出现 1 次,所以最大次数是 2。
- 24答案 = [10, 40]次数等于 2 的和有两个:10 和 40,它俩并列最多。题目要求把出现次数最多的和全部返回,所以答案是 [10, 40]。这也提醒我们:最多的可能不止一个,别只返回一个就交卷。
⚠️ 容易写错的地方
✗ 错:用前序/层序去算子树和
✓ 对:必须后序:先有左右子树和才能算父亲
父节点的和依赖孩子,孩子没算完父亲就是错的
✗ 错:只返回出现次数最多的一个和
✓ 对:并列最多的要全部返回
本题可能有多个和并列第一,漏返回会判错
✗ 错:忘了叶子也要计数
✓ 对:每个节点(含叶子)的子树和都要记一笔
叶子的子树和就是它本身,同样参与统计
完整代码(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 findFrequentTreeSum(self, root: Optional[TreeNode]) -> List[int]:
cnt = Counter()
def dfs(node):
if not node:
return 0
s = node.val + dfs(node.left) + dfs(node.right)
cnt[s] += 1
return s
dfs(root)
if not cnt:
return []
best = max(cnt.values())
return sorted([s for s, c in cnt.items() if c == best])C++
#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) {}
};
class Solution {
public:
vector<int> findFrequentTreeSum(TreeNode* root) {
unordered_map<int, int> cnt;
function<int(TreeNode*)> dfs = [&](TreeNode* node) {
if (!node) return 0;
int s = node->val + dfs(node->left) + dfs(node->right);
++cnt[s];
return s;
};
dfs(root);
int best = 0;
for (auto& [_, c] : cnt) best = max(best, c);
vector<int> ans;
for (auto& [s, c] : cnt) if (c == best) ans.push_back(s);
sort(ans.begin(), ans.end());
return ans;
}
};Java
import java.util.*;
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 Map<Integer, Integer> cnt;
public int[] findFrequentTreeSum(TreeNode root) {
cnt = new HashMap<>();
dfs(root);
int best = 0;
for (int c : cnt.values()) best = Math.max(best, c);
ArrayList<Integer> ans = new ArrayList<>();
for (Map.Entry<Integer, Integer> e : cnt.entrySet()) if (e.getValue() == best) ans.add(e.getKey());
Collections.sort(ans);
int[] out = new int[ans.size()];
for (int i = 0; i < ans.size(); i++) out[i] = ans.get(i);
return out;
}
private int dfs(TreeNode node) {
if (node == null) return 0;
int s = node.val + dfs(node.left) + dfs(node.right);
cnt.put(s, cnt.getOrDefault(s, 0) + 1);
return s;
}
}复杂度
时间
O(n)
每个节点只在后序里被访问、结算一次,哈希表的记一笔和最后扫表都是均摊 O(1)
空间
O(n)
哈希表最多存 n 个不同的子树和;递归栈深为树高 h,最坏(链状树)也是 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 出现次数最多的子树元素和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
树很深时递归会不会爆栈?+
会。链状的极端树深度能到 n,递归一层层往下压栈,栈就很深。想避开可以改成用一个显式栈手动模拟的迭代后序遍历,或者给节点带上父指针、自底向上结算,都不依赖系统的调用栈。面试时能讲清后序的本质是先子后父、并说出一个迭代替代方案,就够了。
能不能一遍就拿到答案,不用最后再扫一次哈希表?+
能。在 dfs 结算每个和、更新 cnt 的同时,维护一个全局最大次数 best 和对应的和集合:某个和的新次数超过 best,就清空集合、把 best 更新成它;正好等于 best,就把这个和加进集合。遍历结束,集合里就是答案,省掉末尾再扫一遍哈希表那一步。
为什么一定要后序,中序或前序不行吗?+
因为父节点的子树和依赖两个孩子的和,孩子没算完,父亲就算不对。前序是先结算父亲再下去处理孩子,轮到父亲时它要的孩子和还是空的;中序在处理完左孩子、还没碰右孩子时就结算父亲,右边那半又漏了。只有后序保证左右孩子都返回了和,父亲才动手,顺序对了值才对。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 出现次数最多的子树元素和 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。