
コンピュータサイエンスにおいて、二分木は、各ノードが最大で2つの子(左の子と右の子と呼ばれる)を持つ木データ構造である。つまり、k = 2のk分木である。集合論を用いた再帰的な定義では、二分木は( L、S、R )の三重項であり、LとRは二分木または空集合であり、Sは根を含む単一要素集合(単一要素集合)である。 [ 1 ] [ 2 ]
グラフ理論の観点から見ると、ここで定義される二分木は樹状構造です。[ 3 ]したがって、二分木は分岐樹状構造とも呼ばれます。[ 3 ]この用語は、現代のコンピュータ科学用語が普及する前の初期のプログラミング書籍に登場します。 [ 4 ]また、二分木を有向グラフではなく無向グラフとして解釈することも可能で、その場合、二分木は順序付けられた根付き木となります。[ 5 ]一部の著者は、木が根付きであることを強調するために、二分木の代わりに根付き二分木を使用しますが、上記のように定義すると、二分木は常に根付きです。[ 6 ]
数学において、二分木と呼ばれるものは、著者によって大きく異なる場合があります。コンピュータサイエンスで一般的に使用される定義を使用する著者もいますが[ 7 ]、葉以外のすべての要素がちょうど2つの子を持つものと定義し、子を必ずしも左と右とラベル付けしない著者もいます[ 8 ] 。
コンピュータにおいて、二分木は大きく異なる2つの方法で使用できます。
二分木を記述するシンプルで非公式な方法は、次のようになるでしょう。
より正式には:
この定義には2つの制約があります。完全二分木には少なくとも1つのノードが含まれること、そしてどのノードも1つの子しか持たないことはあり得ないことです。これは次の定義で解決されます。
拡張ツリーの定義は、ツリーが空である可能性があるという前提から始まります。
グラフの観点から完全を期すには、木の定義の両方に、対応する枝集合の定義を追加する必要があります。非公式には、枝集合は、定義に現れる任意の部分木の根ノードをr 、その部分木の T 1および T 2のいずれかの根ノードをsとする、すべての順序付きノードのペア(r, s)の集合として記述できます(それぞれの部分木が空でない場合)。
この構造を想像する(そして用語を理解する)別の方法は、空集合の代わりに別のタイプのノード、例えば通常のノードが円形であれば正方形のノードを考えることである。[ 13 ]
二分木は、各ノードが最大で 2 つの子を持つ、順序付き木(別名平面木)でもある根付き木です。根付き木は自然にレベル (根からの距離) の概念を与えます。したがって、各ノードについて、子の概念は、そのノードの 1 レベル下に接続されているノードとして定義できます。これらの子を順序付け (たとえば、平面上に描画する) することで、左の子と右の子を区別できます。[ 14 ]しかし、これだけでは、左の子は持つが右の子を持たないノードと、右の子は持つが左の子を持たないノードを区別することはできません。
必要な区別は、まずエッジを分割することによって行うことができます。つまり、二分木をトリプレット (V, E 1 , E 2 ) として定義します。ここで、(V, E 1 ∪ E 2 ) は根付き木 (同等に有木構造) であり、E 1 ∩ E 2は空です。また、すべてのj ∈ { 1, 2 } に対して、すべてのノードが最大で 1 つの E j子を持つ必要があります。[ 15 ]区別するより非公式な方法は、数学百科事典を引用して、「すべてのノードは左の子、右の子、どちらでもない、または両方を持つ」と言い、これらが「すべて異なる」二分木であることを指定することです。[ 7 ]
樹木に関する用語は十分に標準化されていないため、既存の文献における例によって異なる場合がある。



