web-dev-qa-db-ja.com

ヒープを構築する方法 O(n) 時間の複雑さ?

誰かがヒープを構築する方法を説明するのを手助けすることができますか?O(n)複雑さ

項目をヒープに挿入するのはO(log n)です。挿入はn/2回繰り返されます(残りはリーフで、ヒーププロパティに違反することはありません)。だから、これは複雑さがO(n log n)であるべきであることを意味する、と私は思います。

言い換えれば、我々が "heapify"する各項目に対して、それまでのヒープの各レベル(log nレベル)に対して一度だけフィルタしなければならない可能性があります。

何が足りないの?

382
GBa

このトピックにはいくつかの質問が埋まっていると思います。

  • O(n)時間で実行されるようにbuildHeapをどのように実装しますか?
  • buildHeapが正しく実装されている場合、 O(n)時間で実行されることをどのように示しますか?
  • なぜ同じロジックが O(n log n)ではなく O(n)時間でヒープソートを実行するのに機能しないのですか?

O(n)時間で実行されるようにbuildHeapをどのように実装しますか?

多くの場合、これらの質問に対する答えは、siftUpsiftDownの違いに焦点を当てています。 siftUpsiftDownを正しく選択することは、buildHeap O(n)パフォーマンスを得るために重要ですが、一般的にbuildHeapheapSortの違いを理解する助けにはなりません。実際、buildHeapheapSortの両方の適切な実装では、onlysiftDownを使用します。 siftUp操作は、既存のヒープへの挿入を実行するためにのみ必要であるため、たとえば、バイナリヒープを使用して優先度キューを実装するために使用されます。

最大ヒープがどのように機能するかを説明するためにこれを書きました。これは、通常、ヒープの並べ替えまたは値が高いほど優先度が高いことを示す優先度キューに使用されるヒープのタイプです。最小ヒープも役立ちます。たとえば、昇順の整数キーまたはアルファベット順の文字列を持つアイテムを取得する場合。原則はまったく同じです。ソート順を切り替えるだけです。

heap propertyは、バイナリヒープの各ノードが少なくともその両方の子と同じ大きさでなければならないことを指定します。特に、これは、ヒープ内の最大のアイテムがルートにあることを意味します。ふるいにかけることとふるいにかけることは、本質的に反対方向の同じ操作です。ヒーププロパティを満たすまで、問題のノードを移動します。

  • siftDownは、その下の両方のノードと少なくとも同じ大きさになるまで、小さすぎるノードを最大の子と交換します(したがって、ノードを下に移動します)。
  • siftUpは、大きすぎるノードをその上位のノードよりも大きくならないまで親と交換します(それにより、ノードを上に移動します)。

siftDownおよびsiftUpに必要な操作の数は、ノードが移動しなければならない距離に比例します。 siftDownの場合、これはツリーの最下部までの距離であるため、siftDownはツリーの最上部のノードにとって高価です。 siftUpを使用すると、作業はツリーの最上部までの距離に比例するため、siftUpはツリーの最下部にあるノードにとってコストがかかります。両方の操作は O(log n)ですが、最悪の場合、ヒープでは1つのノードのみが最上位にあり、半分のノードは最下層にあります。したがって、すべてのノードに操作を適用する必要がある場合、siftDownよりもsiftUpを好むことは驚くべきことではありません。

buildHeap関数は、並べ替えられていない項目の配列を受け取り、それらがすべてヒーププロパティを満たすまでそれらを移動し、有効なヒープを生成します。説明したbuildHeapおよびsiftUp操作を使用して、siftDownに対して行うことができる2つのアプローチがあります。

  1. ヒープの先頭(配列の先頭)から開始し、各項目でsiftUpを呼び出します。各ステップで、以前にふるいにかけられたアイテム(配列内の現在のアイテムの前のアイテム)が有効なヒープを形成し、次のアイテムをふるいにかけると、ヒープ内の有効な位置に配置されます。各ノードをふるいにかけた後、すべてのアイテムはヒーププロパティを満たします。

  2. または、反対方向に移動します。配列の最後から開始し、前方に向かって後方に移動します。各反復で、アイテムが正しい位置にくるまでアイテムをふるいにかけます。

