回答:
ヒープは、高レベルの要素が低レベルの要素よりも大きい(最大ヒープの場合)または小さい(最小ヒープの場合)ことを保証するだけですが、BSTは順序(「左」から「右」)を保証します。ソートされた要素が必要な場合は、BSTを使用します。ダンテはオタクではありません
ヒープはfindMin / findMax(O(1))で優れていますが、BSTはすべての検索(O(logN))で優れています。挿入は両方の構造に対してO(logN)です。findMin / findMax(優先順位関連など)のみに関心がある場合は、ヒープを使用します。すべてを並べ替えるには、BSTを使用します。
概要
Type BST (*) Heap
Insert average log(n) 1
Insert worst log(n) log(n) or n (***)
Find any worst log(n) n
Find max worst 1 (**) 1
Create worst n log(n) n
Delete worst log(n) log(n)
この表のすべての平均時間は、挿入を除いて最悪の時間と同じです。
*
:この答えのどこでも、BST == Balanced BST、unbalancedは漸近的に吸い込むため**
:この回答で説明されている些細な変更を使用する***
:log(n)
ポインタツリーヒープn
用、動的配列ヒープ用BSTに対するバイナリヒープの利点
バイナリヒープへの平均時間の挿入はO(1)
、BSTの場合ですO(log(n))
。これは、ヒープのキラー機能です。
フィボナッチヒープのO(1)
ように償却された(より強力な)ヒープに到達する他のヒープ、およびBrodalキューのような最悪のケースもありますが、非漸近的なパフォーマンスのために実用的ではない場合があります:https : //stackoverflow.com/questions/30782636 / are-fibonacci-heaps-or-brodal-queues-used-in-practice-anywhere
バイナリヒープは、動的配列またはポインターベースのツリーの最上部に効率的に実装できます。BSTはポインターベースのツリーのみです。そのため、時折のサイズ変更の遅延を許容できる場合は、ヒープに対して、よりスペース効率の良いアレイ実装を選択できます。
BSTの場合、バイナリヒープの作成はO(n)
最悪のケースO(n log(n))
です。
バイナリヒープに対するBSTの利点
任意の要素の検索はO(log(n))
です。これはBSTのキラー機能です。
ヒープの場合O(n)
、最も大きい要素であるを除き、一般的にはO(1)
です。
BSTに対するヒープの「偽」の利点
ヒープはO(1)
、max、BSTを見つけることO(log(n))
です。
これはよくある誤解です。なぜなら、BSTを変更して最大の要素を追跡し、その要素が変更されるたびに更新するのは簡単だからです。https://stackoverflow.com/questions/7878622/can-we-use-binary-search-tree-to-simulate-heap-operation(Yeo による言及)。
実際、これはBSTと比較したヒープの制限です。唯一の効率的な検索は、最大要素の検索です。
平均バイナリヒープ挿入は O(1)
ソース:
直感的な議論:
バイナリヒープでは、特定のインデックスで値を増やすこともO(1)
同じ理由です。しかし、それをしたい場合は、ヒープ操作に関する追加のインデックスを最新に保ちたいと思う可能性が高いですhttps://stackoverflow.com/questions/17009056/how-to-implement-ologn-decrease-たとえば、ダイクストラの場合、最小ヒープベースの優先度キューのキー操作。追加費用なしで可能です。
実際のハードウェアでのGCC C ++標準ライブラリ挿入ベンチマーク
C ++ std::set
(赤黒木BST)およびstd::priority_queue
(動的配列ヒープ)挿入のベンチマークを行い、挿入時間について正しいかどうかを確認しました。
だから明らかに:
ヒープ挿入時間は基本的に一定です。
動的配列のサイズ変更ポイントを明確に見ることができます。システムノイズを超えてすべてを見ることができるように 10k挿入ごとに平均化するため、これらのピークは実際には表示されているものの約10倍です!
ズームされたグラフは、基本的に配列のサイズ変更ポイントのみを除外し、ほとんどすべての挿入が25ナノ秒未満であることを示しています。
BSTは対数です。すべての挿入は、平均的なヒープ挿入よりもはるかに遅いです。
BSTとハッシュマップの詳細な分析:https : //stackoverflow.com/questions/18414579/what-data-structure-is-inside-stdmap-in-c/51945119#51945119
gem5のGCC C ++標準ライブラリ挿入ベンチマーク
gem5は完全なシステムシミュレータであるため、で無限に正確なクロックを提供しm5 dumpstats
ます。そこで、それを使用して個々の挿入のタイミングを推定しようとしました。
解釈:
ヒープはまだ一定ですが、ここで詳細に見ると、いくつかの行があり、各上位行はより疎です。
これは、メモリアクセスのレイテンシに対応する必要があります。
TODO BSTを完全に解釈することはできません。BSTは対数的に見えず、多少一定しているためです。
しかし、この詳細を見ると、いくつかの明確な行も見ることができますが、それらが何を表しているのかわかりません。
aarch64 HPI CPU上のこのBuildrootセットアップでベンチマークされています。
BSTはアレイに効率的に実装できません
ヒープ操作は、単一のツリーブランチをバブルアップまたはダウンするだけでよいため、O(log(n))
最悪の場合のスワップはO(1)
平均的です。
BSTのバランスを保つには、ツリーの回転が必要です。これにより、最上部の要素を別の要素に変更できますO(n)
。また、配列全体を移動する必要があります()。
ヒープをアレイに効率的に実装できます
ここに示すように、現在のインデックスから親インデックスと子インデックスを計算できます。
BSTのようなバランシング操作はありません。
Delete minはトップダウンである必要があるため、最も心配な操作です。ただし、ここで説明するように、ヒープの1つのブランチを「パーコレート」することで常に実行できます。ヒープは常にバランスがとれているため、これはO(log(n))最悪のケースにつながります。
削除するノードごとに1つのノードを挿入する場合、削除が支配するため、ヒープが提供する漸近的なO(1)平均挿入の利点を失い、BSTを使用することもできます。ただし、Dijkstraは削除するたびにノードを数回更新するため、問題ありません。
動的配列ヒープとポインターツリーヒープ
ポインターヒープの上にヒープを効率的に実装できます:https : //stackoverflow.com/questions/19720438/is-it-possible-to-make-efficient-pointer-based-binary-heap-implementations
動的配列の実装は、スペース効率がより高くなります。各ヒープ要素にaへのポインタのみが含まれているとしますstruct
。
ツリーの実装は、各要素に対して、親、左の子、右の子の3つのポインターを格納する必要があります。したがって、メモリ使用量は常に4n
(3ツリーポインター+ 1 struct
ポインター)です。
ツリーBSTには、さらにバランスの取れた情報(黒赤など)も必要です。
動的配列の実装は2n
、倍増直後のサイズにすることができます。ですから、平均してそうなるでしょう1.5n
。
一方、バッキングダイナミックアレイをコピーしてサイズを2倍にするのはO(n)
最悪のケースですが、ツリーヒープはノードごとに新しい小さな割り当てを行うため、ツリーヒープにはワーストケースの挿入が適しています。
それでも、バッキングアレイの倍増はO(1)
償却されるため、最大遅延の考慮事項になります。ここで言及しました。
哲学
BSTは、親とすべての子孫の間でグローバルプロパティを維持します(左に小さく、右に大きく)。
BSTの最上位ノードは中央の要素であり、これを維持するにはグローバルな知識が必要です(小さい要素と大きい要素がいくつあるかを知る)。
このグローバルプロパティは、メンテナンス(log n insert)がより高価ですが、より強力な検索(log n search)を提供します。
ヒープは、親と直接の子(親>子)の間のローカルプロパティを維持します。
ヒープの一番上にあるのは大きな要素です。これは、ローカルの知識(親を知ること)のみを必要とします。
二重リンクリスト
二重にリンクされたリストは、最初の項目が最も優先されるヒープのサブセットと見なすことができるので、ここでも比較してみましょう。
O(1)
アイテムへのポインタがあるため、最悪の場合、更新は本当に簡単ですO(1)
平均、したがってリンクリストよりも悪い。より一般的な挿入位置を持つためのトレードオフ。O(n)
両方のこの使用例は、ヒープのキーが現在のタイムスタンプである場合です。その場合、新しいエントリは常にリストの先頭に移動します。したがって、正確なタイムスタンプを完全に忘れて、リスト内の位置を優先順位として保持することさえできます。
これを使用して、LRUキャッシュを実装できます。同じようにダイクストラのようなヒープのアプリケーションのための、あなたはすぐに更新するノードを見つけるために、リストの対応するノードの鍵から追加HashMapを維持したいと思うでしょう。
異なるバランスBSTの比較
これまで見てきた「バランスの取れたBST」として一般的に分類されるすべてのデータ構造の漸近挿入と検索時間は同じですが、BBSTによってトレードオフが異なります。私はまだこれを十分に研究していませんが、これらのトレードオフをここにまとめるのは良いことです:
こちらもご覧ください
CSに関する同様の質問:バイナリ検索ツリーとバイナリヒープの違いは何ですか?
データ構造では、懸念のレベルを区別する必要があります。
この質問の抽象データ構造(保存されているオブジェクト、それらの操作)は異なります。1つは優先度キューを実装し、もう1つはセットを実装します。優先度キューは、任意の要素の検索には関心がなく、最大の優先度を持つ要素のみを検索します。
構造の具体的な実装。ここでは、どちらも(バイナリ)ツリーですが、構造的な特性は異なります。キーの相対的な順序と可能なグローバル構造の両方が異なります。(多少不正確、BST
キーでは左から右に、ヒープではトップダウンで順序付けられます。)IPlantが正しく述べているように、ヒープも「完全」でなければなりません。
低レベルの実装には最終的な違いがあります。(不均衡な)バイナリ検索ツリーには、ポインターを使用した標準実装があります。反対に、バイナリヒープには、配列を使用した効率的な実装があります(正確に構造が制限されているため)。