ツリーは「最初の子、次の兄弟」構造で構成されていますか?そうでない場合は、なぜですか?


12

通常、ツリーデータ構造は、各ノードがそのすべての子へのポインタを含むように編成されます。

       +-----------------------------------------+
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------------+    +---------------+    +---------------+
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

これは自然に思えますが、いくつかの問題があります。たとえば、子ノードの数が異なる場合、子を管理するには配列やリストなどが必要です。

代わりに(最初の)子ポインターと(次の)兄弟ポインターのみを使用すると、次のようになります。

       +-------------------+
       |        root       |
       | child    sibling  +--->NULL
       +--+----------------+
          |             
+----------------+    +----------------+    +----------------+
|    node1       |    |     node2      |    |     node3      |
| child  sibling +--->| child  sibling +--->| child  sibling +--->NULL
+--+-------------+    +--+-------------+    +--+-------------+
   |                     |                     |

もちろん、この種の構造はツリーを表すこともできますが、いくつかの利点もあります。最も重要なのは、子ノードの数をこれ以上心配する必要がないことです。解析ツリーに使用すると、深いツリーになることなく、「a + b + c + d + e」などの用語の自然な表現を提供します。

コレクションライブラリはそのようなツリー構造を提供しますか?パーサーはそのような構造を使用しますか?そうでない場合、その理由は何ですか?


2
さて、この構造は明らかに複雑さが増します。実際に可変数の子が必要な場合にのみ価値あります。多くのツリーには、その設計に固有の固定数の子(または少なくとも固定の最大)があります。これらの場合、追加の間接指定は値を追加しません。
ヨアヒムザウアー

4
リンクリストにアイテムを配置するとO(n)、アルゴリズムに要因が導入されます。

ルートからnode3にアクセスするには、ルートのcddarを取得する必要があります...
タクロイ

Tacroy:正しい、ルートに戻るのは簡単ではありませんが、本当に必要な場合は、バックポインターが適切です(ただし、ダイアグラムを台無しにします;-)
user281377

回答:


7

ツリーは、リストと同様に、さまざまな方法で実装できる「抽象データ型」です。それぞれの方法には長所と短所があります。

最初の例では、この構造の主な利点は、O(1)のどの子にもアクセスできることです。欠点は、配列を拡張する必要がある場合、子を追加するのが少し高価になる場合があることです。ただし、このコストは比較的小さいです。また、最も単純な実装の1つです。

2番目の例の主な利点は、O(1)に常に子を追加することです。主な欠点は、子へのランダムアクセスにO(n)がかかることです。また、それはできるにはあまり興味深いもの巨大な二つの理由のために木:それは一つのオブジェクトヘッダとノードごとに2つのポインタのメモリオーバーヘッドを有し、ノードがランダムにメモリ上に広がっていることがあり、CPUのキャッシュとの間でスワップの多くを引き起こしますツリーをたどるときのメモリ。この実装は彼らにとってあまり魅力的ではありません。ただし、これは通常のツリーおよびアプリケーションでは問題になりません。

言及されていない最後の興味深い可能性の1つは、ツリー全体を単一の配列に格納することです。これによりコードはより複雑になりますが、オブジェクトヘッダーのコストを節約して連続したメモリを割り当てることができるため、特定のケース、特に巨大な固定ツリーでは非常に有利な実装になる場合があります。


1
たとえば、B +ツリーはこの「firstchild、nextsibling」構造を使用しません。ディスクベースのツリーでは不合理な点までは非効率ですが、メモリベースのツリーでは依然として非常に非効率です。インメモリRツリーはこの構造を許容できますが、それでもキャッシュミスが多くなることを意味します。「最初の子、次の兄弟」の方が優れている状況を考えるのは難しいです。ええ、ええ、ammoQが述べたように、構文ツリーで機能します。他に何か?
Qwertie

3
「常にO(1)に子を追加する」-O(1)のインデックス0にいつでも子を挿入できると思いますが、子の追加は明らかにO(n)のようです。
スコットホイットロック

ヒープでは、ツリー全体を単一の配列に格納するのが一般的です。
ブライアン

1
@Scott:それがのOPS例では欠落しているがよく、私は...最初または最後のPOSのためにO(1)になるだろうれ、リンクされたリストも同様に最後の項目へのポインタ/参照を含んでいたと仮定
dagnelies

(おそらく非常に退化した場合を除いて)「firstchild、nextsibling」実装は、配列ベースの子テーブル実装よりも効率的ではないに違いない。キャッシュの局所性が大いに役立ちます。Bツリーは、キャッシュ局所性が改善されたために、従来の赤黒ツリーに比べて、現代のアーキテクチャではるかに効率的な実装であることが証明されています。
コンラッドルドルフ

2

編集可能なモデルまたはドキュメントを含むほとんどすべてのプロジェクトには、階層構造があります。さまざまなエンティティの基本クラスとして「階層ノード」を実装すると便利です。多くの場合、リンクリスト(子の兄弟、2番目のモデル)は多くのクラスライブラリが成長する自然な方法ですが、子はさまざまなタイプである可能性があり、おそらく「オブジェクトモデル」は一般的に木について話すときは考慮しません。

