web-dev-qa-db-ja.com

ヒープとバイナリ検索ツリー(BST)

ヒープとBSTの違いは何ですか?

ヒープを使用する場合とBSTを使用する場合

ソートされた方法で要素を取得したい場合、BSTはヒープよりも優れていますか?

150
kc3

概要

          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 ==バランスの取れたBST。
  • **:この回答で説明されている些細な変更を使用する
  • ***log(n)ポインターツリーヒープ用、n動的配列ヒープ用

BST上のバイナリヒープの利点

  • バイナリヒープへの平均時間挿入はO(1)です。BSTはO(log(n))です。 これはヒープのキラー機能です。

    フィボナッチヒープ などのO(1)償却(より強力な)に達するヒープ、および Brodalキュー などの最悪の場合もありますが、そうではない場合もあります非漸近的なパフォーマンスのため実用的: フィボナッチヒープまたはブロダルキューは実際にどこでも使用されていますか?

  • バイナリヒープは、 動的配列 またはポインターベースのツリー(BSTのみのポインターベースのツリー)の上に効率的に実装できます。そのため、時折のサイズ変更の遅延を許容できる場合は、ヒープに対して、よりスペース効率の良いアレイ実装を選択できます。

  • バイナリヒープ作成 O(n)最悪の場合 、BSTの場合はO(n log(n)).

バイナリヒープ上のBSTの利点

  • 任意の要素の検索はO(log(n))です。 これはBSTのキラー機能です。

    ヒープについては、O(n)である最大の要素を除いて、一般にO(1)です。

「偽」のBSTよりもヒープの利点

平均バイナリヒープ挿入はO(1)

ソース:

直感的な議論:

  • 最下位ツリーレベルには最上位レベルよりも指数関数的に多くの要素があるため、新しい要素はほとんど確実に最下位に移動します
  • ヒープ挿入 下から開始 、BSTは上から開始する必要があります

バイナリヒープでは、同じ理由で、特定のインデックスで値を増やすこともO(1)です。ただし、それを行う場合は、ヒープ操作に関する追加のインデックスを最新の状態に保ちたいと思う可能性があります O(logn)ヒープベースの優先度キュー? 例ダイクストラ。追加費用なしで可能。

実際のハードウェアでのGCC C++標準ライブラリ挿入ベンチマーク

C++ std::set赤黒木BST )およびstd::priority_queue動的配列ヒープ )挿入のベンチマークを行って、正しいかどうかを確認しました時間を挿入し、これは私が得たものです:

enter image description here

  • ベンチマークコード
  • プロットスクリプト
  • プロットデータ
  • ubuntu 19.04、GCC 8.3.0、CPUを搭載したLenovo ThinkPad P51ラップトップでテスト済み:Intel Core i7-7820HQ CPU(4コア/ 8スレッド、2.90 GHzベース、8 MBキャッシュ)、RAM:Samsung M471A2K43BB1-CRC(2x 16GiB) 、2400 Mbps)、SSD:Samsung MZVLB512HAJQ-000L7(512GB、3,000 MB/s)

だから明らかに:

  • ヒープ挿入時間は基本的に一定です。

    動的な配列のサイズ変更ポイントを明確に見ることができます。 10kの挿入ごとに平均化するため システムノイズを超えてすべてを見ることができるように であるため、これらのピークは実際には表示されているものの約1万倍です!

    ズームされたグラフは、本質的に配列のサイズ変更ポイントのみを除外し、ほとんどすべての挿入が25ナノ秒未満であることを示しています。

  • BSTは対数です。すべての挿入は、平均的なヒープ挿入よりもはるかに遅いです。

  • BSTとハッシュマップの詳細な分析: C++のstd :: map内のデータ構造は?

gem5のGCC C++標準ライブラリ挿入ベンチマーク

gem5 は完全なシステムシミュレータであるため、m5 dumpstatsで無限に正確なクロックを提供します。そこで、それを使用して個々の挿入のタイミングを推定しようとしました。

enter image description here

解釈:

  • ヒープはまだ一定ですが、ここで詳細に見ると、いくつかの行があり、それぞれの上位行がより疎です。

    これは、より高い挿入で行われるメモリアクセスのレイテンシに対応する必要があります。

  • TODO BSTを完全に解釈することはできません。BSTが対数的に見えず、多少一定しているためです。

    ただし、この詳細を見ると、いくつかの明確な行も表示されますが、それらが何を表しているのかわかりません。上から下に挿入するので、下の行が細くなると思いますか?

これでベンチマーク Buildroot setup aarch64 HPI CP で。

BSTはアレイに効率的に実装できません

ヒープ操作は、単一のツリーブランチをバブルアップまたはダウンするだけでよいため、O(log(n))最悪の場合のスワップ、O(1)の平均。

BSTのバランスを保つには、ツリーの回転が必要です。これにより、最上部の要素を別の要素に変更できます。また、配列全体を移動する必要があります(O(n))。

ヒープを配列に効率的に実装できます

親インデックスと子インデックスは、現在のインデックスから計算できます ここに示すように

BSTのようなバランシング操作はありません。

Delete minは、トップダウンである必要があるため、最も心配な操作です。しかし、ヒープの単一ブランチを「パーコレート」することで常に実行できます ここで説明したように 。これは、ヒープのバランスが常に取れているため、O(log(n))最悪の場合につながります。

削除するノードごとに1つのノードを挿入する場合、削除が支配するためヒープが提供する漸近的なO(1)平均挿入の利点を失い、BSTを使用することもできます。ただし、Dijkstraは削除するたびにノードを数回更新するため、問題ありません。

動的配列ヒープとポインタツリーヒープ

