LeetCode: 572. 另一棵树的子树

另一棵树的子树

原题

给你两棵二叉树 rootsubRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false

二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。

示例 1:
https://assets.leetcode.com/uploads/2021/04/28/subtree2-tree.jpg

输入:root = [3,4,5,1,2], subRoot = [4,1,2]
输出:true

示例 2:
https://assets.leetcode.com/uploads/2021/04/28/subtree2-tree.jpg

输入: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) {

    }
}

解题思路

  1. 判断一棵二叉树是否为另一棵二叉树的子树:
    • 两树相等为子树。
    • 递归地判断左右子树是否存在相等的树。
  2. 判断两棵二叉树是否相等:
    • 两棵树根节点都为空则相等。
    • 递归地判断左右子树是否全都相等。

代码示例

/**
 * 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)是一种非常有效的优化方法,特别是在处理大规模数据时。它可以巧妙地将复杂的结构比较问题转化为简单的数值比较,从而提高算法的效率。

思路

  1. 计算 subRoot 的哈希值。
  2. 遍历 root 树,为每个节点计算其作为根节点的子树的哈希值。
  3. 如果能找到一个哈希值匹配的节点,我们进行详细的树结构比较以确认它们确实相同,避免因为极少数的哈希冲突而得出错误的结果。

优化后代码

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);
    }
}

说明

在理想情况下,我们希望哈希函数能够完全区分所有不同的树结构。然而,实际上可能会遇到哈希值相同但树结构不同的情况,原因诸如有限的哈希空间、鸽巢原理和哈希函数的局限等。

减少冲突的方法
  1. 合适的质数与大模数:在该实现中,我们使用了质数 31 和大的模数 1000000007 来减少冲突的可能性。
    • 为什么选择 31:使用质数可以避免在连续乘法中出现周期性模式,这有助于减少规律性冲突。而且 31 既不会太小导致哈希值分布不均,也不会太大导致计算开销过高。
    • 为什么选择 1000000007 (10^9 + 7)
      • 在 Java 中,int 是 32 位有符号整数,范围是 -2^312^31 - 1,即 -2,147,483,6482,147,483,647
      • 对任意整数 n 和正整数 mn % m 的结果总是在 0m-1 之间,即 (任何数) % 1,000,000,007 的结果必定在 01,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 范围,因此在许多算法实现中已经成为一种习惯和约定的模数。
  2. 复杂的哈希函数:我们的哈希函数考虑了节点值和左右子树的哈希值,这大大降低了冲突的概率。
    • node.val * PRIME:将节点值乘以 PRIME ,扩大数值范围。
    • (node.val * PRIME + leftHash):加入左子树的哈希值。
    • ((node.val * PRIME + leftHash) * PRIME:乘以 PRIME ,进一步扩大范围。
    • ((node.val * PRIME + leftHash) * PRIME + rightHash):加入右子树的哈希值。
使用 longint 类型转换
  • 先将 node.val 转换为long 类型,避免中间结果溢出 int 范围。
  • 将最终的 long 结果转回 int,因为哈希值通常使用 int 类型表示。
  • 27
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值