【问题标题】:Find whether a tree is a subtree of other判断一棵树是否是其他树的子树
【发布时间】:2016-04-02 04:10:39
【问题描述】:

有两个二叉树 T1 和 T2 存储字符数据,允许重复。
如何确定 T2 是否是 T1 的子树? .
T1 有数百万个节点,T2 有数百个节点。

【问题讨论】:

  • 这棵树有某种排序吗?例如是二叉搜索树吗?
  • no.. 它不是二叉树
  • 对不起,我在误读上述 cmets 后将其重新标记为树(而不是二叉树),但当我意识到我的错误时将其改回 :-)
  • 定义子树 - 是 (T1 ( a ) ( b ) ) ( T2 ( T1 ( a x y ) ( b z ) ) ) 的子树,还是您只考虑等于 ( 而不是的子图)植根于 T1 的特定节点的树?
  • 所有节点都存储字符数据,还是只存储叶子?

标签: algorithm binary-tree


【解决方案1】:

穿越 T1。如果当前节点等于 T2 的根节点,则同时遍历两棵树(T2 和 T1 的当前子树)。比较当前节点。如果它们总是相等,则 T2 是 T1 的子树。

【讨论】:

  • 还要考虑深度。您只需搜索深度的 0 到 (T1_depth - T2_depth) 假设 0 是根深度并且 T1_depth >= T2_depth。
  • 确实如此。但是,根据树的实现方式,T1 的深度可能不知道,除非通过遍历它。
  • 抱歉挖掘了这么老的问题,但问题是允许重复数据。如果实际上存在重复,则此算法可能无法正确运行。我错过了什么吗?
  • @Aditya 如果您在每次失败的部分匹配后继续搜索,那么此算法是正确的。
  • @Jan Dvorak 这确实是正确的,但复杂性是什么? O(n^2)?您将多次访问同一个节点。
【解决方案2】:

我建议使用预处理的算法:

1) 预处理 T1 树,保留所有可能的子树根的列表(缓存列表将有数百万个条目);

2) 将缓存的根列表按数据的降序排序,保存在根中。您可以选择更优雅的排序功能,例如将字符树解析为字符串。

3) 使用二分查找来定位必要的子树。您可以快速拒绝节点数不等于 T2 节点数(或具有不同深度)的子树。

请注意,步骤 1) 和 2) 只能执行一次,然后您可以使用相同的预处理结果测试多个候选子树。