buildHeapのどの実装がより効率的ですか?

これらのソリューションはどちらも有効なヒープを生成します。当然のことながら、より効率的なのは、siftDownを使用する2番目の操作です。

h = log n がヒープの高さを表すとします。 siftDownアプローチに必要な作業は、合計によって与えられます

(0 * n/2) + (1 * n/4) + (2 * n/8) + ... + (h * 1).

合計の各項には、指定された高さのノードが移動しなければならない最大距離(最下層の場合はゼロ、ルートの場合はh)にその高さのノードの数を掛けます。対照的に、各ノードでsiftUpを呼び出す合計は

(h * n/2) + ((h-1) * n/4) + ((h-2)*n/8) + ... + (0 * 1).

2番目の合計が大きいことは明らかです。最初の項だけが hn/2 = 1/2 n log n であるため、このアプローチはせいぜい O(n log n)

siftDownアプローチの合計が実際に O(n)であることをどのように証明しますか?

1つの方法(他にも有効な分析があります)は、有限和を無限級数に変換してから、テイラー級数を使用することです。ゼロである最初の項を無視できます:

Taylor series for buildHeap complexity

これらの各手順がなぜ機能するのかわからない場合は、プロセスの正当な理由を言葉で示します。

  • 項はすべて正であるため、有限和は無限和よりも小さくなければなりません。
  • シリーズは、 x = 1/2 で評価されるべき級数に等しい。
  • そのべき級数は(一定の時間) f(x)= 1 /(1-x)のテイラー級数の導関数に等しい。
  • x = 1/2 は、そのテイラー級数の収束区間内にあります。
  • したがって、テイラー級数を 1 /(1-x)で置き換え、微分し、評価して無限級数の値を見つけることができます。

無限の合計は正確に n であるため、有限の合計はこれ以上大きくなく、したがって O(n)であると結論付けます。

ヒープソートに O(n log n)時間が必要なのはなぜですか?

buildHeapを線形時間で実行できる場合、ヒープソートで O(n log n) timeが必要なのはなぜですか?ヒープソートは2つの段階で構成されています。まず、配列でbuildHeapを呼び出します。最適に実装する場合は、 O(n)時間を必要とします。次の段階では、ヒープ内の最大のアイテムを繰り返し削除し、配列の最後に配置します。ヒープからアイテムを削除するため、ヒープの終了直後には常にアイテムを格納できる空きスペースがあります。したがって、ヒープソートは、次に大きいアイテムを連続して削除し、最後の位置から先頭に向かって配列に配置することで、ソートされた順序を実現します。この最後の部分の複雑さが、ヒープソートで支配的です。ループは次のようになります。

for (i = n - 1; i > 0; i--) {
    arr[i] = deleteMax();
}

明らかに、ループはO(n)回実行されます(正確には n-1 、最後のアイテムは既に配置されています)。ヒープのdeleteMaxの複雑さは O(log n)です。通常、ルート(ヒープ内に残っている最大のアイテム)を削除し、ヒープ内の最後のアイテムであるリーフ、つまり最小のアイテムの1つで置き換えることで実装されます。この新しいルートはほぼ確実にヒーププロパティに違反するため、許容される位置に戻すまでsiftDownを呼び出す必要があります。これには、次に大きいアイテムをルートまで移動する効果もあります。ほとんどのノードでツリーの下部からbuildHeapを呼び出しているsiftDownとは対照的に、各反復でツリーの上部からsiftDownを呼び出していることに注意してください。 ツリーは縮小していますが、十分に速く縮小しません:ノードの前半を削除するまで(最下層をクリアするとき)、ツリーの高さは一定のままです完全に)。次の四半期の高さは h-1 です。したがって、この第2段階の合計作業は

h*n/2 + (h-1)*n/4 + ... + 0 * 1.

スイッチに注目してください。ゼロワークケースは単一ノードに対応し、 h ワークケースはノードの半分に対応します。この合計は O(n log n)で、siftUpを使用して実装されるbuildHeapの非効率的なバージョンと同じです。しかし、この場合、ソートしようとしているため、次に大きなアイテムを削除する必要があるため、選択の余地はありません。

