【问题标题】:The most efficient way to test two binary trees for equality测试两棵二叉树是否相等的最有效方法
【发布时间】:2012-03-24 17:27:47
【问题描述】:

你将如何在 Java 中实现二叉树节点类和二叉树类以支持最有效的(从运行时角度)相等检查方法(也必须实现):

    boolean equal(Node<T> root1, Node<T> root2) {}

    boolean equal(Tree t1, Tree t2) {}

首先,我按如下方式创建了 Node 类:

    public class Node<T> {
        private Node<T> left;
        private Node<T> right;
        private T data;

        // standard getters and setters
    }

然后是接受两个根节点作为参数并运行标准递归比较的equals方法:

    public boolean equals(Node<T> root1, Node<T> root2) {
        boolean rootEqual = false;
        boolean lEqual = false;
        boolean rEqual = false;    

        if (root1 != null && root2 != null) {
            rootEqual = root1.getData().equals(root2.getData());

            if (root1.getLeft()!=null && root2.getLeft() != null) {
                // compare the left
                lEqual = equals(root1.getLeft(), root2.getLeft());
            }
            else if (root1.getLeft() == null && root2.getLeft() == null) {
                lEqual = true;
            }
            if (root1.getRight() != null && root2.getRight() != null) {
                // compare the right
                rEqual = equals(root1.getRight(), root2.getRight());
            }
            else if (root1.getRight() == null && root2.getRight() == null) {
                rEqual = true;
            }

            return (rootEqual && lEqual && rEqual);
        }
        return false;
    } 

我的第二次尝试是使用数组和索引来实现树的遍历。然后可以使用两个数组上的按位运算 (AND) 进行比较 - 从 2 个数组中读取块并使用逻辑 AND 一个一个地屏蔽。我没有让我的代码正常工作,所以我不在这里发布(我很感激你对第二个想法的实现以及你的改进)。

任何想法如何最有效地对二叉树进行相等性测试?

编辑

问题假设结构平等。 (不是语义平等)

但是,测试语义相等性的代码,例如“如果它们的内容相同,即使它们的结构不同,我们是否应该认为两棵树是相等的?”将只是按顺序迭代树,它应该很简单。

【问题讨论】:

  • “我们应该考虑...”暗示了主观意见,并且使 SO 不适合这些问题 [可能被关闭为“不具建设性”]。您应该定义它是哪一个:您所追求的结构平等或语义平等。 [至少国际海事组织]

标签: java algorithm data-structures binary-tree


【解决方案1】:

一方面,您总是检查分支,即使您发现根不相等。如果您在发现不等式后立即返回 false,您的代码会更简单 (IMO) 且更高效。

另一个简化操作的选项是允许您的equals 方法接受null 值并将两个空值比较为相等。这样你就可以避免在不同的分支中进行所有这些无效检查。这不会提高效率,但会更简单:

public boolean equals(Node<T> root1, Node<T> root2) {
    // Shortcut for reference equality; also handles equals(null, null)
    if (root1 == root2) {
        return true;
    }
    if (root1 == null || root2 == null) {
        return false;
    }
    return root1.getData().equals(root2.getData()) &&
           equals(root1.getLeft(), root2.getLeft()) &&
           equals(root1.getRight(), root2.getRight());
} 

请注意,如果root1.getData() 返回null,目前这将失败。 (您添加节点的方式可能会也可能不会。)

编辑:正如 cmets 中所讨论的,您可以使用哈希码快速“提前退出”——但这会增加复杂性。

要么你需要让你的树不可变(这完全是另一个讨论)或者你需要每个节点都知道它的父节点,这样当节点是更改(例如通过添加叶子或更改值)它需要更新其哈希码并要求其父级也更新

【讨论】:

  • 谢谢乔恩,同意...如何粘贴我的问题中的代码 sn-p 以及您对答案的改进,以便我投票? (我想在接受之前让其他人有机会分享他们的想法:))
  • 您也可以在使用equals 之前使用hashcode。您仍然面临 O(n) 运行时。
  • @JonSkeet,你怎么看Hounshell的评论(见答案的第一句话)?
  • @stryba:hashCode() 有什么帮助?它必须遍历树,这将是一个 O(n) 操作......即使这样,如果两个哈希码相等,您仍然需要遍历所有内容。跨度>
  • @aviad:我假设你想要结构上的平等,否则你会以不同的方式编写代码:) 我们无法真正告诉你你的要求是什么。
【解决方案2】:

出于好奇,如果它们的内容相同,即使它们的结构不同,你是否认为两棵树是相等的?例如,这些是否相等?

  B         C        C      A
 / \       / \      / \      \
A   D     B   D    A   D      B
   /     /          \          \
  C     A            B          C
                                 \
                                  D