組み合わせ論では、与えられたサイズの完全二分木の数を数える問題が考えられます。ここでは、木のノードには値が付けられていません(値を付けると、可能な木の数が簡単に決定できる係数で増えるだけです)。また、木は構造によってのみ区別されます。ただし、任意のノードの左の子と右の子は区別されます(異なる木であれば、それらを交換することで元の木とは異なる木が生成されます)。木のサイズは、内部ノード(2つの子を持つノード)の数nとします。その他のノードは葉ノードであり、n + 1個あります。このようなサイズnの二分木の数は、n + 1個の記号(葉を表す)からなる文字列をn個の二項演算子(内部ノードを表す)で区切って完全に括弧で囲み、各演算子の引数部分式を決定する方法の数に等しくなります。たとえば、n = 3の場合、次のような文字列を括弧で囲む必要があります。これは以下の5つの方法で可能です。
二分木との対応関係は明白であるべきであり、冗長な括弧の追加(既に括弧で囲まれた式や式全体を囲むこと)は許可されない(少なくとも新たな可能性を生み出すとはみなされない)。
サイズ0の二分木(単一の葉からなる)は一意に存在し、他の二分木は左右の子のペアによって特徴付けられます。これらの子のサイズがそれぞれiとjである場合、完全な木のサイズはi + j + 1になります。したがって、その数はサイズnの二分木は、以下の再帰的な記述を持つ。、 そして任意の正の整数nに対して、次のことが成り立つ。は指数nのカタラン数である。[ 18 ]
上記の括弧付き文字列は、ディック語の長さ 2 nの単語の集合と混同してはならない。ディック語の長さ 2 n の単語の集合は、適切にバランスのとれた括弧のみで構成されている。このような文字列の数は、同じ再帰的な記述を満たす(長さ 2 n の各ディック語は、最初の「(」とその対応する「)」で囲まれたディック語部分語と、その閉じ括弧の後に残るディック語部分語によって決定され、その長さ 2 iと 2 jはi + j + 1 = nを満たす)。したがって、この数はカタロニア語の数でもある。[ 27 ]つまり、長さ6のディック語も5つある。
これらの Dyck 単語は、同じ方法では二分木に対応しません。代わりに、次の再帰的に定義された全単射によって関連付けられます。空文字列に等しい Dyck 単語は、葉が 1 つだけのサイズ 0 の二分木に対応します。他の Dyck 単語は次のように書くことができます ()、 どこ、はそれ自体が(おそらく空の)ディック語であり、2 つの書かれた括弧が一致する。その後、単語をそしてルートの左と右の子である二分木に対応する。
全単射対応は次のように定義することもできます。ディック語を括弧で囲み、結果をLispリスト式(空リスト()が唯一の出現アトム)として解釈できるようにします。すると、その適切なリストのドットペア式は、対応する二分木(実際には適切なリストの内部表現)を記述する、完全に括弧で囲まれた式(シンボルはNIL、演算子は'.')になります。
二分木を記号と括弧の文字列として表現できるということは、二分木が単一要素集合上の自由マグマの要素を表現できることを意味する。
二分木は、プログラミング言語の基本要素を用いていくつかの方法で構築することができる。
レコードと参照を持つ言語では、二分木は通常、データと左の子ノードおよび右の子ノードへの参照を含むツリーノード構造によって構築されます。場合によっては、一意の親ノードへの参照も含まれていることがあります。ノードの子ノードが2つ未満の場合、子ノードへのポインタの一部は、特別なヌル値、または特別な番兵ノードに設定されることがあります。
このバイナリツリーの格納方法は、ポインタが半分以上の時間でヌル(または番兵を指す)になるため、かなりのメモリを無駄にします。より保守的な表現方法として、スレッドバイナリツリーがあります。[ 28 ]
MLのようなタグ付き共用体を持つ言語では、ツリーノードは多くの場合、2 種類のノードのタグ付き共用体であり、1 つはデータ、左の子、右の子の 3 タプルであり、もう 1 つは「リーフ」ノードで、データを含んでおらず、ポインタを持つ言語の null 値とよく似た機能を持ちます。たとえば、OCaml (ML の方言) の次のコード行は、各ノードに文字を格納する二分木を定義します。[ 29 ]
type chr_tree = Empty | Node of char * chr_tree * chr_tree二分木は、幅優先順序で暗黙的なデータ構造として配列に格納することもできます。また、木が完全二分木の場合、この方法はスペースを無駄にしません。このコンパクトな配置では、ノードのインデックスがiの場合、その子はインデックス 1 で見つかります。(左の子供の場合)そして(右側の場合)一方、その親(存在する場合)はインデックスで見つかります(ルートのインデックスがゼロであると仮定します)。あるいは、1から始まるインデックスの配列では、子要素が次の位置にあるため、実装は簡略化されます。そして、親は以下で見つかりました[ 30 ]
この方法は、特に先行順走査の際に、よりコンパクトなストレージとより優れた参照の局所性という利点があります。バイナリヒープによく使用されます。[ 31 ]

簡潔なデータ構造とは、情報理論の下限によって確立された、可能な限り最小限のスペースを占める構造のことです。ノードは、カタラン数(構造が同一の木を同一とみなすと仮定)。これは、;したがって、少なくとも約それをエンコードするにはビットが必要です。したがって、簡潔なバイナリツリーは2 n + o( n )ビットを占有します ('o()' はリトル オー表記です)。
この制約を満たす単純な表現方法の1つは、ツリーのノードを順方向に訪問し、内部ノードには「1」、葉ノードには「0」を出力することです。[ 32 ]ツリーにデータが含まれている場合は、それを順方向に連続した配列に同時に格納するだけで済みます。この関数はこれを実現します。
function EncodeSuccinct( node n, bitstring structure, array data) { if n = nil then 構造体に0を追加する。 それ以外 構造体に1を追加する。 n.dataをデータに追加する。 EncodeSuccinct(n.left, structure, data); EncodeSuccinct(n.right, structure, data); }文字列構造には最終的には、は(内部)ノードの数です。長さを保存する必要すらありません。情報が失われていないことを示すために、出力を次のように元のツリーに変換できます。
function DecodeSuccinct( bitstring structure, array data) {構造 の最初の部分を削除してb に格納する。b = 1の場合は 新しいノードnを作成する。 データの最初の要素を削除して、n.dataに格納します。 n.left = DecodeSuccinct(structure, data) n.right = DecodeSuccinct(structure, data) n を返す。そうでなけれ ばnを返す。 }より洗練された簡潔な表現を用いることで、ツリーをコンパクトに格納できるだけでなく、簡潔な形式のまま、ツリーに対して直接有用な操作を行うことも可能になる。
順序付き木と二分木の間には、自然な一対一の対応関係が存在する。これにより、任意の順序付き木は一意に二分木として表現でき、その逆もまた同様である。
Tを順序付き木のノードとし、Bを対応する二分木におけるTの像とする。このとき、Bの左の子はTの最初の子を表し、Bの右の子はTの次の兄弟を表す。
例えば、左側の順序付き木と右側の二分木は、それぞれ以下のように対応します。