ヒープは、ポインターヒープの上に効率的に実装できます。 効率的なポインターベースのバイナリヒープ実装を行うことは可能ですか?

動的配列の実装は、スペース効率がより高くなります。各ヒープ要素に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キャッシュ を実装できます。 Dijkstraのようなヒープアプリケーションの場合 のように、キーからリストの対応するノードへの追加のハッシュマップを保持して、どのノードを迅速に更新するかを見つけます。

こちらもご覧ください

CSに関する同様の質問: https://cs.stackexchange.com/questions/27860/whats-the-difference-between-a-binary-search-tree-and-a-binary-heap

ヒープは、高レベルの要素が低レベルの要素よりも大きい(最大ヒープの場合)または小さい(最小ヒープの場合)ことを保証するだけですが、BSTは順序(「左」から「右」)を保証します。ソートされた要素が必要な場合は、BSTを使用します。

73
Dante May Code

ヒープを使用する場合とBSTを使用する場合

ヒープはfindMin/findMax(O(1))で優れていますが、BSTはallの検索(O(logN))で優れています。挿入は、両方の構造に対してO(logN)です。 findMin/findMax(優先順位関連など)のみを重視する場合は、ヒープを使用します。すべてを並べ替えるには、BSTを使用します。

here の最初のいくつかのスライドは、物事を非常に明確に説明しています。

47
xysun

他の人が述べたように、ヒープはO(1)でfindMinorfindMaxを実行できますが、同じデータ構造で両方を実行することはできません。しかし、ヒープがfindMin/findMaxで優れていることに同意しません。実際、わずかな変更で、BSTはbothfindMinandfindMax in O(1)。

この変更されたBSTでは、データ構造を潜在的に変更できる操作を行うたびに、最小ノードと最大ノードを追跡します。たとえば、挿入操作では、最小値が新しく挿入された値よりも大きいかどうかを確認し、最小値を新しく追加されたノードに割り当てることができます。同じ手法を最大値に適用できます。したがって、このBSTにはO(1)で取得できるこれらの情報が含まれています。 (バイナリヒープと同じ)

このBST(バランスBST)では、pop minまたはpop maxを使用する場合、次に割り当てられる最小値は、最小ノードのsuccessorです。割り当てられる次の最大値は、最大ノードのpredecessorです。したがって、O(1)で実行されます。ただし、ツリーのバランスを再調整する必要があるため、引き続きO(log n)が実行されます。 (バイナリヒープと同じ)

以下のコメントであなたの考えを聞いてみたいです。ありがとう:)

更新

同様の質問への相互参照 バイナリ検索ツリーを使用してヒープ操作をシミュレートできますか? BSTを使用してヒープをシミュレートする詳細については.

7
Yeo

ヒープ上のBSTの別の使用法。重要な違いのため:

  • bSTで後続および先行を見つけるには、O(h)時間かかります。 (バランスBSTのO(logn))
  • ヒープ内では、O(n)時間を要して、ある要素の後続または先行を検索します。

ヒープ上でのBSTの使用:では、データ構造を使用してフライトの着陸時刻を保存するとしましょう。着陸時間の差が「d」より小さい場合、着陸するフライトをスケジュールできません。また、多くのフライトがデータ構造(BSTまたはヒープ)に着陸するようにスケジュールされているとします。

ここで、tに着陸する別のフライトをスケジュールします。したがって、tとその後続および先行(> d)の差を計算する必要があります。 したがって、このためにBSTが必要になります。これは高速に実行しますie in O(logn)バランスが取れている場合。

編集済み:

並べ替え BSTは、ソートされた順序で要素を出力するのにO(n)時間(順序走査)を要しますが、ヒープはO(n logn)時間でそれを行います。ヒープはmin要素を抽出し、配列を再ヒープ化します。これにより、O(n logn)時間で並べ替えが行われます。

3
CODError

バイナリ検索ツリーは定義を使用します。つまり、すべてのノードについて、その左側のノードの値(キー)は小さく、右側のノードの値(キー)は大きくなります。

ヒープとして、バイナリツリーの実装は次の定義を使用します。

AとBがノードで、BがAの子ノードである場合、Aの値(キー)はBの値(キー)以上でなければなりません。つまり、key(A)≥key(B )。

http://wiki.answers.com/Q/Difference_between_binary_search_tree_and_heap_tree

私は今日、試験のために同じ質問に出くわし、正解しました。笑顔... :)

配列からn個の要素をすべてBSTに挿入すると、O(n logn)がかかります。配列内のn個の要素は、O(n)時間でヒープに挿入できます。ヒープに明確な利点があります

1
AMR

ヒープは、高レベルの要素が低レベルの要素よりも大きい(最大ヒープの場合)または小さいこと(最小ヒープの場合)を保証するだけです。

私は上記の答えが大好きで、自分のニーズと使用法にさらに具体的なコメントを入れています。 n個のロケーションリストを取得して、各ロケーションから特定のポイント(0,0)までの距離を見つけてから、より小さな距離を持つa m個のロケーションを返す必要がありました。ヒープである優先度キューを使用しました。距離を見つけてヒープに入れるには、挿入ごとにn(log(n)) n個の場所log(n)が必要でした。次に、最短距離でmを取得するために、m(log(n)) m-locations log(n)のヒープの削除が必要でした。

私はこれをBSTで行う必要がある場合、n(n)最悪の場合の挿入が必要になります。(最初の値が非常に小さく、他のすべてが順次長くなり、ツリーが右側の子のみ、または小さい場合は左側の子。最小時間はO(1)時間かかりましたが、再びバランスを取る必要がありました。最小または最大優先度ベースの値がヒープに移行した後にのみです。

0
Sahib Khan