体验算法的魅力——数据结构(另一棵树的子树)

给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。

示例 1:
给定的树 s:

 3
/  \   
4   5   
/  \  
1   2

给定的树 t:

 4  
/  \  
1   2

返回 true,因为 t 与 s 的一个子树拥有相同的结构和节点值。

示例 2:
给定的树 s:

    3
  /   \   
 4     5
/  \ 
1   2
   /    
  0

给定的树 t:

 4   
/  \  
1   2

返回 false。

方法一:DFS 暴力匹配

思路和算法

这是一种最朴素的方法 —— DFS 枚举 s 中的每一个节点,判断这个点的子树是否和 t 相等。如何判断一个节点的子树是否和 t相等呢,我们又需要做一次 DFS 来检查,即让两个指针一开始先指向该节点和 t 的根,然后「同步移动」两根指针来「同步遍历」这两棵树,判断对应位置是否相等。

c++

class Solution {
public:
    bool check(TreeNode *o, TreeNode *t) {
        if (!o && !t) return true;
        if ((o && !t) || (!o && t) || (o->val != t->val)) return false;
        return check(o->left, t->left) && check(o->right, t->right);
    }

    bool dfs(TreeNode *o, TreeNode *t) {
        if (!o) return false;
        return check(o, t) || dfs(o->left, t) || dfs(o->right, t);
    }

    bool isSubtree(TreeNode *s, TreeNode *t) {
        return dfs(s, t);
    }
};

java

	public boolean isSubtree(TreeNode s, TreeNode t) {
		if(t==null) {//t=null,一定都是true
			return true;
		}
		if(s==null) {//t一定不为null.只要s=null,就是false
			return false;
		}
		return isSubtree(s.left,t)||isSubtree(s.left, t)||isSameTree(s,t);
	}
	//判断两客树是否相同
	private boolean isSameTree(TreeNode s, TreeNode t) {
		// TODO Auto-generated method stub
		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);
	}

复杂度分析
●时间复杂度:对于每一个s上的点,都需要做一次DFS来和t匹配,匹配-次的时间代价是O(|t|), 那么总的时间代价就是O(|s| x |tl)。 故渐进时间复杂度为O(|s| x |t)。.
●空间复杂度:假设s深度为ds, t的深度为dt,任意时刻栈空间的最大使用代价是0(max{ds, dt})。 故渐进空间复杂度为0(max{ds,dt})。

方法二:DFS 序列上做串匹配

思路和算法

这个方法需要我们先了解一个「小套路」:一棵子树上的点在 DFS 序列(即先序遍历)中是连续的。了解了这个「小套路」之后,我们可以确定解决这个问题的方向就是:把 s 和 t 先转换成 DFS 序,然后看 t 的 DFS 序是否是 s 的 DFS 序的「子串」。

这样做正确吗? 假设 s 由两个点组成,1 是根,2 是 1 的左孩子;t 也由两个点组成,1是根,2 是 1 的右孩子。这样一来 s 和 t 的 DFS 序相同,可是 t 并不是 s 的某一棵子树。由此可见「s 的 DFS 序包含 t 的 DFS 序」是「t 是 s 子树」的 必要不充分条件,所以单纯这样做是不正确的。

为了解决这个问题,我们可以引入两个空值 lNull 和 rNull,当一个节点的左孩子或者右孩子为空的时候,就插入这两个空值,这样 DFS 序列就唯一对应一棵树。处理完之后,就可以通过判断 「s 的 DFS 序包含 t 的 DFS 序」来判断答案。

在判断「s 的 DFS 序包含 t 的 DFS 序」的时候,可以暴力匹配,也可以使用 KMP 或者 Rabin-Karp 算法,在使用 Rabin-Karp 算法的时候,要注意串中可能有负值。

这里给出用 KMP 判断的代码实现。

c++

class Solution {
public:
    vector <int> sOrder, tOrder;
    int maxElement, lNull, rNull;

