このトピックにはいくつかの疑問が潜んでいると思います。
- O(n)時間
buildHeap
で実行できるようにどのように実装しますか?
- 正しく実装されている場合
buildHeap
、O(n)時間で実行されることをどのように示しますか?
- ヒープソートをO(n log n)ではなくO(n)時間で実行するために同じロジックが機能しないのはなぜですか?
O(n)時間buildHeap
で実行できるようにどのように実装しますか?
多くの場合、これらの質問に対する答えは違いに焦点を当てるsiftUp
とsiftDown
。間に正しい選択をするsiftUp
とsiftDown
取得することが重要であるO(N)のパフォーマンスをbuildHeap
、しかし、1つの違いを理解する助けに何もしませんbuildHeap
し、heapSort
一般的に。実際、両方の適切な実装buildHeap
とheapSort
なりますのみ使用しますsiftDown
。このsiftUp
操作は、既存のヒープへの挿入を実行するためにのみ必要であるため、たとえば、バイナリヒープを使用して優先度キューを実装するために使用されます。
最大ヒープがどのように機能するかを説明するためにこれを書きました。これは、ヒープのソートまたは優先度キューに通常使用されるヒープのタイプであり、値が高いほど優先度が高いことを示します。最小ヒープも役立ちます。たとえば、昇順の整数キーまたはアルファベット順の文字列を持つアイテムを取得する場合などです。原則はまったく同じです。ソート順を切り替えるだけです。
ヒーププロパティバイナリヒープ内の各ノードは、少なくともその子の両方として大きいようでなければならないことを指定します。特に、これはヒープ内の最大のアイテムがルートにあることを意味します。ふるいにかけることとふるいにかけることは、本質的に反対の方向に同じ操作です。問題のあるノードを、ヒーププロパティを満たすまで移動します。
siftDown
小さすぎるノードをその最大の子と入れ替えます(その結果、ノードが下に移動します)。少なくとも、その下の両方のノードと同じ大きさになります。
siftUp
大きすぎるノードをその親ノードと入れ替えます(それにより、そのノードを上に移動します)。
操作の数のために必要siftDown
とsiftUp
ノードが移動する必要がある可能性があり、距離に比例しています。の場合siftDown
、これはツリーの最下部までの距離であるため、ツリーsiftDown
の最上位にあるノードではコストが高くなります。を使用するsiftUp
と、作業はツリーの最上部までの距離に比例するため、ツリーsiftUp
の最下部にあるノードではコストが高くなります。最悪の場合、両方の操作はO(log n)ですが、ヒープでは、1つのノードのみが一番上にあり、半分のノードが一番下のレイヤーにあります。したがって、すべてのノードに操作を適用する必要がある場合は、を優先siftDown
することはそれほど驚くべきことではありませんsiftUp
。
このbuildHeap
関数は、並べ替えられていない項目の配列を受け取り、それらがすべてヒーププロパティを満たすまでそれらを移動して、有効なヒープを生成します。ここで説明buildHeap
したsiftUp
とのsiftDown
操作を使用するには、2つの方法があります。
ヒープの最上部(配列の先頭)から開始し、siftUp
各項目を呼び出します。各ステップで、以前にふるいにかけられたアイテム(配列内の現在のアイテムの前のアイテム)が有効なヒープを形成し、次のアイテムをふるいにかけることで、ヒープ内の有効な位置に配置します。各ノードをふるいにかけた後、すべてのアイテムがヒーププロパティを満たします。
または、反対方向に進みます。アレイの最後から開始して、前方に向かって後方に移動します。各反復で、アイテムが正しい位置に来るまで下にふるいにかけます。
どちらの実装buildHeap
がより効率的ですか?
これらのソリューションはどちらも有効なヒープを生成します。当然のことながら、より効率的なのは、を使用する2番目の操作ですsiftDown
。
ましょH =ログ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つの方法(これも機能する他の分析があります)は、有限和を無限級数に変換してから、テイラー級数を使用することです。ゼロである最初の項は無視できます。
![buildHeapの複雑さのためのTaylorシリーズ](https://i.stack.imgur.com/959f6.png)
これらの各ステップが機能する理由がわからない場合は、ここにプロセスの正当性を言葉で説明します。
- 項はすべて正であるため、有限の合計は無限の合計よりも小さくなければなりません。
- 級数は、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)時間を必要とするのはなぜですか?ヒープのソートは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)でありbuildHeap
、siftUpを使用して実装された非効率なバージョンと同じです。しかし、この場合は、ソートしようとしているため、次に選択できるアイテムはありません。次に大きいアイテムを次に削除する必要があります。
要約すると、ヒープの並べ替えの作業は、2つの段階の合計です: buildHeapのO(n)時間と各ノードを順番に削除するO(n log n)なので、複雑度はO(n log n)です。比較ベースの並べ替えの場合、とにかくO(n log n)が期待できる最高であることを(情報理論のいくつかのアイデアを使用して)証明できるため、これに失望したり、ヒープの並べ替えを期待して理由を達成したりする必要はありません。 O(n)時間制限buildHeap
。