二叉树的堂兄弟节点 II 图解题解
这道题到底在问什么
- 输入
- root=[5,4,9,1,10,null,7]
- 输出
- [0,0,0,7,7,null,11]
- 输入
- root=[3,1,2]
- 输出
- [0,0,0]
最优解:为什么这么做
一句话答案:LeetCode 2641 二叉树的堂兄弟节点 II:把每个节点的值换成它所有堂兄弟之和,两遍遍历——先按层求总和,再自上而下用本层总和减掉同父那一组,时间 O(n)。
把每个节点换成堂兄弟之和,到底换成几
给一棵二叉树 root,把每个节点的值改成它所有堂兄弟节点值的和。两节点同在一层、可父节点不同,就互为堂兄弟;深度是从根往下数的边数。题面例子 root=[5,4,9,1,10,null,7],节点 1 的堂兄弟只有 7 换成 7,节点 7 的堂兄弟是 1 和 10 换成 11,根 5 独占一层没堂兄弟换成 0,整棵树变成 [0,0,0,7,7,null,11]。
照定义挨个找同层伙伴,慢在哪
照字面直译,得给每个节点算出它在第几层、父亲是谁,再把同层里父亲不同的挑出来相加。每个节点都扫一遍同层,重复的定位比较堆起来,轻松滑到 O(n²)。别扭的是『父亲不同』这个条件:同层里既混着堂兄弟又混着亲兄弟,得先把亲兄弟摘干净才行。
堂兄弟和,其实是本层总和减掉亲兄弟
一个节点的堂兄弟,是同层里所有节点除掉它和它的亲兄弟,亲兄弟就是跟它同一个父亲的那些。所以堂兄弟之和 = 本层所有节点之和 − 同父那一组之和。这样就不用挨个找谁是堂兄弟,只要备好每层总和、每个父亲名下孩子的和这两样。两趟遍历——先求层总和,再自上而下发放。
两趟怎么分工,为什么读到的是原值
第一趟 dfs1 顺着树往下走,也就是顺一条路走到底再回头,带个 depth 记当前第几层,把每个节点值累加进数组 s,走完 s[depth] 就是第 depth 层总和。第二趟 dfs2 也自上而下,对每个父亲:先算它左右孩子的值之和 sub(孩子为空按 0),孩子在 depth+1 层,把每个孩子改成 s[depth+1] − sub。
根在第 0 层独一个,没有同层伙伴,堂兄弟和是 0,发放前得手动置 0。还有顺序的巧劲:dfs2 自上而下,轮到某个父亲时孩子还没被碰过,sub 读到的正好是原始值,不会被改写值污染。
拿 [5,4,9,1,10,null,7] 把两趟都走一遍
第一趟求层总和。第 0 层只有 5,s[0]=5;第 1 层是 4 和 9,s[1]=13;第 2 层是 1、10、7,s[2]=18。
第二趟发放。先把根 5 置 0。根的两个孩子 4、9 同父,sub=4+9=13,s[1]−sub=13−13=0,4、9 都变 0。原来是 4 的父亲名下 1 和 10,sub=1+10=11,在第 2 层,s[2]−sub=18−11=7,1、10 都变 7。原来是 9 的父亲只有右孩子 7,左边空按 0,sub=0+7=7,s[2]−sub=18−7=11,7 变 11。整棵树成了 [0,0,0,7,7,null,11]。
减法要对准同父那一组,别在这些地方栽
最常见的是把堂兄弟和当成『本层总和减掉自己』,漏了亲兄弟——同父的亲兄弟也在同一层,不减掉就被错当成堂兄弟多算一份。根忘了单独置 0,会留着原值,第 0 层就错。算同父那组和时孩子若是空的,得按 0 计入,否则减法对不上整层总和。发放顺序也别反,自上而下父亲读到的孩子才是原值。
复杂度上,两趟各把每个节点访问一次,每次只做几步加减,时间 O(n);数组 s 长度和递归栈深都等于树高 h,最坏退化成链时 h 到 n,空间 O(n)。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:堂兄弟和 = 本层总和 减去 同父那一组的和。下面先把三层的总和算出来,再自上而下一个父亲一个父亲地发放。
- 4原始输入这是初始的树,层序是 5,4,9,1,10,空,7。根 5 在第 0 层,4 和 9 在第 1 层,1、10、7 在第 2 层。我们的目标,是把每个节点的值,换成它所有堂兄弟节点的值之和。先把「谁和谁是堂兄弟」看清楚。
- 51 和 10 是亲兄弟 · 7 是它们的堂兄弟看第 2 层这三个节点。1 和 10 的父亲都是 4,它俩是亲兄弟,不算堂兄弟。7 的父亲是 9,和 1、10 的父亲不一样。所以 7 与 1、10 才互为堂兄弟。抓住这个区别:同一层还不够,必须父亲不同。
- 6第 0 层只有根 5第一趟自上而下,把每一层的总和都求出来。第 0 层只有根节点 5,这一层的总和就是 5。记到右边面板里。
- 7第 1 层是 4 和 9第 1 层有 4 和 9 两个节点,它们相加是 13,这就是第 1 层的总和。注意这一层里 4 和 9 是亲兄弟,同一个父亲根 5 底下。
- 8第 2 层是 1, 10, 7第 2 层有 1、10、7 三个节点,相加是 18。三层的总和都算好了,分别是 5、13、18。第一趟到此结束,接下来第二趟开始发放。
- 94 底下是一组亲兄弟发放前先把「同父的组」看清楚。节点 4 底下的 1 和 10 是一组亲兄弟。等下算这两个孩子的堂兄弟和时,要从第 2 层总和里减掉的,正是这一组 1 加 10。
- 109 底下只有一个孩子节点 9 底下只有右孩子 7,左孩子是空的。所以 9 名下这一组就只有 7 自己。等下算 7 的堂兄弟和,要减掉的就是它自己这一格。
- 11根在第 0 层,孤零零一个第二趟自上而下发放。先看根 5。它在第 0 层,整层就它一个,没有任何同层的伙伴,自然也没有堂兄弟。所以它的堂兄弟和是 0。
- 12根 5 → 0把根改写成 0。注意这一改不影响后面的计算,因为算孩子时用的是孩子那一层的原始值,和根现在是几没关系。接着往下一层发放。
- 134 和 9 是同父亲(根)的一组看根的两个孩子 4 和 9。它俩同层,而且同一个父亲根,是一组亲兄弟。它们所在的第 1 层总和是 13。堂兄弟和,就要拿这个 13 去减掉它们这一组自己的和。
- 144 和 9 各自的堂兄弟和4 和 9 这一组自己的和是 4 加 9 等于 13,恰好等于整层总和 13。相减得到 0,说明它们在这一层里没有别的父亲的伙伴。所以 4 和 9 的堂兄弟和都是 0。
- 15第 1 层发放完毕把 4 和 9 都改写成 0。第 1 层发放完毕。继续往下,进入第 2 层,分别由 4 和 9 这两个父亲来发放。
- 161 和 10 是同父的一组轮到左边这个父亲,它原来的值是 4,现在已经是 0 了。它名下有 1 和 10 两个孩子,都在第 2 层。第 2 层总和是 18,接下来拿它减掉 1 和 10 这一组自己的和。
- 171 和 10 各自的堂兄弟和1 和 10 这一组自己的和是 1 加 10 等于 11。第 2 层总和 18 减去 11,得到 7。这个 7 就是它俩在第 2 层里、别的父亲那几个孩子的和,也就是它们唯一的堂兄弟 7。所以 1 和 10 都改成 7。
- 18左支的孩子发放完毕把 1 和 10 都改写成 7,和我们一开始说的答案对上了。左边这个父亲名下的孩子处理完,只剩右边父亲名下那个 7 了。
- 19左孩子是空的轮到右边这个父亲,它原来是 9,现在也是 0 了。它只有右孩子 7,左孩子是空的。算 7 的堂兄弟和时,要从第 2 层总和里减掉的,就是这个父亲名下的孩子之和,也就是 7 自己加上空的那半边算 0。
- 207 的堂兄弟和这个父亲名下的孩子之和是 0 加 7 等于 7。第 2 层总和 18 减去 7,得到 11。这个 11 正好是 7 在第 2 层里的两个堂兄弟 1 和 10 之和。所以 7 改成 11。
- 21所有节点发放完毕把 7 改写成 11。至此,除根之外每个节点都拿到了它的堂兄弟之和,整棵树发放完毕。下面回头验一遍。
- 22结果 = [0,0,0,7,7,null,11]最终这棵树的层序变成 0、0、0、7、7、空、11。和官方给的答案一模一样。回顾一下每一步都在做同一件事:本层总和,减去同父那一组的和。
- 23逐点检验堂兄弟关系逐个检验。节点 1 在第 2 层,同层里父亲不同的只有 7,所以它的堂兄弟和是 7,对得上。10 同理,堂兄弟也只有 7,改成 7。7 的堂兄弟是 1 和 10,和为 11,也对得上。根和第 1 层没有堂兄弟,全归 0。全部吻合。
⚠️ 容易写错的地方
✗ 错:把堂兄弟和算成「本层总和减去节点自身的值」
✓ 对:要减去它和它亲兄弟(同父那一组)之和
亲兄弟同层但同父,不是堂兄弟,必须一起排除;只减自己会把亲兄弟错当成堂兄弟算进去
✗ 错:忘了把根单独置 0
✓ 对:根在第 0 层只有它自己,堂兄弟和是 0
根没有同层伙伴,若不置 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 *
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 replaceValueInTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
def dfs1(root: Optional[TreeNode], depth: int):
if root is None:
return
if len(s) <= depth:
s.append(0)
s[depth] += root.val
dfs1(root.left, depth + 1)
dfs1(root.right, depth + 1)
def dfs2(root: Optional[TreeNode], depth: int):
sub = (root.left.val if root.left else 0) + (
root.right.val if root.right else 0
)
depth += 1
if root.left:
root.left.val = s[depth] - sub
dfs2(root.left, depth)
if root.right:
root.right.val = s[depth] - sub
dfs2(root.right, depth)
s = []
dfs1(root, 0)
root.val = 0
dfs2(root, 0)
return rootC++
#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:
TreeNode* replaceValueInTree(TreeNode* root) {
memset(s, 0, sizeof(s));
dfs1(root, 0);
root->val = 0;
dfs2(root, 0);
return root;
}
private:
int s[100010];
void dfs1(TreeNode* root, int depth) {
if (!root) {
return;
}
s[depth] += root->val;
dfs1(root->left, depth + 1);
dfs1(root->right, depth + 1);
};
void dfs2(TreeNode* root, int depth) {
int l = root->left ? root->left->val : 0;
int r = root->right ? root->right->val : 0;
int sub = l + r;
++depth;
if (root->left) {
root->left->val = s[depth] - sub;
dfs2(root->left, depth);
}
if (root->right) {
root->right->val = s[depth] - sub;
dfs2(root->right, depth);
}
};
};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 List<Integer> s = new ArrayList<>();
public TreeNode replaceValueInTree(TreeNode root) {
dfs1(root, 0);
root.val = 0;
dfs2(root, 0);
return root;
}
private void dfs1(TreeNode root, int depth) {
if (root == null) {
return;
}
if (s.size() <= depth) {
s.add(0);
}
s.set(depth, s.get(depth) + root.val);
dfs1(root.left, depth + 1);
dfs1(root.right, depth + 1);
}
private void dfs2(TreeNode root, int depth) {
int l = root.left == null ? 0 : root.left.val;
int r = root.right == null ? 0 : root.right.val;
int sub = l + r;
++depth;
if (root.left != null) {
root.left.val = s.get(depth) - sub;
dfs2(root.left, depth);
}
if (root.right != null) {
root.right.val = s.get(depth) - sub;
dfs2(root.right, depth);
}
}
}复杂度
时间
O(n)
n 是节点数。两趟遍历,每趟都恰好访问每个节点一次,每次只做常数次加减,总量随节点数线性增长
空间
O(h)
按峰值算。存每层总和的数组长度等于树高 h;递归栈深也是 h。二者都是 O(h),最坏树退化成一条链时 h 等于 n,即 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 二叉树的堂兄弟节点 II 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题的核心思路一句话说清是什么?+
两趟遍历。第一趟顺着树往下按层把每层节点值加起来,得到每一层的总和;第二趟自上而下,先把根置 0,再让每个父亲把它名下的孩子改成『孩子那一层的总和 减去 这个父亲名下孩子之和』。道理是:和某个孩子同层、又不同父的那些节点,正好就是它的堂兄弟,而它们的和等于整层总和扣掉同父这一组。
为什么减的是同父孩子之和,不是节点自己的值?+
堂兄弟的定义是同层且父亲不同。跟一个节点同层又同父的是它的亲兄弟,这些不算堂兄弟,必须一起排除。要是只减节点自己,它的亲兄弟就还留在和里,被当成堂兄弟多算了一份。所以得从整层总和里,把它连同亲兄弟这一整组都减掉,剩下的才是父亲不同的那部分,也就是真正的堂兄弟。
能不能只用一趟 BFS 完成?+
可以。用队列一层一层横着遍历,处理某一层时先顺手把下一层的总和累加出来,再回过头把这一层每个父亲的孩子按『下层总和 减 同父孩子和』改写好。这样一趟就够,时间同样 O(n),空间是一层最多多少个节点。两趟 DFS 写起来更直白,一趟 BFS 更省内存,看你更在意哪头。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 二叉树的堂兄弟节点 II 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。