TL

ヒープと優先度付きキューの内部

最小値の取り出しがなぜO(log n)で済むのかが原理から腑に落ちる。二分ヒープの配列表現とsift操作、ビルドヒープがO(n)になる訳、フィボナッチヒープがダイクストラを速める仕組みまで正確に押さえる。

応用ヒープ優先度付きキューアルゴリズム計算量ダイクストラ最終更新: 2026-06-21
TL;DR要点だけ先に
  • 1.二分ヒープは完全二分木を配列1本で表す。親はi、子は2i+1と2i+2で添字計算だけで辿れ、ポインタが要らない。
  • 2.挿入はsift-up、取り出しはsift-downでO(log n)。一方ビルドヒープは下から積むと総和が収束しO(n)で組める。
  • 3.二項・フィボナッチヒープはmerge/decrease-keyを償却で軽くし、ダイクストラの理論計算量をO(E + V log V)へ改善する。

優先度付きキューが解く問題

優先度付きキュー(priority queue)は「今ある要素のうち最小(または最大)を取り出す」操作を主役にする抽象データ型です。FIFOのキューが入れた順を守るのに対し、こちらは順序を無視して最優先の1つを高速に返します。タスクスケジューラ、ダイクストラ法やプリム法、ハフマン符号、イベント駆動シミュレーションなど、応用は広範です。

素朴に実装すると壁にぶつかります。ソート済み配列なら最小取り出しは O(1) ですが挿入が O(n)、未ソート配列なら挿入は O(1) でも最小探索が O(n)。どちらかが必ず重い。この挿入と取り出しの両方を O(log n) に揃える定番の解が二分ヒープ(binary heap)です。各操作の速さを表す計算量(Big-O)の記法は前提として進めます。

二分ヒープ ― 完全二分木を配列で表す

二分ヒープは2つの性質を同時に満たす二分木です。

  • ヒープ条件: 親は子以下(最小ヒープの場合)。これは根が全体の最小であることだけを保証し、左右の兄弟間に順序はない(探索木との決定的な違い)。
  • 完全二分木: 最下段を除き全段が埋まり、最下段は左詰め。形に隙間がない。

この「完全」という形の縛りが効きます。隙間がないので、木を幅優先順に1列に並べた配列でそのまま表現でき、ポインタが一切要りません。0始まりの配列なら添字 i のノードについて、

親      : (i - 1) / 2   (切り捨て)
左の子  : 2*i + 1
右の子  : 2*i + 2

という算術だけで親子を行き来できます。ポインタで木を組むよりメモリが密でキャッシュに乗りやすく、ノード確保も不要です。これがヒープが実務で多用される地味な強みです。

ヒープ条件はゆるい順序

二分探索木は「左の子 < 親 < 右の子」と全順序を強制しますが、ヒープは「親 ≦ 子」だけ。兄弟やいとこの大小は問いません。だから最小は O(1) で覗ける一方、k番目の最小を探すのは苦手で、ソート済みの列挙もできません。順序の縛りを根への一点だけに絞った結果が、この軽さです。

sift-up と sift-down ― 形を保ったまま順序を直す

挿入・取り出しは、まず形(完全二分木)を壊さずに要素を出し入れし、崩れたヒープ条件を1経路だけ辿って修復します。修復の向きが2種類あります。

挿入(sift-up / 上方修正): 新要素を配列の末尾(=最下段の右端の次)に置く。形は保たれるが、親より小さいとヒープ条件が崩れる。そこで親と比較し、小さければ親と交換を、条件を満たすか根に着くまで繰り返す。経路は葉から根への一本道なので、交換回数は高々木の高さ=O(log n)。

末尾(7 の子)に 2 を挿入し、親と比較しながら上げる

      5                5                2
     / \              / \              / \
    8   7     →      8   2     →      8   5
   / \  / \         / \  / \         / \  / \
  9 10 … 2         9 10 … 7         9 10 … 7
   2 < 7 で交換      2 < 5 で交換      根に到達、終了

