web-dev-qa-db-ja.com

2つのバイナリツリーが等しいかどうかをテストする最も効率的な方法

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
    }

次に、2つのルートノードを引数として取り、標準の再帰的比較を実行する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;
    } 

私の2番目の試みは、トラバース用の配列とインデックスを使用してツリーを実装することでした。次に、2つの配列のビット単位演算(AND)を使用して比較を実行できます。2つの配列からチャンクを読み取り、論理ANDを使用して1つずつマスクします。私は自分のコードを機能させることができなかったので、ここに投稿しません(2番目のアイデアの実装と改善点に感謝します)。

バイナリツリーの同等性テストを最も効率的に行う方法はありますか?

[〜#〜]編集[〜#〜]

質問は構造的平等を前提としています。 (意味的平等ではない)

ただし、セマンティックの等価性をテストするコード。 「構造が異なっていても、内容が同じであれば2つのツリーは等しいと考えるべきでしょうか?」順番にツリーを反復するだけで、簡単です。

18
aviad

1つには、ルートが等しくないことに気付いたとしても、ブランチをチェックしているalwaysです。不等式を見つけてすぐにfalseを返した場合、コードはより単純(IMO)で効率的になります。

物事を単純化する別のオプションは、equalsメソッドがnull値を受け入れ、2つの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を返すと失敗することに注意してください。 (これは、ノードを追加する方法で可能になる場合とできない場合があります。)

編集:コメントで説明したように、ハッシュコードを使用すると、非常にすばやく「早期に」作成できますが、複雑になります。

どちらかあなたはツリーを不変にする必要があります(これは他の議論全体です)or各ノードがその親について知る必要があるため、ノードが変更されると(例えば葉を追加するか、値を変更することで)ハッシュコードを更新する必要がありますそして親にも更新を依頼する

30
Jon Skeet

好奇心から、2つのツリーの構造が異なっていても、内容が同じであれば2つのツリーは等しいと考えますか?たとえば、これらは同じですか?

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

これらのツリーは同じコンテンツを同じ順序で持っていますが、構造が異なるため、テストによっては等しくありません。

この等価性をテストしたい場合は、個人的には、順序トラバーサルを使用してツリーのイテレータを作成し、要素ごとに比較してツリーを反復処理します。

25
Hounshell

まず、いくつかの一般的な仮定を行います。これらは、ほとんどのツリーベースのコレクションクラスに有効な前提条件ですが、常に確認する価値があります。

  1. 2つのツリーが等しいと見なすのは、各ノードでツリー構造データ値の両方が等しい場合のみです(data.equalsで定義)。 (...))
  2. nullデータ値はツリーノードで許可されます(これは明示的にnullを許可するか、データ構造がリーフノードで非nu​​ll値のみを格納するためです)
  3. 利用できるデータ値の分布について知っている特定の異常な事実はありません(たとえば、考えられる唯一のデータ値がnullまたは文字列 "foo"であることを知っていれば、必要ありません。 null以外の2つの文字列値を比較する)
  4. 木は通常、適度なサイズで、適度にバランスが取れています。特に、これにより、ツリーが深くなりすぎて、深い再帰によって発生するStackOverflowExceptionsのリスクが発生することがなくなります。

これらの仮定が正しいと仮定すると、私が提案するアプローチは次のとおりです。

  • 最初にルート参照の等価性チェックを実行します。これにより、2つのヌルまたは同じツリーがそれ自体との比較のために渡されるケースがすぐになくなります。どちらも非常に一般的なケースであり、参照等価チェックは非常に安価です。
  • 次にヌルを確認してください。非nullは明らかにnullと等しくないため、早期に救済することができますplusこれにより、後のコードで非nu​​llが保証されます。非常にスマートなコンパイラは、理論的にはこの保証を使用して、後でnullポインタチェックを最適化することもできます(JVMが現在これを行っているかどうかはわかりません)。
  • 次に、データ参照の等価性とnullを確認します。これにより、最初にツリーの枝を下った場合にデータが等しくない場合でも、ツリーの枝をずっと下がるのを回避できます。
  • 次にdata.equals()を確認してください。ここでも、ツリーが分岐する前にデータの等価性を確認します。 data.equals()は潜在的に高価であり、NullPointerExceptionが発生しないことを保証したいので、nullをチェックした後にこれを行います。
  • 最後のステップとして、ブランチの等価性を再帰的に確認します。最初に左または右のどちらを行うかは関係ありませんnless片側が等しくない可能性が高くなります。その場合は、最初にその側を確認する必要があります。これは、たとえばほとんどの変更はツリーの右側のブランチに追加されていました...
  • 比較を静的メソッドにします。これは、2つのパラメーターのいずれかとしてnullを受け入れる方法で再帰的に使用するためです(そのため、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;
}
22
mikera

どのツリーでも、等価性を簡単に確認できるようにツリーを表現する最も効率的な方法は、親リストです。各頂点について、その親のインデックスを覚えている配列を保持します(実際にはペアを保持します-親のインデックスとデータ値)。次に、メモリの2つの連続したブロックを比較するだけです。

これは、ツリーが静的である(つまり、時間とともに変化しない)場合にのみ機能します。また、頂点インデックスが2つのツリーで同じである場合にのみ、ツリーが等しいと見なします。

上記の2つのステートメントが当てはまらない場合の一般的なケースは、私は信じています。あなたの実装は、可能な限り速くすべきです。

編集:実際には、ジョン・スキートの回答のアドバイスに従うと、実装が改善される可能性があります(少なくともツリーが等しくないことがわかったらすぐにfalseを返します)

3

上記のコードは、同じルート値を持つ2つの等しくないツリーに対してtrueを返します。これがあなたの望んでいることだとは思いません。それはすべきではありません:

if(!a == b)はfalseを返します。

このようにして、メソッドは残りのチェックを実行します。

(何らかの理由で、ここからログインできません。)

0
Gerry Howser