最初のモデルのツリー(ノード)の私のお気に入りの実装は、ワンライナー(C#)です:

public class node : List<node> { /* props go here */ }

独自のタイプの汎用リストから継承します(または、独自のタイプの汎用コレクションから継承します)。歩行は一方向に可能です。ルートを下向きに形成します(アイテムは親を知りません)。

親のみのツリー

あなたが言及しなかった別のモデルは、すべての子がその親への参照を持っているものです:

               null
                 |
       +---------+---------------------------------+
       |       parent                              |
       | root                                      |
       +-------------------------------------------+
          |                   |                |
+---------+------+    +-------+--------+    +--+-------------+
|     parent     |    |     parent     |    |     parent     |
|     node 1     |    |     node 2     |    |     node 3     |
+----------------+    +----------------+    +----------------+

このツリーをたどることは逆方向にのみ可能です。通常、これらのノードはすべてコレクション(配列、ハッシュテーブル、辞書など)に格納され、ノードは、通常は主に重要ではないツリー。

これらの親のみのツリーは、通常データベースアプリケーションで見られます。「SELECT * WHERE ParentId = x」ステートメントを使用して、ノードの子を見つけるのは非常に簡単です。ただし、これらがツリーノードクラスオブジェクトに変換されることはほとんどありません。ステートフル(デスクトップ)アプリケーションでは、既存のツリーノードコントロールにラップされる場合があります。ステートレス(Web)アプリケーションでは、その可能性は低いかもしれません。ORMマッピングクラスジェネレーターツールは、それ自体と関係のあるテーブル(クラスル)のクラスを生成するときにスタックオーバーフローエラーをスローするので、これらのツリーはそれほど一般的ではないかもしれません。

双方向のナビゲート可能なツリー

ただし、ほとんどの実際のケースでは、両方の長所を兼ね備えていると便利です。子のリストを持ち、さらにその親である双方向のナビゲート可能なツリーを知っているノード。

                          null
                            |
       +--------------------+--------------------+
       |                  parent                 |
       |        root                             | 
       | child1            child2         child3 |
       +--+------------------+----------------+--+
          |                  |                |
+---------+-----+    +-------+-------+    +---+-----------+
|      parent   |    |     parent    |    |  parent       |
|    node1      |    |     node2     |    |     node3     |
| child1 child2 |    | child1 child2 |    | child1 child2 |
+--+---------+--+    +--+---------+--+    +--+---------+--+
   |         |          |         |          |         |

これにより、考慮すべき多くの側面がもたらされます。

  • 親のリンクとリンク解除の実装場所
    • 業務ロジックに注意を払い、ノードからアスペクトを除外します(それらは忘れられます!)
    • ノードには子を作成するためのメソッドがあります(並べ替えは許可されません)(MicrosoftがSystem.Xml.XmlDocument DOM実装で選択したため、最初に遭遇したときはほとんど夢中になりました)
    • ノードはコンストラクターで親を取得します(並べ替えを許可しません)
    • すべてのadd()、insert()およびremove()メソッド、およびそれらのノードのオーバーロード(通常は私の選択)
  • 持続性
    • 永続化するときにツリーを歩く方法(たとえば、親リンクを除外する)
    • デシリアライズ後に双方向リンクを再構築する方法(デシリアライズ後のアクションとしてすべての親を再度設定する)
  • 通知
    • 静的メカニズム(IsDirtyフラグ)、プロパティで再帰的に処理しますか?
    • イベント、親を介してバブルアップ、子を介してダウンバブル、またはその両方(Windowsメッセージポンプを考慮してください)。

質問に答えるために、双方向のナビゲート可能なツリーは(これまでの私のキャリアと分野で)最も広く使用される傾向があります。例としては、MicrosoftのSystem.Windows.Forms.Controlの実装、または.NetフレームワークのSystem.Web.UI.Controlがありますが、すべてのDOM(Document Object Model)実装には、親および列挙を認識するノードがあります子供たちの。理由:実装の容易さよりも使いやすさ。また、これらは通常、より特定のクラスの基本クラスであり(XmlNodeはTag、Attribute、Textクラスのベースである可能性があります)、これらの基本クラスは一般的なシリアル化とイベント処理アーキテクチャを配置する自然な場所です。

Treeは多くのアーキテクチャの中心にあり、自由にナビゲートできるということは、ソリューションをより迅速に実装できることを意味します。


1

2番目のケースを直接サポートするコンテナライブラリは知りませんが、ほとんどのコンテナライブラリはそのシナリオを簡単にサポートできます。たとえば、C ++では次のことができます。

class Node;  // forward reference to satisfy the compiler
typedef std::list<Node*> NodeList;
class Node : public NodeList { /* . . . */ };  // a node is also a list

Node* n = new Node;
n->push_back(new Node);
Node* tree = new Node;
tree->push_back(new Node);
tree->push_back(n);

パーサーは、おそらくこれと同様の構造を使用します。これは、可変数のアイテムと子を持つノードを効率的にサポートするためです。私は通常彼らのソースコードを読まないので、確かにわかりません。


1

子の配列を持つことが望ましい場合の1つは、子へのランダムアクセスが必要な場合です。そして、これは通常、子がソートされるときです。たとえば、ファイルのような階層ツリーでは、これを使用してパス検索を高速化できます。または、インデックスアクセスが非常に自然な場合のDOMタグツリー

別の例は、すべての子への「ポインター」があると、より便利に使用できるようになる場合です。たとえば、リレーショナルデータベースでツリーリレーションを実装する場合、説明した両方のタイプを使用できます。ただし、前者(この場合は親から子へのマスター詳細)では、有用なデータを一般的なSQLで照会できますが、後者では大幅に制限されます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.