测试两个二叉树是否相等的最有效方法

时间:2012-03-07 07:09:43

标签: java algorithm data-structures binary-tree

如何在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方法,它将2个根节点作为参数并运行标准的递归比较:

    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逐个屏蔽。我没有让我的代码工作,所以我不在这里发布(我很感激你实现的第二个想法以及你的改进)。

有关如何最有效地对二叉树进行相等测试的想法吗?

修改

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

然而,测试语义相等性的代码,例如“如果它们的内容相同,我们是否应该认为这两棵树是相同的,即使它们的结构不相同?”将按顺序迭代树,它应该是直截了当的。

5 个答案:

答案 0 :(得分:29)

好吧,有一件事你总是总是检查分支,即使你发现根是不相等的。如果您在发现不平等时刚刚返回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,目前会失败。 (对于添加节点的方式,这可能是也可能不存在。)

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

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

答案 1 :(得分:25)

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

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

这些树在相同的顺序中具有相同的内容,但由于结构不同,通过您的测试将不相等。

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

答案 2 :(得分:21)

首先,我做了一些一般的假设。这些假设对大多数基于树的集合类都有效,但总是值得检查:

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

    • 首先进行根参考相等检查。这样可以快速消除两个空值或传入相同树的情况,以便与自身进行比较。两者都是非常常见的情况,参考等式检查非常便宜。
    • 接下来检查空值。非null显然不等于null,这使您能够提前拯救 plus 它为以后的代码建立了非null保证!一个非常聪明的编译器理论上也可以使用这个保证来优化掉以后的空指针检查(不确定JVM当前是否这样做)
    • 检查数据引用相等并接下来为空。这样可以避免在树枝下一直向下移动,即使是在数据不一致的情况下,如果你先在树枝上下去,也会这样做。
    • 接下来检查data.equals()。您想再次在树枝之前检查数据相等性。检查空值后执行此操作,因为data.equals()可能更昂贵,并且您希望保证不会出现NullPointerException
    • 作为最后一步递归检查分支的相等性。如果你先做左或右做,除非更有可能让一方不平等,在这种情况下你应该首先检查那一方。例如,如果是大多数更改被附加到树的右侧分支....
    • 使比较成为静态方法。这是因为您希望以一种接受空值的方式递归使用它作为两个参数中的任何一个(因此它不适用于实例方法,因为this不能为null)。此外,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;
    }
    

答案 3 :(得分:3)

对于任何树,最有效的方式来表示它,以便您可以轻松检查相等是父列表 - 保持一个数组,其中为每个顶点记住其父项的索引(实际上保持一对 - 索引为父亲和数据值)。那你简直就是 应该比较两个连续的内存块。

仅当树是静态的(即不随时间变化)时才会起作用。如果两个树中的顶点索引相同,它也只会认为树是相等的。

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

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

答案 4 :(得分:0)

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

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

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

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