单值二叉树 图解题解
这道题到底在问什么
- 输入
- root=[1,1,1,1,1,null,1]
- 输出
- true(全是 1)
- 输入
- root=[2,2,2,5,2]
- 输出
- false(有个 5)
先想最直接的笨办法
先看全局。DFS 从根节点出发(紫色),第一件事是把根的值记下来当基准:x 等于 7。之后每个节点都要和这个 7 去比。这棵树七个节点,我们按先序一个一个走。(动画第 4 步)
最优解:为什么这么做
一句话答案:LeetCode 965 单值二叉树:判断是不是每个节点的值都相同,记下根值 x 做一次 DFS,逐点和 x 比、有一个不等就短路返回 false,时间 O(n)、空间 O(h)。
每个节点的值都相同吗,返回什么
给一棵二叉树 root,判断它是不是单值二叉树,也就是所有节点的值都一模一样。全都相同返回 true,只要冒出一个不同的就返回 false。题面例子 root=[1,1,1,1,1,null,1] 里各个位置的值全是 1,返回 true;root=[2,2,2,5,2] 中间混进一个 5,返回 false。节点数最多 100,值在 0 到 99 之间。
拿每个节点和父亲比,哪里别扭
一个想法是让每个节点和它的父节点比,只要相邻的父子处处相等,整棵树自然也就处处相等。这个判法本身没错,可写起来要一路把父节点的值当参数传下去,根节点又没有父亲、得单独开个头,这些边界很容易写岔。既然所有节点最终都要等于同一个值,不如挑一个固定的基准,让大家都跟它比。
干脆让所有节点都和根值 x 比
这个固定基准选根节点的值最省心,记成 x。之后不管走到哪个节点,都只和这个 x 比一次:等于就放行,不等就说明整棵树已经不是单值的了,直接返回 false,剩下的节点都不必再看。这样每个节点只关心自己和 x 的关系,不用惦记父亲是谁,边界一下子清爽了。遍历用 DFS,也就是顺着一条路一直走到底再回头。
一次 DFS 怎么逐点比、遇到不等就停
递归函数 dfs 收到一个节点:空节点直接返回 true,因为没有节点就谈不上有不同的值,空子树天然满足条件。非空时要三件事同时成立——当前节点值等于 x、左子树全等、右子树全等,用 and 串起来。and 带短路:当前值就不等于 x 时,后面两个递归压根不会执行;某棵子树返回了 false,也会立刻往上冒,不会再白跑剩下的节点。最外层先取 x=root.val,再从根调用 dfs。
全 1 的树和混进 5 的那棵各走几步
先看第一棵,x=1。从根开始,根值 1 等于 x,往下比左孩子 1、右孩子 1,再比下一层的几个 1,七个位置一路全等,最后返回 true。
再看第二棵,x=2。根值 2 对上 x,进入左孩子 2 也对上,接着比左孩子的左孩子,它的值是 5,5 不等于 2,这一层的 dfs 立刻返回 false。这个 false 顺着 and 一层层往上传,右边那半棵树根本没被碰过,最终返回 false。
空子树要返回 true,别把它写成 false
空节点的返回值是这里的坑点:把空子树当成 false,那么只要某个节点缺一个孩子,整棵树就会被误判成非单值。空子树没有任何节点,不存在「不同的值」,返回 true 才能让上层的 and 正确合并。还得记得,发现不等之后别再坚持把整棵树遍历完、白白多走一圈,靠 and 短路第一时间收手就好。时间上最坏要访问每个节点各比一次,O(n);空间是递归栈的深度,等于树高 h,平衡时 O(log n)、退化成一条链时 O(n)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3记住这句「记下根值 x,每个节点都和 x 比,有一个不等就 false」,下面每一帧都在套它。
- 4基准 x = 7先看全局。DFS 从根节点出发(紫色),第一件事是把根的值记下来当基准:x 等于 7。之后每个节点都要和这个 7 去比。这棵树七个节点,我们按先序一个一个走。
- 57 等于 7,匹配根自己当然等于基准 7,匹配通过,标绿。基准已经定好是 7,接下来轮到它的孩子们逐个来比。
- 6当前值 7DFS 先序走到根的左孩子(紫色),它的值是 7。先别急着放过,按套路要拿它和基准根值 7 比一比,看是不是同一个值。
- 77 等于 7,匹配根的左孩子的值 7 正好等于基准 7,匹配通过,标绿。到目前为止还没遇到不一样的,继续往下走。
- 8当前值 7DFS 先序走到左孩子的左孩子(紫色),它的值是 7。先别急着放过,按套路要拿它和基准根值 7 比一比,看是不是同一个值。
- 97 等于 7,匹配左孩子的左孩子的值 7 正好等于基准 7,匹配通过,标绿。到目前为止还没遇到不一样的,继续往下走。
- 10当前值 7DFS 先序走到左孩子的右孩子(紫色),它的值是 7。先别急着放过,按套路要拿它和基准根值 7 比一比,看是不是同一个值。
- 117 等于 7,匹配左孩子的右孩子的值 7 正好等于基准 7,匹配通过,标绿。到目前为止还没遇到不一样的,继续往下走。
- 12当前值 7DFS 先序走到根的右孩子(紫色),它的值是 7。先别急着放过,按套路要拿它和基准根值 7 比一比,看是不是同一个值。
- 137 等于 7,匹配根的右孩子的值 7 正好等于基准 7,匹配通过,标绿。到目前为止还没遇到不一样的,继续往下走。
- 14当前值 7DFS 先序走到右孩子的左孩子(紫色),它的值是 7。先别急着放过,按套路要拿它和基准根值 7 比一比,看是不是同一个值。
- 157 等于 7,匹配右孩子的左孩子的值 7 正好等于基准 7,匹配通过,标绿。到目前为止还没遇到不一样的,继续往下走。
- 16当前值 7DFS 先序走到右孩子的右孩子(紫色),它的值是 7。先别急着放过,按套路要拿它和基准根值 7 比一比,看是不是同一个值。
- 177 等于 7,匹配右孩子的右孩子的值 7 正好等于基准 7,匹配通过,标绿。到目前为止还没遇到不一样的,继续往下走。
- 18答案 true七个节点全部走完,每一个都等于基准 7,整棵树标绿。没有任何一个节点和根值不同,所以这是单值二叉树,返回 true。一遍 DFS 就把全体节点比完了。
- 19基准 x = 7基准 根值 x = 7,已确认同值: 0 / 3
- 207 等于 7,匹配基准 根值 x = 7,已确认同值: 1 / 3
- 21当前值 7基准 根值 x = 7,已确认同值: 1 / 3
- 227 等于 7,匹配基准 根值 x = 7,已确认同值: 2 / 3
- 23当前值 9基准 根值 x = 7,已确认同值: 2 / 3
- 249 不等于 7基准 根值 x = 7,发现不同值 9 ≠ 7
- 25答案 false判定: 存在不同值,结果 false
⚠️ 容易写错的地方
✗ 错:拿「当前节点和它的父节点」比
✓ 对:统一和「根值 x」比
和父比也对(相邻相等可推出全等),但和固定的根值 x 比更直观、不易写错
✗ 错:空树或空子树当成 false
✓ 对:空子树应返回 true
没有节点就没有「不同值」,空子树天然满足条件,返回 true 才能让递归正确合并
✗ 错:发现不等还继续递归整棵树
✓ 对:发现不等立刻短路返回 false
靠 and 短路,第一个不等就停,避免无谓遍历
完整代码(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 isUnivalTree(self, root: Optional[TreeNode]) -> bool:
def dfs(root: Optional[TreeNode]) -> bool:
if root is None:
return True
return root.val == x and dfs(root.left) and dfs(root.right)
x = root.val
return dfs(root)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:
bool isUnivalTree(TreeNode* root) {
int x = root->val;
function<bool(TreeNode*)> dfs = [&]( TreeNode* root ) -> bool {
if (!root) {
return true;
}
return root->val == x && dfs(root->left) && dfs(root->right);
};
return dfs(root);
}
};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 x;
public boolean isUnivalTree(TreeNode root) {
x = root.val;
return dfs(root);
}
private boolean dfs(TreeNode root) {
if (root == null) {
return true;
}
return root.val == x && dfs(root.left) && dfs(root.right);
}
}复杂度
时间
O(n)
最坏要访问每个节点各一次做一次比较,n 是节点总数;提前发现不等会更快返回
空间
O(h)
只用递归栈,深度等于树高 h;平衡时 O(log n),退化成链时最坏 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 单值二叉树 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
不想用递归,能改成 BFS 层序遍历吗?+
能。用一个队列做层序遍历,也就是一层一层横着走:先取根值当基准 x,每弹出一个节点就比一次它的值是否等于 x,不等立刻返回 false,等于就把它的非空孩子入队。所有节点都比完没发现不同,就返回 true。和 DFS 是一回事,时间同样 O(n),空间是队列里同时排队的节点数,最坏 O(n)。
为什么说「和相邻父子比」跟「都和根值比」是等价的?+
如果每一对相邻的父子值都相等,那沿着任意一条从根往下的路径,值一路不变,走到哪个节点都还等于根值,于是全体都等于根值;反过来全体都等于根值,相邻父子当然也相等。两种判法结论一样,只是和固定的根值 x 比不用把父节点的值传来传去,写起来更不容易出错。
空子树为什么要返回 true,而不是 false?+
空子树里一个节点都没有,也就无所谓「有没有不同的值」,它对「是否单值」这个判断不构成任何反例,所以应当放行返回 true。代码里当前节点的结果是「自己等于 x」和左右子树结果用 and 合并,缺了的那侧要贡献 true 才不会把好端端的一棵树误判掉;要是返回 false,任何有节点缺孩子的树都会被冤枉成非单值。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 单值二叉树 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。