LeetCode 865中等树 · BST
具有所有最深节点的最小子树 图解题解
这道题到底在问什么
每个节点的深度是它到根的距离。深度最大的节点叫最深节点(可能有多个)。请返回同时包含所有最深节点的那棵最小子树的根。子树 = 某节点加上它的全部后代。
- 输入
- root=[30,50,15,60,20,85,90,null,null,70,45]
- 输出
- 以 20 为根的子树 [20,70,45]
最优解:一步一步想明白
- 3记牢这句口诀:左右等深当前为答案,否则跟更深那一侧。下面每一帧都在套它。
- 4根=30,目标找最小子树这是一棵 9 个节点的二叉树,根是 30。我们要找包含全部最深节点的最小子树。最底层的 70 和 45 在深度 3(到根 3 条边),是全树最深的两个节点。直觉上,能同时罩住它们的最小子树,根就是它们的最近公共祖先。下面用一次后序 DFS 把这个结论严格算出来。
- 570 与 45 深度最大先把目标点亮:70 和 45 都在第 4 层(根算第 1 层,到根 3 条边、深度 3),是全树最深的节点。我们要的子树必须同时包含这两个,而且尽量小。带着这个目标开始后序遍历,注意我们全程不直接去测深度,而是靠每个节点比较左右子树的高度来推断。
- 6展开 30 的子树下行进入节点 30。它还有孩子,现在还不能结算,得先把左右两棵子树的高度都递归算出来,再回头处理 30。先去它的左孩子。
- 7展开 50 的子树下行进入节点 50。它还有孩子,现在还不能结算,得先把左右两棵子树的高度都递归算出来,再回头处理 50。先去它的左孩子。
- 8叶子 60沿当前路径下行,来到节点 60。它没有左右孩子,是一个叶子。叶子下面是两个空,高度都按 0 算,马上结算它。
- 960 高度=1结算叶子 60:左右都是空,两侧高度都是 0,正好相等。按口诀“等深则当前为答案”,60 自己就是这棵单节点子树里所有最深节点的最小子树。返回一对值:高度 1,候选根 60。
- 10展开 20 的子树下行进入节点 20。它还有孩子,现在还不能结算,得先把左右两棵子树的高度都递归算出来,再回头处理 20。先去它的左孩子。
- 11叶子 70沿当前路径下行,来到节点 70。它没有左右孩子,是一个叶子。叶子下面是两个空,高度都按 0 算,马上结算它。
- 1270 高度=1结算叶子 70:左右都是空,两侧高度都是 0,正好相等。按口诀“等深则当前为答案”,70 自己就是这棵单节点子树里所有最深节点的最小子树。返回一对值:高度 1,候选根 70。
- 13叶子 45沿当前路径下行,来到节点 45。它没有左右孩子,是一个叶子。叶子下面是两个空,高度都按 0 算,马上结算它。
- 1445 高度=1结算叶子 45:左右都是空,两侧高度都是 0,正好相等。按口诀“等深则当前为答案”,45 自己就是这棵单节点子树里所有最深节点的最小子树。返回一对值:高度 1,候选根 45。
- 1520 左右各 1回到节点 20 结算。左子树高度 1,右子树高度 1,两边一样深。这说明 20 的左右两侧各藏着同样深的最深节点,唯一能同时罩住它们的最小子树,就是 20 自己。于是 20 升级为候选答案根,返回高度 2。
- 16右侧更深 高度=3回到节点 50 结算。左子树高度 1,右子树高度 2,右边更深。最深的节点全在更深那一侧,50 自己虽然能罩住所有最深节点,但会把浅的一侧也带进来,所以不是最小答案。把右侧抬上来的候选根 20 原样继续往上抛,高度更新为 3。
- 17展开 15 的子树下行进入节点 15。它还有孩子,现在还不能结算,得先把左右两棵子树的高度都递归算出来,再回头处理 15。先去它的左孩子。
- 18叶子 85沿当前路径下行,来到节点 85。它没有左右孩子,是一个叶子。叶子下面是两个空,高度都按 0 算,马上结算它。
- 1985 高度=1结算叶子 85:左右都是空,两侧高度都是 0,正好相等。按口诀“等深则当前为答案”,85 自己就是这棵单节点子树里所有最深节点的最小子树。返回一对值:高度 1,候选根 85。
- 20叶子 90沿当前路径下行,来到节点 90。它没有左右孩子,是一个叶子。叶子下面是两个空,高度都按 0 算,马上结算它。
- 2190 高度=1结算叶子 90:左右都是空,两侧高度都是 0,正好相等。按口诀“等深则当前为答案”,90 自己就是这棵单节点子树里所有最深节点的最小子树。返回一对值:高度 1,候选根 90。
- 2215 左右各 1回到节点 15 结算。左子树高度 1,右子树高度 1,两边一样深。这说明 15 的左右两侧各藏着同样深的最深节点,唯一能同时罩住它们的最小子树,就是 15 自己。于是 15 升级为候选答案根,返回高度 2。
- 23左侧更深 高度=4回到节点 30 结算。左子树高度 3,右子树高度 2,左边更深。最深的节点全在更深那一侧,30 自己虽然能罩住所有最深节点,但会把浅的一侧也带进来,所以不是最小答案。把左侧抬上来的候选根 20 原样继续往上抛,高度更新为 4。
- 24答案 = 以 20 为根的子树根 30 结算时,左边高度 3、右边高度 2,左边更深,于是把左边一路抬上来的候选根 20 定为最终答案。点亮的子树 20、70、45 恰好同时罩住两个最深节点 70 和 45,而且是满足条件里最小的一棵。答案就是以 20 为根的子树。
⚠️ 容易写错的地方
✗ 错:想用「最深节点的下标差」直接求
✓ 对:后序回传(高度, 子树根),自底向上判断
一遍 DFS 就能定位,不用先测全树深度再二次遍历
✗ 错:左右等深时还往某一侧抛
✓ 对:等深必须返回当前节点自己
两侧各有最深节点,只有当前节点能同时罩住
✗ 错:把「高度」和「深度」搞混
✓ 对:回传的是子树高度,越往上越大
高度自底向上累加,叶子为 1,根最大
完整代码(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 subtreeWithAllDeepest(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
def dfs(root: Optional[TreeNode]) -> Tuple[Optional[TreeNode], int]:
if root is None:
return None, 0
l, ld = dfs(root.left)
r, rd = dfs(root.right)
if ld > rd:
return l, ld + 1
if ld < rd:
return r, rd + 1
return root, ld + 1
return dfs(root)[0]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) {}
};
/**
* 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* subtreeWithAllDeepest(TreeNode* root) {
using pti = pair<TreeNode*, int>;
function<pti(TreeNode*)> dfs = [&]( TreeNode* root ) -> pti {
if (!root) {
return {nullptr, 0};
}
auto [l, ld] = dfs(root->left);
auto [r, rd] = dfs(root->right);
if (ld > rd) {
return {l, ld + 1};
}
if (ld < rd) {
return {r, rd + 1};
}
return {root, ld + 1};
};
return dfs(root).first;
}
};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 Pair<K, V> { private K key; private V value; Pair(K key, V value){this.key=key;this.value=value;} K getKey(){return key;} V getValue(){return value;} }
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 {
public TreeNode subtreeWithAllDeepest(TreeNode root) {
return dfs(root).getKey();
}
private Pair<TreeNode, Integer> dfs(TreeNode root) {
if (root == null) {
return new Pair<>(null, 0);
}
Pair<TreeNode, Integer> l = dfs(root.left);
Pair<TreeNode, Integer> r = dfs(root.right);
int ld = l.getValue(), rd = r.getValue();
if (ld > rd) {
return new Pair<>(l.getKey(), ld + 1);
}
if (ld < rd) {
return new Pair<>(r.getKey(), rd + 1);
}
return new Pair<>(root, ld + 1);
}
}复杂度
时间
O(n)
一次后序遍历,每个节点只在回溯时做一次常数比较
空间
O(h)
递归栈深度等于树高 h,最坏(退化成链)为 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 具有所有最深节点的最小子树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么一次后序 DFS 就够,不用先求全树最大深度再找?+
因为每个节点回传的是「自己这棵子树的高度」和「子树内最深节点的最小公共子树」。父节点拿到左右两个高度一比,就能在回溯途中顺手把答案往上传,信息是自底向上一次性凑齐的,不需要先扫一遍测深度、再扫一遍找节点。
这题和「最深叶子的最近公共祖先」是同一道吗?+
是的,本题和 LeetCode 1123 等价。「包含所有最深节点的最小子树的根」就是「所有最深叶子的最近公共祖先」,两种说法描述的是同一个节点,解法也完全一样。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 具有所有最深节点的最小子树 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。