取り出し(sift-down / 下方修正): 根(最小)を取り出したい。末尾要素を根へ移して末尾を削れば形は保たれるが、その値は大きすぎてヒープ条件を破る。そこで2人の子のうち小さい方と比較し、自分が大きければその子と交換を、両方の子以下になるか葉に着くまで繰り返す。経路は根から葉への一本道で、やはり O(log n)。

peek(最小の参照) : O(1)   ← 根 = 配列[0] を見るだけ
insert(挿入)      : O(log n) ← sift-up
extract-min(取出) : O(log n) ← 末尾を根へ→sift-down

下方修正で2人の子のうち小さい方を選ぶのが要点です。大きい方と交換すると、交換後にそちらの枝でヒープ条件が崩れ、修復が破綻します。

ビルドヒープがO(n)になる理由

n個の要素から一気にヒープを組むビルドヒープ(build-heap)には、直感に反する結果があります。「n回挿入すれば O(n log n) では?」と思えますが、下から sift-down を積むと O(n) で済みます。

手順は、配列をそのまま完全二分木とみなし、最後の内部ノードから根に向かって順に sift-down をかけるだけです。葉(後半の約半分)は子を持たず修復不要なので飛ばします。

なぜ O(n) か。sift-down のコストはそのノードの高さに比例し、木の高さ全体ではありません。高さ h のノードは約 n / 2^(h+1) 個あります。総コストは各高さの「ノード数 × 高さ」の総和で、

総コスト ≒ n * Σ (h / 2^(h+1))   (h = 0,1,2,…)

ここで Σ (h / 2^h) は 2 に収束する有限和。
よって 総コスト = O(n)。

直感的には、最も数が多い下段のノードはほとんど動かず(高さ0〜1)、高くつく上段のノードはごく少数だからです。逆に sift-up を上から積む方式だと、数の多い下段ノードが最大の高さを登るため O(n log n) に戻ってしまう。修復の向き一つで定数倍ではなくオーダーが変わる、ヒープで最も誤解されやすい点です。

ヒープソートとの混同に注意

ビルドヒープ自体は O(n) ですが、ヒープソートはその後 extract-min を n 回繰り返すため全体は O(n log n) です。ソートの比較ベース下限と一致します(比較ソートの下限 参照)。ビルドヒープの O(n) は「ヒープを作るまで」の話で、ソート全体が O(n) になるわけではありません。

二分ヒープの弱点 ― マージとdecrease-key

二分ヒープは挿入・取り出しに優れますが、2つの操作が重いままです。

  • マージ(meld): 2つのヒープを1つに統合する。配列表現では結局作り直しに近く O(n)。
  • decrease-key: 既存要素の優先度を下げる(小さくする)。位置を特定できれば sift-up で O(log n) だが、それでも対数コストがかかる。

ダイクストラ法では、隣接ノードの暫定距離を更新する decrease-key が辺の数だけ呼ばれます。ここを軽くできれば、最短経路探索そのものが速くなる。これが、より高度なヒープが生まれた動機です。

二項ヒープとフィボナッチヒープ ― 償却で軽くする

二項ヒープ(binomial heap)は、サイズが2の冪に対応する二項木の森として要素を保持します。各二項木の根を連結リストでつなぐため、2つのヒープのマージは2進数の足し算と同型になり O(log n) で済みます。挿入・取り出しも O(log n) を保ちつつ、二分ヒープが苦手なマージを対数化したのが利点です。

フィボナッチヒープ(Fibonacci heap, 1984年 Fredman と Tarjan)は、ここから償却計算量を徹底的に削ります。鍵は「修復を怠けて先送りする」設計です。挿入やマージでは木を整理せず根のリストに放り込むだけにし、extract-min のときにまとめて木を統合(consolidate)します。これにより、

操作二分ヒープ二項ヒープフィボナッチヒープ(償却)
insertO(log n)O(log n)O(1)
find-minO(1)O(log n)O(1)
extract-minO(log n)O(log n)O(log n)
decrease-keyO(log n)O(log n)O(1)
meld(マージ)O(n)O(log n)O(1)