    void getMaxElement(TreeNode *o) {
        if (!o) return;
        maxElement = max(maxElement, o->val);
        getMaxElement(o->left);
        getMaxElement(o->right);
    }

    void getDfsOrder(TreeNode *o, vector <int> &tar) {
        if (!o) return;
        tar.push_back(o->val);
        if (o->left) getDfsOrder(o->left, tar);
        else tar.push_back(lNull);
        if (o->right) getDfsOrder(o->right, tar);
        else tar.push_back(rNull);
    }

    bool kmp() {
        int sLen = sOrder.size(), tLen = tOrder.size();
        vector <int> fail(tOrder.size(), -1);
        for (int i = 1, j = -1; i < tLen; ++i) {
            while (j != -1 && tOrder[i] != tOrder[j + 1]) j = fail[j];
            if (tOrder[i] == tOrder[j + 1]) ++j;
            fail[i] = j;
        }
        for (int i = 0, j = -1; i < sLen; ++i) {
            while (j != -1 && sOrder[i] != tOrder[j + 1]) j = fail[j];
            if (sOrder[i] == tOrder[j + 1]) ++j;
            if (j == tLen - 1) return true;
        }
        return false;
    }

    bool isSubtree(TreeNode* s, TreeNode* t) {
        maxElement = INT_MIN;
        getMaxElement(s);
        getMaxElement(t);
        lNull = maxElement + 1;
        rNull = maxElement + 2;

        getDfsOrder(s, sOrder);
        getDfsOrder(t, tOrder);

        return kmp();
    }
};


java


import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class test2
{

    public static class TreeNode
    {
        int val;
        TreeNode left;
        TreeNode right;
        TreeNode(int x) { val = x; }
    }

    List<Integer> sOrder,tOrder;
    private int maxElement = Integer.MIN_VALUE;
    private int lNull, rNull;

    private void getMaxElement(TreeNode root)
    {
        if(root == null) return;
        maxElement = Math.max(maxElement,root.val);
        getMaxElement(root.left);
        getMaxElement(root.right);
    }

    private void getDfsOrder(TreeNode root,List<Integer> list)
    {
        if(root == null) return;
        list.add(root.val);

        if(root.left != null) getDfsOrder(root.left,list);
        else list.add(lNull);
        if(root.right != null) getDfsOrder(root.right,list);
        else list.add(rNull);
    }

    private boolean kmp()
    {
        int sLen = sOrder.size();
        int tLen = tOrder.size(); // 配置串
        int[] next = new int[tLen];

        Arrays.fill(next,-1);

        int i = 0;
        int k = -1, j = 0;
        while (j < tLen - 1)
        {
            if(k == -1 || tOrder.get(j).equals(tOrder.get(k)))
            {
                ++k;++j;
                next[j] = k;
            }
            else
            {
                k = next[k];
            }
        }

        j=0;
        while (i < sLen && j < tLen)
        {
            if(j == -1 || sOrder.get(i).equals(tOrder.get(j)))
            {
                ++i;
                ++j;
            }
            else
            {
                j = next[j];
            }
        }
        return j == tLen;
    }



    public boolean isSubtree(TreeNode s, TreeNode t)
    {

        if(s == null || t == null)
        {
            if(s == t) return true; // 证明全是null
            return false;
        }

        sOrder = new ArrayList<>();
        tOrder = new ArrayList<>();

        getMaxElement(s);
        getMaxElement(t);

        // 作为左右节点空节点的标识
        lNull = maxElement + 1;
        rNull = maxElement + 2;

        getDfsOrder(s,sOrder);
        getDfsOrder(t,tOrder);

        return kmp();
    }

    public static void main(String[] args)
    {
        TreeNode node1 = new TreeNode(3);
        TreeNode node2 = new TreeNode(1);
        TreeNode node3 = new TreeNode(5);
        TreeNode node4 = new TreeNode(1);
        TreeNode node5 = new TreeNode(1);

        node1.left = node2;
        node1.right = node3;
        node2.left = node4;
        node2.right = node5;

        TreeNode node6 = new TreeNode(1);
        TreeNode node7 = new TreeNode(1);
        TreeNode node8 = new TreeNode(1);

        node6.left = node7;
        node6.right = node8;

        test2 of = new test2();
        System.out.println(of.isSubtree(node1,node6));
    }
}



