另一棵树的子树
原题
给你两棵二叉树 root
和 subRoot
。检验 root
中是否包含和 subRoot
具有相同结构和节点值的子树。如果存在,返回 true
;否则,返回 false
。
二叉树 tree
的一棵子树包括 tree
的某个节点和这个节点的所有后代节点。tree
也可以看做它自身的一棵子树。
示例 1:
输入:root = [3,4,5,1,2], subRoot = [4,1,2]
输出:true
示例 2:
输入:root = [3,4,5,1,2,null,null,null,null,0], subRoot = [4,1,2]
输出:false
提示:
root
树上的节点数量范围是[1, 2000]
subRoot
树上的节点数量范围是[1, 1000]
-104 <= root.val <= 104
-104 <= subRoot.val <= 104
/**
* 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 Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
}
}
解题思路
- 判断一棵二叉树是否为另一棵二叉树的子树:
- 两树相等为子树。
- 递归地判断左右子树是否存在相等的树。
- 判断两棵二叉树是否相等:
- 两棵树根节点都为空则相等。
- 递归地判断左右子树是否全都相等。
代码示例
/**
* 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 Solution {
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
// 如果两个树相同,返回 true
if (isSameTree(root, subRoot)) return true;
// 如果根节点是空的,说明没有子树
if (root == null) return false;
// 否则递归检查左子树和右子树
return isSubtree(root.left, subRoot) || isSubtree(root.right, subRoot);
}
private boolean isSameTree(TreeNode s, TreeNode t) {
// 如果两个根节点都为空,则相同
if (s == null && t == null) return true;
// 如果只有一个为空,则不同
if (s == null || t == null) return false;
// 如果值不同,也不相同
if (s.val != t.val) return false;
// 递归检查左子树和右子树是否相同
return isSameTree(s.left, t.left) && isSameTree(s.right, t.right);
}
}
优化
对于这种需要频繁比较树结构的问题,哈希化(Hashing)是一种非常有效的优化方法,特别是在处理大规模数据时。它可以巧妙地将复杂的结构比较问题转化为简单的数值比较,从而提高算法的效率。
思路
- 计算
subRoot
的哈希值。 - 遍历
root
树,为每个节点计算其作为根节点的子树的哈希值。 - 如果能找到一个哈希值匹配的节点,我们进行详细的树结构比较以确认它们确实相同,避免因为极少数的哈希冲突而得出错误的结果。
优化后代码
class Solution {
private static final int PRIME = 31;
private static final int MOD = 1000000007;
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
if (root == null) return subRoot == null;
if (subRoot == null) return true;
// 计算子树的哈希值
int subRootHash = computeHash(subRoot);
// 在主树中搜索匹配的哈希值
return searchSubtreeWithHash(root, subRoot, subRootHash);
}
private boolean searchSubtreeWithHash(TreeNode node, TreeNode subRoot, int subRootHash) {
if (node == null) return false;
// 计算当前节点为根的子树的哈希值
int currentHash = computeHash(node);
// 如果哈希值匹配,进行详细比较
if (currentHash == subRootHash && isSameTree(node, subRoot)) {
return true;
}
// 继续在左右子树中搜索
return searchSubtreeWithHash(node.left, subRoot, subRootHash) || searchSubtreeWithHash(node.right, subRoot, subRootHash);
}
private int computeHash(TreeNode node) {
if (node == null) return 0;
int leftHash = computeHash(node.left);
int rightHash = computeHash(node.right);
// 使用适中大小的质数和较大的模数来生成哈希值来降低哈希冲突概率
return (int)((((long)node.val * PRIME + leftHash) * PRIME + rightHash) % MOD);
}
private boolean isSameTree(TreeNode s, TreeNode t) {
if (s == null && t == null) return true;
if (s == null || t == null) return false;
if (s.val != t.val) return false;
return isSameTree(s.left, t.left) && isSameTree(s.right, t.right);
}
}
说明
在理想情况下,我们希望哈希函数能够完全区分所有不同的树结构。然而,实际上可能会遇到哈希值相同但树结构不同的情况,原因诸如有限的哈希空间、鸽巢原理和哈希函数的局限等。
减少冲突的方法
- 合适的质数与大模数:在该实现中,我们使用了质数
31
和大的模数1000000007
来减少冲突的可能性。- 为什么选择
31
:使用质数可以避免在连续乘法中出现周期性模式,这有助于减少规律性冲突。而且31
既不会太小导致哈希值分布不均,也不会太大导致计算开销过高。 - 为什么选择
1000000007 (10^9 + 7)
:- 在 Java 中,
int
是 32 位有符号整数,范围是-2^31
到2^31 - 1
,即-2,147,483,648
到2,147,483,647
。 - 对任意整数
n
和正整数m
,n % m
的结果总是在0
到m-1
之间,即(任何数) % 1,000,000,007
的结果必定在0
到1,000,000,006
之间。由于1,000,000,007
小于int
的最大值,取模的结果必定小于int
的最大值。 - 如果使用更大的值如
2,147,483,647
作为MOD
值时,在进行模运算之前的乘法操作很容易导致溢出。例如,(a * PRIME) % MOD
这样的操作,如果a
接近int
的最大值,乘以PRIME
后很可能会在取模之前就溢出,故不合适。 1,000,000,007
是一个接近2^30
的大素数。它足够大,可以减少哈希冲突。同时,它又足够小,保证取模结果不会超出int
范围,因此在许多算法实现中已经成为一种习惯和约定的模数。
- 在 Java 中,
- 为什么选择
- 复杂的哈希函数:我们的哈希函数考虑了节点值和左右子树的哈希值,这大大降低了冲突的概率。
node.val * PRIME
:将节点值乘以PRIME
,扩大数值范围。(node.val * PRIME + leftHash)
:加入左子树的哈希值。((node.val * PRIME + leftHash) * PRIME
:乘以PRIME
,进一步扩大范围。((node.val * PRIME + leftHash) * PRIME + rightHash)
:加入右子树的哈希值。
使用 long
和 int
类型转换
- 先将
node.val
转换为long
类型,避免中间结果溢出int
范围。 - 将最终的
long
结果转回int
,因为哈希值通常使用int
类型表示。