回答:
バイナリツリーのパフォーマンスについて論じることは意味がありません。それらはデータ構造ではなく、すべて異なるパフォーマンス特性を持つデータ構造のファミリーです。不均衡な二分木は、検索のための自己均衡二分木よりもパフォーマンスがはるかに悪いことは事実ですが、「均衡」が意味を持たない多くの二分木(二分試行など)があります。
map
set
二分木がn-aryツリーよりも頻繁に検索に使用される理由は、n-aryツリーはより複雑ですが、通常、実際の速度上の利点はありません。
m
ノードのある(バランスのとれた)バイナリツリーでは、1つのレベルから次のレベルに移動するには1つの比較が必要でありlog_2(m)
、log_2(m)
比較の合計にはレベルがあります。
対照的に、n分木は次のレベルに移動するlog_2(n)
ために(二分探索を使用した)比較が必要になります。あるのでlog_n(m)
、合計レベルは、検索が必要になりますlog_2(n)*log_n(m)
= log_2(m)
比較は合計します。したがって、n-aryツリーはより複雑ですが、必要な合計比較の点で利点はありません。
(ただし、n-aryツリーはニッチな状況でも依然として有用です。すぐに頭に浮かぶ例は、クワッドツリーおよびその他のスペース分割ツリーです。レベルごとに2つのノードのみを使用してスペースを分割すると、ロジックが不必要に複雑になります。多くのデータベースで使用されるBツリー。制限要因は、各レベルで実行される比較の数ではなく、ハードドライブから一度にロードできるノードの数です。
ほとんどの人が二分木について話すとき、彼らは二分探索木について考えるよりも頻繁であるので、最初にそれをカバーします。
非平衡型の二分探索木は、データ構造について学生を教育することよりも実際に役立ちます。これは、データが比較的ランダムな順序で到着しない限り、単純なバイナリツリーのバランスが取れていないため、ツリーがリンクリストであるワーストケースの形式に簡単に縮退する可能性があるためです。
適切な事例として、操作や検索のためにデータをバイナリツリーに読み込むソフトウェアを修正する必要がありました。ソートされた形式でデータを書き出しました。
Alice
Bob
Chloe
David
Edwina
Frank
読み返すと、次のようなツリーになります。
Alice
/ \
= Bob
/ \
= Chloe
/ \
= David
/ \
= Edwina
/ \
= Frank
/ \
= =
これは退化した形式です。そのツリーでフランクを探しに行く場合、彼を見つける前に6つすべてのノードを検索する必要があります。
二分木は、バランスをとると、検索に非常に役立ちます。これには、ルートノードを介してサブツリーを回転させて、2つのサブツリー間の高さの差が1以下になるようにします。バランスツリーに一度に1つ以上の名前を追加すると、次のシーケンスが得られます。
1. Alice
/ \
= =
2. Alice
/ \
= Bob
/ \
= =
3. Bob
_/ \_
Alice Chloe
/ \ / \
= = = =
4. Bob
_/ \_
Alice Chloe
/ \ / \
= = = David
/ \
= =
5. Bob
____/ \____
Alice David
/ \ / \
= = Chloe Edwina
/ \ / \
= = = =
6. Chloe
___/ \___
Bob Edwina
/ \ / \
Alice = David Frank
/ \ / \ / \
= = = = = =
エントリが追加されると、サブツリー全体が左に回転して(ステップ3と6で)実際に見ることができます。これにより、退化フォームが与える)O(log N)
ではなく、最悪の場合のルックアップが行われるバランスのとれたバイナリツリーがO(N
得られます。最高のNULL(=
)が最低のレベルと2レベル以上異なることはありません。そして、上記の最後のツリーでは、3つのノード(Chloe
、Edwina
そして最後にFrank
)を見るだけでフランクを見つけることができます。
もちろん、二分木ではなく多方向ツリーのバランスをとると、さらに便利になります。つまり、各ノードは複数のアイテムを保持します(技術的には、NアイテムとN + 1ポインターを保持します。バイナリツリーは、1アイテムと2ポインターを持つ1ウェイマルチウェイツリーの特別なケースです)。
3方向ツリーでは、次のようになります。
Alice Bob Chloe
/ | | \
= = = David Edwina Frank
/ | | \
= = = =
これは通常、アイテムのインデックスのキーを維持するために使用されます。ノードがディスクブロックのサイズ(たとえば512バイト)であるハードウェア用に最適化されたデータベースソフトウェアを作成し、単一のノードにできるだけ多くのキーを配置しました。この場合のポインタは、実際には、インデックスファイルとは別の固定長レコードの直接アクセスファイルへのレコード番号X
でした(したがって、を検索するだけでレコード番号を見つけることができますX * record_length
)。
たとえば、ポインタが4バイトで、キーサイズが10の場合、512バイトノードのキーの数は36です。これは36キー(360バイト)と37ポインタ(148バイト)で、合計508バイトです。ノードごとに4バイトが無駄になります。
多方向キーを使用すると、2フェーズ検索(正しいノードを見つける多方向検索と小さなシーケンシャル(または線形バイナリ)検索を組み合わせてノード内の正しいキーを見つける)が複雑になりますが、これを補うよりも多くのディスクI / Oを実行します。
インメモリ構造でこれを行う理由はないと思います。バランスの取れたバイナリツリーを使い続け、コードをシンプルに保つ方が良いでしょう。
またの利点があることに注意してくださいO(log N)
以上がO(N)
あなたのデータセットが小さい場合、実際には表示されません。多方向ツリーを使用して15人をアドレス帳に保存している場合、多すぎるでしょう。過去10年間で10万人の顧客からのすべての注文のようなものを保管しているときに、利点が生まれます。
big-O表記の要点は、N
無限に近づくにつれて何が起こるかを示すことです。一部の人々は同意しないかもしれませんが、データセットが特定のサイズ未満に留まることが確実であり、他に何も容易に利用できない限り、バブルソートを使用しても大丈夫です:-)
二分木の他の用途としては、次のような多くのものがあります。
検索ツリーについて説明した説明の量を考えると、他のものについては詳しく説明するのは控えめですが、必要に応じて、それらを調査するには十分です。
二分木は、各ノードに最大2つの子ノードがあり、通常「左」と「右」として区別されるツリーデータ構造です。子を持つノードは親ノードであり、子ノードには親への参照が含まれる場合があります。ツリーの外には、「ルート」ノード(すべてのノードの祖先)が存在する場合、それへの参照があることがよくあります。データ構造のどのノードにも、ルートノードから開始して、左または右の子への参照を繰り返したどることで到達できます。バイナリツリーでは、すべてのノードの次数は最大2つです。
バイナリツリーは便利です。図からわかるように、ツリー内のノードを見つけたい場合は、最大6回見ればよいだけです。たとえば、ノード24を検索する場合は、ルートから始めます。
この検索を以下に示します。
最初のパスでツリー全体のノードの半分を除外できることがわかります。2番目の左側のサブツリーの半分。これは非常に効果的な検索になります。これが40億個の要素に対して行われた場合、最大32回検索するだけで済みます。したがって、ツリーに含まれる要素が多いほど、検索の効率が上がります。
削除は複雑になる可能性があります。ノードに0または1の子がある場合は、いくつかのポインターを移動して、削除するポインターを除外するだけです。ただし、2つの子を持つノードを簡単に削除することはできません。だから私たちは近道をします。ノード19を削除したいとしましょう。
左と右のポインターをどこに移動するかを決定するのは簡単ではないので、それを置き換えるポインターを見つけます。左側のサブツリーに行き、できる限り右に行きます。これにより、削除するノードの次に大きい値が得られます。
次に、左と右のポインタを除くすべての18のコンテンツをコピーし、元の18ノードを削除します。
これらのイメージを作成するために、私はAVLツリー、セルフバランシングツリーを実装しました。これにより、ツリーはいつでも、リーフノード(子のないノード)間で最大で1レベルの違いを持つようになります。これにより、ツリーが歪むのを防ぎ、最大のO(log n)
検索時間を維持しますが、挿入と削除に必要な時間が少し長くなります。
これは、私のAVLツリーが可能な限りコンパクトでバランスの取れた状態を維持する方法を示すサンプルです。
ソートされた配列ではO(log(n))
、ツリーと同様に、ルックアップにはが必要ですが、ランダムな挿入と削除には、ツリーのの代わりにO(n)が必要O(log(n))
です。一部のSTLコンテナーはこれらのパフォーマンス特性を有利に使用しているため、挿入と削除の時間は最大でO(log n)
となり、非常に高速です。これらのコンテナのいくつかはmap
、multimap
、set
、とmultiset
。
AVLツリーのサンプルコードは、http://ideone.com/MheW8にあります。
主なアプリケーションは、バイナリ検索ツリーです。これらは、検索、挿入、および削除がすべて非常に高速なデータ構造です(log(n)
操作について)。
言及されていないバイナリツリーの興味深い例の1つは、再帰的に評価される数式の例です。基本的には実用的ではありませんが、そういう表現を考えるのも面白いですね。
基本的に、ツリーの各ノードには、それ自体に固有の値、またはその子の値を操作して再帰的に評価される値があります。
たとえば、式(1+3)*2
は次のように表すことができます。
*
/ \
+ 2
/ \
1 3
式を評価するために、親の値を要求します。このノードは、子、プラス演算子、および「2」のみを含むノードから値を取得します。次に、plus演算子は、値「1」と「3」を持つ子から値を取得して加算し、4を乗算ノードに返し、8を返します。
バイナリツリーのこの使用法は、操作が実行される順序が同じであるという意味で、ポリッシュ表記を逆にしたものと似ています。また、注意する必要があるのは、必ずしもバイナリツリーである必要はなく、最も一般的に使用される演算子がバイナリであるということだけです。最も基本的なレベルでは、ここのバイナリツリーは実際には非常に単純な純粋に関数型のプログラミング言語です。
二分木のアプリケーション:
「純粋な」二分木の使用法はないと思います。(教育目的を除く)赤黒木やAVLツリーなどのバランスのとれた二分木 O(logn)操作を保証するため、はるかに便利です。通常のバイナリツリーは最終的にリスト(またはほとんどリスト)になる可能性があり、多くのデータを使用するアプリケーションでは実際には役立ちません。
バランスツリーは、マップまたはセットの実装によく使用されます。また、O(nlogn)での並べ替えにも使用できます。これを行うには、より良い方法があります。
検索/挿入/削除にも ハッシュテーブルを使用こともできます。これは通常、バイナリサーチツリーよりもパフォーマンスが優れています(バランスが取れているかどうかにかかわらず)。
(バランスのとれた)バイナリ検索ツリーが役立つアプリケーションは、検索、挿入、削除、並べ替えが必要な場合です。ソートは、準備が整ったビルドバランスツリーを前提として、インプレース(ほとんどの場合、再帰に必要なスタックスペースを無視)にすることができます。それはまだO(nlogn)ですが、定数係数は小さく、余分なスペースは必要ありません(新しい配列を除き、データを配列に入れる必要があると想定しています)。一方、ハッシュテーブルは(少なくとも直接では)ソートできません。
多分それらは何かをするためのいくつかの洗練されたアルゴリズムでも有用ですが、私の頭には何も思い浮かびません。もっと見つけたら、投稿を編集します。
fe B + treesのような他のツリーは、データベースで広く使用されています
最近のハードウェアでは、キャッシュとスペースの動作が悪いため、バイナリツリーはほとんど常に最適ではありません。これは(準)バランスのバリアントにも当てはまります。それらを見つけた場合、パフォーマンスが考慮されない(または比較機能によって支配される)か、または歴史的または無知の理由により可能性が高い場所です。