要約すると、ヒープソートの作業は、2つの段階の合計です。buildHeapの O(n)時間と、各ノードを削除するO(n log n) orderなので、複雑さはO(n log n)です。比較ベースのソートでは、 O(n log n)が最善であると(情報理論のいくつかのアイデアを使用して)証明できるので、とにかく理由はありませんこれに失望するか、buildHeapが行うO(n)の時間制限を達成するためにヒープの並べ替えを期待してください。

321
Jeremy West

あなたの分析は正しいです。しかし、それはタイトではありません。

ヒープの構築が線形操作である理由を説明するのは実際には簡単ではありません。もっと読むのがよいでしょう。

すばらしい分析 アルゴリズムの/ /を見ることができます ここ


主な考え方は、build_heapアルゴリズムでは、実際のheapifyコストがすべての要素に対してO(log n)ではないということです。

heapifyが呼び出されると、実行時間はプロセスが終了する前に要素がツリー内でどれだけ下に移動するかによって異なります。つまり、ヒープ内の要素の高さによって異なります。最悪の場合、要素は葉のレベルまでずっと下がるかもしれません。

作業レベルをレベルごとに数えましょう。

最下位レベルには2^(h)ノードがありますが、これらのいずれにもheapifyを呼び出さないので、作業は0です。レベルの隣には2^(h − 1)ノードがあり、それぞれ1レベル下に移動することがあります。下から3番目のレベルには2^(h − 2)ノードがあり、それぞれが2レベル下に移動する可能性があります。

すべてのheapify操作がO(log n)であるとは限らないので、これがO(n)を取得している理由です。

289

直感的に

「複雑さはO(nLog n)であるべきです...私たちが "heapify"した各項目について、これまでのヒープの各レベル(log nレベル)に対して一度フィルタリングしなければならない可能性があります。

かなりありません。あなたの論理は厳密な限界を生み出さない - それはそれぞれのヒープの複雑さを過大評価する。下から構築すると、挿入(heapify)はO(log(n))よりずっと少なくなります。プロセスは次のとおりです。

(ステップ1) 最初のn/2要素はヒープの一番下の行に移動します。 h=0なので、heapifyは必要ありません。

(ステップ2) 次のn/22要素は下から1行目に移動します。 h=1、1レベル下のフィルターをheapifyします。

