LoginSignup
13
4

More than 1 year has passed since last update.

なぜヒープ化 (heapify) は O(N) で可能なのか

Last updated at Posted at 2022-01-05

ヒープとは

ヒープとはデータ構造の一種であり、親要素が子要素よりも常に大きい(あるいは小さい)という特徴を持つ木構造です。つまり、ヒープの根が最大値(あるいは最小値)になり、$O(1)$ で参照することができます。またデータの挿入や最大値(あるいは最小値)の削除も $O(\log{N})$ と高速で可能なことから、優先度付きキューの実装としても利用されています。本記事では、特に二分木を使用した二分ヒープについて扱います。

ヒープ化にかかる計算量

ヒープ化されてない配列からヒープを作成する場合の計算量についてですが、1つのデータの挿入に $O(\log{N})$ 要することから考えると、直感的には $O(N\log{N})$ になりそうですが、実際には $O(N)$ で可能です。一例として、Python の heapq の実装にも線形時間でリストをヒープに変換するとの記述があります。

heapq — Heap queue algorithm — Python 3.10.1 documentation

Transform list x into a heap, in-place, in linear time.

$O(N)$ のヒープ化では、上記のように一つずつ値を挿入するのではなく、ヒープ化されていない配列に対してヒープの条件を満たすように値を交換していく(これが in-place に相当する)ことでヒープを作成します。

具体例を挙げながら見てみましょう。ここでは親ノードより子ノードが大きい条件を満たす、最大ヒープを作成します。

  1. 最も左にあるのが初期状態です。親ノードより子ノードが大きいという条件を満たしていません。ここから、ヒープの下の方から上の方へと順にヒープ条件を満たすように値を交換していきます。
  2. まず、最も深いノードである 2、6、4、7 については子のノードを持っていないので操作は必要はありません。
  3. 次に、値が 5 のノードとそれより下にあるノードがヒープの条件を満たすように値を交換します。ここでは、5 と 7 を交換します。これにより、親が 7 で子が 4 と 5 になり、ヒープ条件を満たしました。
  4. 同様に 1 と 6 を交換することで、親が 6、子が 1 と 2 になりヒープ条件を満たします。
  5. 最後に、最も親のノードとそれより下にあるノードがヒープ条件を満たすように値を交換します。3 と 7、次いで 3 と 5 の値を交換することで全てのノードにおいてヒープ条件が満たされます。

heapify-2.png

この操作にかかる計算量を考えると、注目したノードとそれより下にあるノードがヒープ条件を満たすために、自分より下のノードの深さに比例する計算量が必要になります(詳細はここでは割愛しますが、heapq の実装1などを読むと理解できるはずです)そして深さに比例する計算量をその深さのノード数分だけ行うことになります。二分木の構造上、深さの浅いノードが大半を占めるので、感覚的にも $O(N\log{N})$ かからなそうな気がしてきましたね。

heap-order.png

一般化してみましょう。
まず、必要な式変形を確認します。$|x|<1$ を満たす場合の無限等比級数の和と、その微分は以下のように表されます。

\sum_{j=0}^{\infty}x^j = \frac{1}{1-x}
\sum_{j=0}^{\infty}jx^{j-1} = \frac{1}{(1-x)^2}

微分した式に $x$ をかけると以下の式が得られます。

\sum_{j=0}^{\infty}jx^{j} = \frac{x}{(1-x)^2}

これを利用すると、ヒープ化に必要な計算量は以下のようになります。ここでは、高さ(深さ)を $h = \lfloor \log{N} \rfloor$ と定義します。($\lfloor \ \rfloor$ は floor 関数で、小数点以下を切り捨てます)

\begin{align}
\sum_{j=0}^{h}j \cdot 2^{h-j} &= 2^h \sum_{j=0}^{h}\frac{j}{2^j} \\
&\leq 2^h \cdot 2 \\
&\in O(N)
\end{align}

このようにしてヒープ化は $O(N)$ で計算できることが確認できました。

ヒープソートにかかる計算量

ヒープ化は $O(N)$ で可能ですが、あくまでヒープはソートとは異なり、最大値(あるいは最小値)を取り出すことしかできません。そのためヒープを利用してソートを行うヒープソートでは、$O(\log{N})$ の取り出しを $N$ 回行う必要があり、計算量が $O(N\log{N})$ になる点には注意が必要です。

まとめ

  • ヒープ化は、ヒープの下から上へと順にヒープ条件を満たすように値を交換することで、$O(N)$ で計算できる。
  • ヒープソートはあくまで $O(N\log{N})$ 要する。
  1. cpython/heapq.py at 3.10 · python/cpython

13
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
4