方法三:树哈希(纯属了解,科普知识)

思路和算法
考虑把每个子树都映射成一个唯一 的数,如果t对应的数字和s中任意一个子树映射的数字相等, 则t是s的某一棵子树。
如何映射呢?我们可以定义这样的哈希函数:
在这里插入图片描述

在这里插入图片描述

这样做为什么可行呢?回到我们的初衷,我们希望把每子树都映射成-个唯一 的数, 这样真的能够确保唯一吗? 实际上未必。但是我们在这个哈希函数中考虑到每个点的val、 子树哈希值、 子树大小以及左右子树的不同权值, 所以这些因素共同影响一个点的哈希值,所以出现冲突的几率较小,- 般我们可以忽略。当然你也可以设计你自己的哈希函数,只要考虑到
这些因素,就可以把冲突的可能性设计得比较小。可是如果还是出现了冲突怎么办呢?我们可以设计两个哈希函数f1和f2,用这两个哈希函数生成第三个哈希函数,比如在这里插入图片描述
等等,这样可以进一步缩小冲突,如果f1的冲突概率是P1,f2 的冲突概率是P2,那么f的冲突概率就是P1 X P2,理论已经非常了,这就是「双哈希。当然,为了减少冲突,你也可以设计「三哈希」、「四哈希 等,可是这样编程的复杂度就会增加。实际上, -般情
况下,只要运气不太差,一个哈希函数就足够了。

我们可以拥「埃氏筛法」或者「欧拉筛法」求出前arg π(max{|s|,t})个素数(中π(x)示x以内素数个数,
arg π(x)为它的反函数,示有多少以内包含x个素数,这个映射是不唯一的,我们取最小值), 然后DFS计算哈希值,骺比较8的所有子树是否有和t相同的哈希值即可。

class Solution {
public:
    static constexpr int MAX_N = 1000 + 5;
    static constexpr int MOD = int(1E9) + 7;

    bool vis[MAX_N];
    int p[MAX_N], tot;
    void getPrime() {
        vis[0] = vis[1] = 1; tot = 0;
        for (int i = 2; i < MAX_N; ++i) {
            if (!vis[i]) p[++tot] = i;
            for (int j = 1; j <= tot && i * p[j] < MAX_N; ++j) {
                vis[i * p[j]] = 1;
                if (i % p[j] == 0) break;
            }
        }
    }

    struct Status {
        int f, s; // f 为哈希值 | s 为子树大小
        Status(int f_ = 0, int s_ = 0) 
            : f(f_), s(s_) {}
    };

    unordered_map <TreeNode *, Status> hS, hT;

    void dfs(TreeNode *o, unordered_map <TreeNode *, Status> &h) {
        h[o] = Status(o->val, 1);
        if (!o->left && !o->right) return;
        if (o->left) {
            dfs(o->left, h);
            h[o].s += h[o->left].s;
            h[o].f = (h[o].f + (31LL * h[o->left].f * p[h[o->left].s]) % MOD) % MOD;
        }
        if (o->right) {
            dfs(o->right, h);
            h[o].s += h[o->right].s;
            h[o].f = (h[o].f + (179LL * h[o->right].f * p[h[o->right].s]) % MOD) % MOD;
        }
    }

    bool isSubtree(TreeNode* s, TreeNode* t) {
        getPrime();
        dfs(s, hS);
        dfs(t, hT);

        int tHash = hT[t].f;
        for (const auto &[k, v]: hS) {
            if (v.f == tHash) {
                return true;
            }
        } 

        return false;
    }
};

最后,不经历风雨,怎能在计算机的大山之顶看见彩虹呢! 无论怎样,相信明天一定会更好!!!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值