ここでの O(1) は最悪ではなく**償却(amortized)**である点が肝心です。個々の操作は時に重くなりますが、操作列全体でならすと1回あたり定数に収まる、という保証です。decrease-key の O(1) 償却は、優先度を下げたノードを親から切り離して根リストへ移すだけにし、切り離しが連鎖した分のツケを後の extract-min に付け替えることで実現します(cascading cut)。名前の由来は、各木のサイズ下限の解析にフィボナッチ数列が現れることです。

ダイクストラ法への寄与

V頂点・E辺のグラフでのダイクストラ法は、優先度付きキューの実装で計算量が変わります。

キューの実装          全体計算量
配列(線形探索)      O(V^2)
二分ヒープ            O((V + E) log V)
フィボナッチヒープ    O(E + V log V)   ← 理論上の最良

ダイクストラは extract-min を V 回、decrease-key を最大 E 回呼びます。二分ヒープでは両方が O(log V) なので合計 O((V + E) log V)。フィボナッチヒープなら decrease-key が償却 O(1) に落ちるため、E 個の更新の対数係数が消え、O(E + V log V) になります。辺が多い密グラフ(E が V^2 に近い)ほど、この差は理論上大きく効きます。

試験・面接での頻出ポイント

「二分ヒープの挿入と取り出しの計算量は?」→ ともに O(log n)、peek は O(1)。「配列での親子の添字は?」→ 0始まりで親 (i-1)/2、子 2i+1 と 2i+2。「ビルドヒープが O(n) の理由は?」→ 下から sift-down、各ノードのコストは高さに比例し Σ(h/2^h) が収束するから。「フィボナッチヒープの利点は?」→ decrease-key と insert と meld が償却 O(1)、ダイクストラを O(E + V log V) に。この4点は定番です。

実務ではどれを使うか

理論最良はフィボナッチヒープですが、定数倍が重くポインタ操作も複雑なため、実務では二分ヒープ(配列1本でキャッシュ効率が高い)が圧倒的に主流です。C++ の priority_queue、Python の heapq、Java の PriorityQueue はいずれも二分ヒープ。フィボナッチヒープは漸近計算量が支配的になる超大規模グラフや、理論解析の文脈で意味を持ちます。

まとめ

ヒープの核心は「根への順序だけを保ち、形を完全二分木に縛る」という割り切りにあります。形の縛りが配列1本での表現を可能にし、sift-up / sift-down が1経路の修復で挿入・取り出しを O(log n) に揃える。ビルドヒープが O(n) になるのは、コストの安い下段ノードが大多数で総和が収束するからで、修復の向きがオーダーを決めます。二項・フィボナッチヒープはマージと decrease-key を償却で軽くし、ダイクストラ法を理論上 O(E + V log V) まで押し下げます。ただし実務では定数倍とキャッシュ効率から二分ヒープが標準であり、これは順序付き集合を支える平衡木とは別系統の、優先度に特化したデータ構造です。

プログラミング Article

ヒープと優先度付きキューの内部を実務で読む

TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。

解決すること

ヒープ

比較で見る軸

難易度: advanced / カテゴリ: プログラミング / タグ数: 5

導入後に効く点

挿入はsift-up、取り出しはsift-downでO(log n)。一方ビルドヒープは下から積むと総和が収束しO(n)で組める。

先に潰すリスク

用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。

数字・仕様の読み方
難易度
advanced
カテゴリ
プログラミング
タグ数
5

判断チェックリスト

  • 自社の用途が「ヒープ / 優先度付きキュー」に近いか確認する。
  • 強みである「二分ヒープは完全二分木を配列1本で表す。親はi、子は2i+1と2i+2で添字計算だけで辿れ、ポインタが要らない。」が本当に評価軸になるか確認する。
  • 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
  • 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
  • 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

ヒープ優先度付きキューアルゴリズム計算量ダイクストラヒープ優先度付きキューアルゴリズム