这些树以相同的顺序具有相同的内容,但由于结构不同,您的测试不会相等。

如果您想测试这种相等性,我个人只需使用中序遍历为树构建一个迭代器,并遍历树,逐个元素地比较它们。

【讨论】:

  • 好点!给我点赞。我想结构很重要,但应该澄清一下......我将把它添加到问题中。看看别人怎么说
  • 最终决定树是否相同取决于问题陈述以及您对假阴性的容忍程度。如果这是一个教科书问题,他们可能意味着结构是相同的。在现实世界中,这通常是无用的,因为通常以不确定的顺序构建树。然后,您实际上是在检查它们是否是同一个对象;参考检查就足够了。另外,请记住,有些树会在读取时改变其结构,而不仅仅是写入,例如 Treaps 和 Splay Trees
【解决方案3】:

首先,我做了一些一般性假设。这些假设适用于大多数基于树的集合类,但始终值得检查:

  1. 当且仅当它们在 树结构 和每个节点的 数据值 方面都相等(定义为data.equals(...))
  2. 在树节点上允许空数据值(这可能是因为您明确允许空值或因为您的数据结构仅在叶节点存储非空值)
  3. 对于可以利用的数据值的分布,您没有任何特别的不寻常事实(例如,如果您知道唯一可能的数据值是 null 或字符串“foo”,那么您就不用'不需要比较两个非空字符串值)
  4. 这些树通常大小适中且平衡良好。特别是,这可确保树永远不会太深,以至于您冒着深度递归导致 StackOverflowExceptions 的风险。

假设这些假设是正确的,那么我建议的方法是:

  • 首先进行根引用相等性检查。这很快消除了两个空值或同一棵树被传入以与自身进行比较的情况。两者都是非常常见的情况,并且引用相等性检查非常便宜。
  • 接下来检查空值。非空显然不等于空,这使您可以提前退出加上它为以后的代码建立了非空保证!一个非常聪明的编译器理论上也可以使用这个保证来优化以后的空指针检查(不确定JVM当前是否这样做)
  • 接下来检查数据引用相等性和空值。这样可以避免一直沿着树枝下降,即使在数据不相等的情况下,如果您先沿着树枝下降,您也会这样做。
  • 接下来检查 data.equals()。同样,您想在树枝之前检查数据是否相等。您在检查空值后执行此操作,因为 data.equals() 可能更昂贵,并且您希望保证不会收到 NullPointerException
  • 作为最后一步,递归检查分支的相等性。先做左边还是右边都没关系除非一侧不相等的可能性更大,在这种情况下,你应该先检查那一侧。这可能是这种情况,例如大多数更改都被附加到树的右分支....
  • 使比较成为静态方法。这是因为您希望以一种接受空值作为两个参数之一的方式递归地使用它(因此它不适合实例方法,因为this 不能为空)。此外,JVM 非常擅长优化静态方法。

因此,我的实现将类似于:

public static boolean treeEquals(Node a, Node b) {
    // check for reference equality and nulls
    if (a == b) return true; // note this picks up case of two nulls
    if (a == null) return false;
    if (b == null) return false;

    // check for data inequality
    if (a.data != b.data) {
        if ((a.data == null) || (b.data == null)) return false;
        if (!(a.data.equals(b.data))) return false;
    }

    // recursively check branches
    if (!treeEquals(a.left, b.left)) return false;
    if (!treeEquals(a.right, b.right)) return false;

    // we've eliminated all possibilities for non-equality, so trees must be equal
    return true;
}

【讨论】:

    【解决方案4】:

    对于任何树,最有效的表示它以便您可以轻松检查是否相等的方法是父列表 - 保存一个数组,其中对于每个顶点您都记住其父节点的索引(实际上保存一对 - 的索引父亲和数据值)。然后你只需 应该比较两个连续的内存块。

    这仅在树是静态的(即不随时间变化)时才有效。此外,如果两棵树中的顶点索引相同,它只会认为树是相等的。

    我相信在上述两个陈述不正确的常见情况下,您的实现应该尽可能快。

    编辑:事实上,如果您遵循 Jon Skeet 的回答中的建议,您的实施可以得到改进(至少在您知道树不相等时返回 false)

    【讨论】:

    • 谢谢,问题中(部分)提到了这个想法。我赞成你的回答,我很高兴看到这样做的代码。
    【解决方案5】:

    上面给出的代码将为具有相同根值的两棵不相等的树返回 true。我不认为这是你想要的。不应该是:

    如果 (!a==b) 返回 false;

    这样该方法将执行其余的检查。

    (由于某种原因无法从这里登录。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-08-01
      • 1970-01-01
      • 2021-11-24
      • 1970-01-01
      • 2021-08-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多