【讨论】:

    【解决方案3】:

    如果您的树没有以任何方式排序,除了进行 brute-force 搜索之外,我看不到任何其他方法:遍历树 T1 并检查您是否到达与树 T2 的第一个节点匹配的节点。如果没有,继续遍历T1。如果是,请检查下一个节点是否匹配,直到找到T2 的结尾,在这种情况下,您有一个命中:您的树T2 确实是T1 的子树。

    如果您知道T1 的每个节点的深度(从叶到节点),您可以跳过任何没有您要查找的子树那么深的节点。这可以帮助您消除很多不必要的比较。假设T1T2 平衡良好,那么树T1 的总深度将为20 (2**20 > 1,000,000),而树T2 的深度为7 (2**7 > 100)。您只需要遍历T1 的前 13 个(8192 个节点 - 或者是 14 层和 16384 个节点?)并且能够跳过大约 90% 的 T1 ...

    但是,如果 subtree 是指T2 的叶节点也是T1 的叶节点,那么您可以对T1 进行第一次遍历并计算每个节点的深度节点(叶子到节点的距离),然后只检查与T2具有相同深度的子树。

    【讨论】:

      【解决方案4】:

      如果您受内存/存储限制(即无法以替代形式预先操作和存储树),您可能无法比其他一些答案建议的蛮力搜索做得更好(遍历P1寻找P2的匹配根,遍历两者以确定该节点是否实际上是匹配子树的根,如果不匹配则继续原始遍历)。此搜索在 O(n * m) 中进行,其中 n 是 P1 的大小,m 是 P2 的大小。根据您可用的树数据,通过深度检查和其他潜在的优化,这个人被优化了一点,但它通常仍然是 O(n * m)。根据您的具体情况,这可能是唯一合理的方法。

      如果您不受内存/存储限制并且不介意有点复杂,我相信这可以通过在 @987654322 的帮助下减少到 longest common substring 问题来改进到 O(n + m) @。关于类似问题的一些讨论可以在here 找到。也许当我有更多时间时,我会回来编辑有关实现的更多细节。

      【讨论】:

      • 我将序列化两个树并查找第二个是否是第一个的子字符串。我们有各种流行语言的优化子字符串查找算法。
      【解决方案5】:

      如果给定两棵树的根,并且给定节点属于同一类型,那么为什么仅仅确定 T2 的根在 T1 中是不够的?

      我假设“给定一棵树 T”意味着给定一个指向 T 根的指针和节点的数据类型。

      问候。

      【讨论】:

        【解决方案6】:

        我不确定我的想法是否正确。尽管如此,对于您的个人而言。

        1. 在树 1 和树 2 中进行 postorder walk,称为 P1 和 P2。
        2. 比较 P1 和 P2。如果他们不同。树不是子树。退出。
        3. 如果它们相同,或者 P1 包含在 P2 中。您可以决定哪一个是子树。

        【讨论】:

          【解决方案7】:

          我认为我们需要通过蛮力进行,但是为什么在 T1 中找到 T2 的根之后还需要匹配树。 它与我们不应该查找树是否相同时不同。(那么我们只需要比较整棵树)

          给你树 T1 和 T2,指针,而不是值。

          如果节点 T2(它是一个指针)存在于 T1 树中。

          那么树 T2 是 T1 的子树。


          编辑:

          仅当它说T2实际上是一个不同的树时,我们需要找出T1中是否存在与T2相同的子树。

          这样就不行了。

          除了采用这里已经讨论过的解决方案外,我们别无选择。

          【讨论】:

            【解决方案8】:

            假设我们有 T1 作为父树,T2 作为树,它可能是 T1 的子树。请执行下列操作。假设 T1 和 T2 是没有任何平衡因子的二叉树。

            1) 在 T1 中搜索 T2 的根。如果没有找到 T2 不是子树。在 BT 中搜索元素需要 O(n) 时间。

            2) 如果找到元素,则从找到T2的节点根元素对T1进行前序遍历。这将花费 o(n) 时间。也做 T2 的前序遍历。将花费 O(n) 时间。前序遍历的结果可以存储到堆栈中。插入堆栈只需 O(1)。

            3) 如果两个栈的大小不相等,则 T2 不是子树。

            4) 从每个堆栈中弹出一个元素并检查是否相等。如果发生不匹配,则 T2 不是子树。

            5) 如果所有匹配的元素 T2 是一个子树。

            【讨论】:

              【解决方案9】:

              我假设你的树是不可变的树,所以你永远不会改变任何子树(在 Scheme 用语中你不做 set-car!),但你只是从叶子或现有的树中构建新树树。

              那么我建议在每个节点中保留(或子树)哈希码该节点。在 C 语言中,将树声明为

               struct tree_st {
                 const unsigned hash;
                 const bool isleaf;
                 union {
                   const char*leafstring; // when isleaf is true
                   struct { // when isleaf is false
                      const struct tree_st* left;
                      const struct tree_st* right;
                   };
                 };
               };
              

              然后在构建时计算哈希,当比较节点是否相等时,首先比较它们的哈希是否相等;大多数时候哈希码会有所不同(而且您不会费心比较内容)。

              这是一个可能的叶子构造函数:

              struct tree_st* make_leaf (const char*string) {
                 assert (string != NULL);
                 struct tree_st* t = malloc(sizeof(struct tree_st));
                 if (!t) { perror("malloc"); exit(EXIT_FAILURE); };
                 t->hash = hash_of_string(string);
                 t->isleaf = true;
                 t->leafstring = string;
                 return t;
              }
              

              计算哈希码的函数是

              unsigned tree_hash(const struct tree_st *t) {
                return (t==NULL)?0:t->hash;
              }
              

              sleft & sright 两个子树构造一个节点的函数是

              struct tree_st*make_node (const struct tree_st* sleft,
                                        const struct tree_st* sright) {
                 struct tree_st* t = malloc(sizeof(struct tree_st));
                 if (!t) { perror("malloc"); exit(EXIT_FAILURE); };
                 /// some hashing composition, e.g.
                 unsigned h = (tree_hash(sleft)*313) ^ (tree_hash(sright)*617);
                 t->hash = h;
                 t->left = sleft;
                 t->right = sright;
                 return t;
               }
              

              比较函数(两棵树txty)利用了如果哈希码不同则比较数不同

              bool equal_tree (const struct tree_st* tx, const struct tree_st* ty) {
                if (tx==ty) return true;
                if (tree_hash(tx) != tree_hash(ty)) return false;
                if (!tx || !ty) return false;
                if (tx->isleaf != ty->isleaf) return false;
                if (tx->isleaf) return !strcmp(tx->leafstring, ty->leafstring);
                else return equal_tree(tx->left, ty->left) 
                            && equal_tree(tx->right, ty->right); 
              

              }

              大多数情况下,tree_hash(tx) != tree_hash(ty) 测试会成功,您不必递归。

              了解hash consing

              一旦您拥有如此高效的基于哈希的equal_tree 函数,您就可以使用其他答案(或手册)中提到的技术。

              【讨论】:

                【解决方案10】:

                一种简单的方法是为树编写 is_equal() 方法并执行以下操作,

                bool contains_subtree(TNode*other) {
                    // optimization
                    if(nchildren < other->nchildren) return false;
                    if(height < other->height) return false;
                
                    // go for real check
                    return is_equal(other) || (left != NULL && left->contains_subtree(other)) || (right != NULL && right->contains_subtree(other));
                }
                

                注意 is_equal() 可以通过对树使用哈希码来优化。它可以通过将树的高度或子节点的数量或值的范围作为哈希码以简单的方式完成。

                bool is_equal(TNode*other) {
                    if(x != other->x) return false;
                    if(height != other->height) return false;
                    if(nchildren != other->nchildren) return false;
                    if(hashcode() != other->hashcode()) return false;
                    // do other checking for example check if the children are equal ..
                }
                

                当树类似于链表时,需要 O(n) 时间。我们还可以在选择要比较的孩子时使用一些启发式方法。

                bool contains_subtree(TNode*other) {
                    // optimization
                    if(nchildren < other->nchildren) return false;
                    if(height < other->height) return false;
                
                    // go for real check
                    if(is_equal(other)) return true;
                    if(left == NULL || right == NULL) {
                          return (left != NULL && left->contains_subtree(other)) || (right != NULL && right->contains_subtree(other));
                    }
                    if(left->nchildren < right->nchildren) { // find in smaller child tree first
                          return (left->contains_subtree(other)) || right->contains_subtree(other);
                    } else {
                          return (right->contains_subtree(other)) || left->contains_subtree(other);
                    }
                }
                

                另一种方法是将两个树序列化为字符串并查找第二个字符串(从 T2 序列化)是否是第一个字符串(从 T1 序列化)的子字符串。

                以下代码在预购中序列化。

                   void serialize(ostream&strm) {
                            strm << x << '(';
                            if(left)
                                    left->serialize(strm);
                            strm << ',';
                            if(right)
                                    right->serialize(strm);
                            strm << ')';
                    }
                

                并且我们可以使用一些优化的算法,例如Knuth–Morris–Pratt algorithm 来查找(可能在 O(n) 时间内)子字符串的存在,并最终找出一棵树是否是 other 的子树。

                再次使用 Burrows–Wheeler_transform 可以有效地压缩字符串。并且可以bzgrep在压缩数据中搜索子串。

                另一种方法是按高度和子节点数对树中的子树进行排序。

                bool compare(TNode*other) {
                    if(height != other->height)
                        return height < other->height;
                
                    return nchildren < other->nchildren;
                }
                

                请注意,会有 O(n^2) 个子树。为了减少数量,我们可以使用基于高度的一些范围。例如,我们只能对高度为 1000 到 1500 的子树感兴趣。

                生成排序后的数据时,可以在其中进行二进制搜索,并在 O(lg n) 时间内查找它是否是子集(考虑到排序后的数据中没有重复)。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2013-07-03
                  • 1970-01-01
                  • 2019-07-26
                  • 1970-01-01
                  • 2021-08-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多