(Step i 次のn/2i要素は、下から上へiの行に入ります。 h=i、heapifyはiレベルを下げます。

(Step log(n) 最後のn/2log2(n) = 1要素は、下から上の行log(n)に入ります。 h=log(n)、ヒープ化フィルターlog(n)のレベルを下げます。

注意: ステップ1の後、要素1/2(n/2)はすでにヒープにあり、一度heapifyを呼び出す必要さえありませんでした。また、1つの要素(ルート)だけが実際に完全なlog(n)の複雑さを被ります。


理論的には

サイズNのヒープを構築するための合計ステップnは、数学的に書き出すことができます。

高さiでは、heapifyを呼び出す必要があるn/2i+1要素があることを上記で示しました。高さiでのheapifyはO(i)であることがわかります。これは与える:

enter image description here

最後の総和の解は、よく知られている幾何級数方程式の両側の導関数を取ることによって見つけることができます。

enter image description here

最後に、x = 1/2を上記の式に代入すると、2が得られます。これを最初の式に代入すると、

enter image description here

したがって、ステップの総数はO(n)のサイズになります。

86
bcorso

繰り返し要素を挿入してヒープを構築した場合、O(n log n)になります。ただし、要素を任意の順序で挿入してから、それらを正しい順序に「ヒープ化」するアルゴリズムを適用することで、新しいヒープをより効率的に作成できます(もちろんヒープの種類によって異なります)。

例としては http://en.wikipedia.org/wiki/Binary_heap 、 "ヒープの構築"をご覧ください。この場合、基本的にツリーの最下位レベルから作業を進め、ヒープ条件が満たされるまで親ノードと子ノードを交換します。

33
mike__t

私たちが知っているように、ヒープの高さは log(n) です。ここで、nは要素の総数です。 h
heapify操作を実行すると、最後のレベルの要素( h )は1ステップでも移動しません。
最後の2番目のレベルの要素数( h-1 )は、 2 です。 h-1 そしてそれらはmax 1 レベルで移動できます(heapifyの間)。
同様に、 i について th 持っているレベル 2 i h - i 位置を移動できる要素.

したがって、移動の総数= _ s _ = 2 h * 0 + 2 h-1 * 1 + 2 h-2 * 2 + ... 2 0 * h

S = 2 h {1/2 + 2/2 2 + 3/2 3 + ... h/2 h ------------------------------------------------- 1
これは _ agp _ seriesです。これを解決するために、両側を2で割ってください。
S/2 = 2 h {1/2 2 + 2/2 3 + ... h/2 h + 1 } ------------------------------------------- ------ 2
1 から式 2 を引くと
S/2 = 2 h {1/2 + 1/2 2 + 1/2 3 + ... + 1/2 h + h/2 h + 1 }
S = 2 h + 1 {1/2 + 1/2 2 + 1/2 3 + ... + 1/2 h + h/2 h + 1 }
now 1/2 + 1/2 2 + 1/2 3 + ... + 1/2 h _ gp _ はその和が 1 より小さい(hが無限大になると、和は1になる)。さらに分析して、合計の上限を1としましょう。
これにより、 S = 2 が得られます。 h + 1 {1 + h/2 h + 1 }
= 2 h + 1 + h
〜2 h + h
h = log(n) 2 h = n

したがって、 S = n + log(n)
T(C)= O(n)

7
Tanuj Yadav

ヒープを構築しながら、ボトムアップアプローチを取っているとしましょう。

  1. 各要素を取り出してその子と比較し、ペアがヒープルールに準拠しているかどうかを確認します。そのため、葉は無料でヒープに含まれます。子供がいないからです。
  2. 上に移動すると、葉の真上にあるノードの最悪のシナリオは1回の比較になります(最大でも1世代の子と比較されます)。
  3. さらに上に移動すると、彼らの直接の両親は最大で2世代の子​​供と比較することができます。
  4. 同じ方向に進むと、最悪のシナリオではルートに対するlog(n)の比較が行われます。その直接の子供のためのlog(n)-1、その直接の子供のためのlog(n)-2など。
  5. それをまとめると、log(n)+ {log(n)-1} * 2 + {log(n)-2} * 4 + ..... + 1 * 2 ^ {()のようになります。 logn)-1}これはO(n)以外の何ものでもありません。
5
Jones

素晴らしい答えがいくつかありますが、視覚的な説明を少し加えたいと思います。

enter image description here

では、画像を見てください。
n/2^1緑のノードheight 0(ここでは23/2 = 12)
n/2^2赤のノードheight 1(ここでは23/4 = 6)
n/2^3height 2を持つ青いノード(ここでは23/8 = 3)
n/2^4紫色のノードheight 3(ここでは23/16 = 2)
したがって、高さhn/2^(h+1)ノードがあります
時間の複雑さを見つけるには、各ノードで実行されたwork of作業量またはmax回実行された反復回数を数えます。
これで、各ノードが(最大で)iterations ==ノードの高さを実行できることがわかります。

Green = n/2^1 * 0 (no iterations since no children)  
red   = n/2^2 * 1 (*heapify* will perform atmost one swap for each red node)  
blue  = n/2^3 * 2 (*heapify* will perform atmost two swaps for each blue node)  
purple = n/4^3 * 3  

したがって、高さhの最大作業量はn/2 ^(h + 1)* hです。

今やった総作業は

->(n/2^1 * 0) + (n/2^2 * 1)+ (n/2^3 * 2) + (n/2^4 * 3) +...+ (n/2^(h+1) * h)  
-> n * ( 0 + 1/4 + 2/8 + 3/16 +...+ h/2^(h+1) ) 

hの任意の値に対して、シーケンス

-> ( 0 + 1/4 + 2/8 + 3/16 +...+ h/2^(h+1) ) 

は1を超えることはありません
したがって、ヒープを構築するために時間の複雑さがO(n)を超えることはありません。

3
Julkar9

ヒープを構築する場合、高さ logn -1 から始めます(ここで、lognはn個の要素からなるツリーの高さです)。高さ 'h'に存在する各要素に対して、最大upto(logn -h)の高さまで下がります。

    So total number of traversal would be:-
    T(n) = sigma((2^(logn-h))*h) where h varies from 1 to logn
    T(n) = n((1/2)+(2/4)+(3/8)+.....+(logn/(2^logn)))
    T(n) = n*(sigma(x/(2^x))) where x varies from 1 to logn
     and according to the [sources][1]
    function in the bracket approaches to 2 at infinity.
    Hence T(n) ~ O(n)
2
Kartik Goyal

連続挿入は次のように記述できます。

T = O(log(1) + log(2) + .. + log(n)) = O(log(n!))

星形近似n! =~ O(n^(n + O(1)))、つまりT =~ O(nlog(n))

これが助けになることを願っていますが、O(n)が与えられたセットに対してbuild heapアルゴリズムを使うのが最適な方法です(順序は関係ありません)。

1
Tomer Shalev

O(n) の証明

証明は空想的ではなく、そして非常に簡単です。私は完全な二分木の場合を証明しただけで、結果は完全な二分木のために一般化することができます。

1
Yi Y

@ bcorsoは、複雑性分析の証明をすでに示しています。しかし、まだ複雑さの分析を学んでいる人のために、私はこれを付け加えます:

最初の間違いの根拠は、「ヒープへの挿入にO(log n)時間がかかる」という文の意味が誤って解釈されたためです。ヒープへの挿入は確かにO(log n)ですが、挿入中のnはヒープのサイズ であることを認識しておく必要があります

N個のオブジェクトをヒープに挿入するという文脈では、i番目の挿入の複雑さはO(log n_i)です。ここで、n_iは挿入iのときのようなヒープのサイズです。最後の挿入のみがO(log n)の複雑さを持ちます。

1
N.Vegeta

ヒープ内に _ n _ 要素があるとします。そのときの高さは Log(N) になります。

今度は別の要素を挿入したいと思います、そして複雑さは次のようになります: Log(N) 、私達はずっとルートと比較しなければなりません _ up _

N + 1 elements&height = Log(N + 1)

Induction の技法を使用すると、挿入の複雑さは Σlogi になることが証明できます。

今使っている

ログa +ログb =ログab

これにより、次のことが簡単になります。 ∑logi = log(n!)

これは実際には O(NlogN)

しかし

私達はここで間違ったことをしています、全てのケースで私達はトップに手が届かないので。したがって、ほとんどの場合、実行している間に、ツリーの途中まで進んでいません。さて、この限界は上記の答えで与えられた数学を使うことによってもう一つのより狭い限界を持つように最適化することができます。

この認識は、詳細とHeapsに関する実験の後に私にももたらされました。

0
Fooo

私は本当にJeremy westによる説明が好きです…本当に理解しやすい別のアプローチはここに与えられます http://courses.washington.edu/css343/zander/NotesProbs/heapcomplexity

なぜならbuildheapはheapifyに依存して使用しており、シフトダウンアプローチはすべてのノードの高さの合計に依存して使用されているからです。したがって、S =(2 ^ i *(hi))のi = 0からi = hまでの合計Sで与えられる節点の高さの合計を求めるには、h = lognはsを解く木の高さです。 s = 2 ^(h + 1) - 1 - (h + 1)である。なぜなら、n = 2 ^(h + 1) - 1 s = n - h - 1 = n − logn − 1 s = O(n)である。したがって、buildheapの複雑さはO(n)です。

0
Nitish Jain

"ビルドHeapの線形時間範囲は、ヒープ内のすべてのノードの高さの合計を計算することによって示すことができます。これは破線の最大数です。N = 2 ^を含む高さhの完全な2分木h + 1) - 1ノードでは、ノードの高さの合計はN - H - 1です。したがって、それはO(N)です。 "

0
sec3

基本的に、作業はヒープを構築している間、リーフ以外のノードでのみ行われます。行われる作業は、ヒープ条件を満たすためにスワップダウンする量です。この問題のすべての複雑さにおいて、すべての非リーフノードの高さの合計に比例します。(2 ^ h + 1 - 1)-h-1 = nh-1 =に)

0
Shubham Jindal