私は二分木の特定のアプリケーションが何であるか疑問に思います。実例を挙げてください。
binary-trees のパフォーマンスについて議論するのは無意味です - それらはデータ構造ではなく、データ構造のファミリーで、すべて異なるパフォーマンス特性を持っています。 不均衡な二分木が検索のための自己均衡二分木よりもはるかに悪いパフォーマンスをすることは事実ですが、多くの二分木があります(バイナリ試行など) "balance" には意味がありません。
map
やset
オブジェクトのように、データが常に出入りする多くの検索アプリケーションで使用されます。 。二分木が検索にn進木よりも頻繁に使用される理由は、n進木がより複雑であるが、通常は本当の速度上の利点を提供しないことである。
m
ノードを持つ(バランスのとれた)二分木では、あるレベルから次のレベルへ移動するには1回の比較が必要で、合計でlog_2(m)
の比較のためにlog_2(m)
レベルがあります。
これとは対照的に、n進ツリーでは次のレベルに移動するためにlog_2(n)
比較(二分探索を使用)が必要になります。 log_n(m)
totalレベルがあるので、検索にはlog_2(n)*log_n(m)
= log_2(m)
比較totalが必要です。そのため、n進木はより複雑ですが、必要な全体比較という点では利点がありません。
(ただし、n進木はまだニッチな状況では有用です。すぐに思い浮かぶ例は、 四分木 や他のスペース分割木です。レベルごとに2つのノードのみを使用してスペースを分割すると、ロジックが不必要に複雑になり、多くのデータベースで Bツリー が使用されます。一度に多くのノードをハードドライブからロードできます)
ほとんどの人が二分木について話すとき、彼らは二分検索木について考えないよりも頻繁に、それで私は最初にそれをカバーします。
バランスのとれていない二分探索木は、実際にはデータ構造について学生を教育する以外には役に立ちません。これは、データが比較的ランダムな順序で入ってこない限り、単純な二分木はではないので、ツリーはリンクリストである最悪の場合の形式に容易に縮退する可能性があるためです。 )バランスの取れた。
その好例です。私はかつて、操作と検索のためにデータをバイナリツリーにロードしたソフトウェアを修正する必要がありました。それはソートされた形式でデータを書き出しました:
Alice
Bob
Chloe
David
Edwina
Frank
それで、それを読み返すときに、次の木になったように:
Alice
/ \
= Bob
/ \
= Chloe
/ \
= David
/ \
= Edwina
/ \
= Frank
/ \
= =
これは縮退形式です。あなたがその木の中でフランクを探しに行くなら、あなたは彼を見つける前にあなたは6つのノードすべてを探さなければならないでしょう。
二分木は、バランスが取れているときに検索に本当に役立ちます。これは、任意の2つのサブツリー間の高さの差が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(N
ではなくO(log N)
であるバランスのとれた二分木になります。 。決して、最高のNULL(=
)が最低のものと2つ以上のレベルで異なることはありません。そして、上記の最後のツリーでは、3つのノード(Chloe
、Edwina
、そして最後にFrank
)を見るだけでFrankを見つけることができます。
もちろん、二分木ではなくバランスの取れた多方向木にすると、さらに便利になります。つまり、各ノードは複数のアイテムを保持します(技術的には、N個のアイテムとN + 1個のポインターを保持します。バイナリツリーは、1個のアイテムと2個のポインターを持つ1方向多方向ツリーの特別な場合です)。
三方木では、次のようになります。
Alice Bob Chloe
/ | | \
= = = David Edwina Frank
/ | | \
= = = =
これは通常、アイテムのインデックスのキーを管理する際に使用されます。私は、ノードが正確にディスクブロックのサイズ(たとえば512バイト)であるハードウェア用に最適化されたデータベースソフトウェアを書きました。そして、あなたはできるだけ多くのキーを単一のノードに入れます。この場合のポインタは、実際にはインデックスファイルとは別の固定長レコード直接アクセスファイルへのレコード番号でした(したがって、レコード番号X
は、単にX * record_length
を探すことによって見つけることができます)。 ).
たとえば、ポインタが4バイトでキーサイズが10の場合、512バイトのノードのキー数は36です。これは、36キー(360バイト)と37ポインタ(148バイト)で合計508バイトの場合です。ノードあたり4バイトが無駄になります。
多方向キーを使用すると、2フェーズ検索(ノード内の正しいキーを検索するための小規模な順次(または線形2進)検索と組み合わせた正しいノードを検索するためのマルチウェイ検索)が複雑になります。これを補うよりも少ないディスクI/Oを実行します。
インメモリ構造のためにこれをする理由はないと思います、あなたはバランスのとれた二分木に固執して、あなたのコードを単純にしておくほうが得策です。
また、O(log N)
に対するO(N)
の利点は、データセットが小さいときには実際には現れないことにも注意してください。アドレス帳に15人の人々を保存するために多方向ツリーを使用している場合、おそらくやり過ぎです。過去10年間に10万人の顧客からのすべての注文のようなものを保管していると、利点が得られます。
Big-O表記の目的は、N
が無限大に近づくにつれて何が起こるかを示すことです。そうでない人もいるかもしれませんが、他のものがすぐに利用可能でない限り、データセットが特定のサイズ以下に留まることが確実な場合は、バブルソートを使用しても問題ありません。
二分木の他の用途に関しては、次のような非常に多くのものがあります。
私が探索木についてどれだけの説明を生成したかを考えると、私は他のものについて多くの詳細に入ることは惜しいです、しかしそれはあなたが望むならそれらを調査するのに十分であるべきです。
バイナリツリーは、各ノードが最大で2つの子ノードを持つツリーデータ構造で、通常は "left"と "right"として区別されます。子を持つノードは親ノードであり、子ノードはそれらの親への参照を含むことができます。ツリーの外側には、「ルート」ノード(すべてのノードの祖先)への参照が存在する場合は、それがしばしばあります。データ構造内の任意のノードには、ルートノードから始めて、左または右の子への参照を繰り返し続けることによって到達できます。二分木では、各ノードの次数は最大2です。
あなたが木の中でノードを見つけたいのであれば、あなたが写真で見ることができるように、二分木は便利です、あなたは最大6回見る必要があるだけです。たとえば、ノード24を検索したい場合は、ルートから始めます。
この検索は以下のとおりです。
最初のパスでツリー全体のノードの半分を除外できることがわかります。そして2番目の左側のサブツリーの半分。これは非常に効果的な検索になります。これが410億要素に対して行われた場合、最大32回検索するだけで済みます。したがって、ツリーに含まれる要素が多いほど、検索は効率的になります。
削除は複雑になる可能性があります。ノードに0個または1個の子がある場合、それは単に削除されるものを除外するためにいくつかのポインタを移動することの問題です。ただし、2つの子を持つノードを簡単に削除することはできません。だから私たちは近道をします。ノード19を削除したいとしましょう。
左右のポインタをどこに移動するかを決めるのは簡単ではないので、それを代用するものを見つけます。私たちは左のサブツリーに行き、私たちが行ける限り右に行きます。これにより、削除したいノードの次に大きな値が得られます。
これで、左右のポインタを除いて、18の内容すべてをコピーし、元の18ノードを削除します。
これらの画像を作成するために、私はAVLツリー、自己均衡ツリーを実装しました。その結果、どの時点でも、ツリーはリーフノード(子を持たないノード)間で最大1レベルの違いがあります。これはツリーが歪むのを防ぎ、挿入と削除にもう少し時間がかかるという犠牲を払って、最大のO(log n)
検索時間を維持します。
これは、私のAVLツリーが可能な限りコンパクトでバランスのとれた状態を保つためのサンプルです。
ソートされた配列では、ツリーのようにルックアップは依然としてO(log(n))
を取りますが、ランダムな挿入と削除はツリーのO(log(n))
の代わりにO(n)を取ります。 STLコンテナの中には、これらのパフォーマンス特性を有利に利用しているため、挿入と削除にかかる時間が最大O(log n)
となるため、非常に高速です。これらのコンテナーのいくつかはmap
、multimap
、set
、およびmultiset
です。
AVLツリーのコード例は、 http://ideone.com/MheW8 にあります。
モールス符号 の構成は二分木です。
主な用途は 二分探索木 です。これらは、検索、挿入、削除がすべて非常に速いデータ構造です(log(n)
操作について)。
言及されていない二分木の興味深い例の1つは、再帰的に評価された数学的表現の例です。実用的な観点からは基本的に無用ですが、そのような表現を考えるのは興味深い方法です。
基本的に、ツリーの各ノードはそれ自身に固有の値か、またはその子の値を操作することによって再帰的に評価される値のどちらかを持ちます。
たとえば、式(1+3)*2
は次のように表現できます。
*
/ \
+ 2
/ \
1 3
式を評価するために、親の値を求めます。このノードは、その子、プラス演算子、および単に '2'を含むノードから値を取得します。次に、プラス演算子は、値が「1」と「3」の子からその値を取得し、それらを加算して、8を返す乗算ノードに4を返します。
この二分木の使用は、ある意味で操作を実行する順序が同一であるという点で、逆ポーランド記法に似ています。また注意すべきことは、それが必ずしも二分木である必要はないということです、それはただ最も一般的に使用される演算子が二項であるということです。最も基本的なレベルでは、ここのバイナリツリーは実際には非常に単純な純粋に機能的なプログラミング言語です。
最も一般的なアプリケーションの1つは、格納されている要素にすばやくアクセスして検索するために、ソートされた形式でデータを効率的に格納することです。たとえば、C++標準ライブラリのstd::map
またはstd::set
です。
データ構造としての二分木は、式パーサーや式ソルバーのさまざまな実装に役立ちます。
データベースの問題のいくつか、たとえば索引付けを解決するためにも使用できます。
一般に、二分木は特定の木ベースのデータ構造の一般的概念であり、様々な特定タイプの二分木を異なる特性で構成することができる。
私は「純粋な」二分木のための使用があるとは思わない。 赤黒木 や AVL木 のようなバランスのとれた二分木は、O(logn)操作。通常の二分木は、リスト(またはほとんどリスト)になる可能性があり、大量のデータを使用するアプリケーションでは実際には役に立ちません。
バランスの取れたツリーは、マップまたはセットを実装するためによく使用されます。それらをO(nlogn)でソートするために使用することもできますが、それを実行するためのより良い方法があります。
検索/挿入/削除 にもハッシュテーブル を使うことができます。これは通常、バイナリ検索ツリーよりもパフォーマンスが優れています(バランスのとれたかどうか)。
(バランスのとれた)二分探索木が役立つアプリケーションは、検索/挿入/削除とソートが必要な場合です。バランスのとれた構築済みのツリーであれば、ソートはその場で実行できます(ほとんどの場合、再帰に必要なスタックスペースは無視されます)。それでもやはりO(nlogn)になりますが、定数係数は小さく、余分なスペースは必要ありません(新しい配列を除き、データを配列に格納する必要があると仮定します)。一方、ハッシュテーブルはソートできません(少なくとも直接ではありません)。
多分それらはまた何かをするためのある洗練されたアルゴリズムで有用である、しかしtbh何も私の頭に浮かぶにはならない。もっと見つけたら私の投稿を編集します。
F.e.のような他の木 B +ツリー はデータベースで広く使われています
C++ STL、およびJavaやC#など、他の言語の他の多くの標準ライブラリ。二分探索木は集合と写像を実行するために使われる。
それらはデータをソートするための素早い方法として使用することができます。 O(log(n))のバイナリサーチツリーにデータを挿入します。それからそれらを分類するために木を横断します。
あなたのプログラムの構文、あるいはそれに関しては自然言語のような他の多くのものは(必ずしもそうとは限らないが)二分木を使って構文解析することができます。
最近のハードウェアでは、悪いキャッシュとスペースの振る舞いのために、二分木はほぼ常に最適とは言えません。これは(半)バランスのとれた変種にも当てはまります。あなたがそれらを見つけた場合、それはパフォーマンスが重要ではない(あるいは比較機能によって支配されている)場所であるか、あるいは歴史的または無知の理由のためによりありそうです。
Java.util.Set
の実装