図示された二分木では、左側の黒い辺は最初の子を表し、右側の青い辺は次の兄弟を表します。
この表現は、左子右兄弟二分木と呼ばれます。

二分木に対しては、さまざまな操作を実行できます。中には変異操作もありますが、単に木に関する有用な情報を返すだけのものもあります。
ノードは、二分木内の他の2つのノードの間に挿入することも、葉ノードの後に追加することもできます。二分木では、挿入されるノードは、どのノードの子になるかが指定されます。
リーフノードAの後に新しいノードを追加するには、Aは新しいノードを自身の子ノードの1つとして割り当て、新しいノードはノードAを自身の親ノードとして割り当てます。

内部ノードへの挿入は、リーフノードへの挿入よりもやや複雑です。内部ノードをノードA、ノードBをAの子とします。(挿入が右の子を挿入する場合、BはAの右の子であり、左の子を挿入する場合も同様です。)Aは子を新しいノードに割り当て、新しいノードは親をAに割り当てます。次に、新しいノードは子をBに割り当て、Bは親を新しいノードに割り当てます。
削除とは、ノードをツリーから削除するプロセスです。二分木では、特定のノードのみを明確に削除できます。[ 33 ]

削除するノードがノードAであるとします。Aに子ノードがない場合、Aの親ノードの子ノードをnullに設定することで削除が実行されます。Aに子ノードが1つある場合は、Aの子ノードの親ノードをAの親ノードに設定し、Aの親ノードの子ノードをAの子ノードに設定します。
二分木では、2 つの子を持つノードは明確に削除できません。[ 33 ]ただし、特定の二分木(二分探索木を含む)では、木構造の再配置を伴いますが、これらのノードを削除できます。
前順走査、中順走査、後順走査は、ルートの左右のサブツリー内の各ノードを再帰的に走査することで、ツリー内の各ノードを走査します。以下に、上記の走査方法の簡単な説明を示します。
先行順走査では、まず現在のノードを訪問し、次に現在のノードの左部分木を再帰的に走査し、最後に現在のノードの右部分木を再帰的に走査します。先行順走査は、親ノードが子ノードよりも先に処理されるため、トポロジカルにソートされた走査となります。
順序通りの場合、常に現在のノードの左部分木を再帰的に走査し、次に現在のノードを訪れ、最後に現在のノードの右部分木を再帰的に走査します。
後順走査では、常に現在のノードの左部分木を再帰的に走査し、次に現在のノードの右部分木を再帰的に走査してから、現在のノードを訪れます。後順走査は、二分式木の後置式を取得するのに役立ちます。[ 34 ]
深さ優先探索では、常にルートノードから最も遠いノードを訪問しようとしますが、ただし、訪問したノードの子ノードである必要があります。グラフの深さ優先探索とは異なり、木構造にはサイクルが存在しないため、訪問したすべてのノードを記憶しておく必要はありません。先行順探索は、この特殊なケースです。詳細については、深さ優先探索を参照してください。
深さ優先探索とは対照的に、幅優先探索は、まだ訪問していないルートに最も近いノードを常に訪問しようとします。詳細については、幅優先探索を参照してください。レベル順走査とも呼ばれます。
完全二分木では、ノードの幅インデックス ( i − (2 d − 1)) をルートからの走査命令として使用できます。ビット d − 1 から左から右にビット単位で読み取ります。ここで d はノードのルートからの距離 ( d = ⌊log 2 ( i +1)⌋ ) であり、対象のノードはルート自体ではありません ( d > 0 )。幅インデックスがビット d − 1でマスクされている場合、ビット値0と1はそれぞれ左または右にステップすることを意味します。このプロセスは、右隣のビットを順にチェックして、それ以上チェックしなくなるまで続きます。一番右のビットは、目的のノードの親からノード自体への最終的な走査を示します。このように完全二分木を反復処理する場合と、各ノードが兄弟ノードへのポインタを持つ場合とでは、時間と空間のトレードオフがあります。
{{cite book}}: CS1 maint: 複数の名前: 著者リスト (リンク){{cite book}}: CS1 メンテナンス: その他 (リンク)