Rubyでバイナリ・ヒープを書いてみた
各ノードの子が常に親より大きくなる(あるいは小さくなる)バイナリ・ヒープを作ってみた。ヒープはソートに使われるほかにも、順序付きキュー(pre-ordered queue)など結構重要な応用があるらしい。
順序のあるキューを配列で実現する場合、3つのアプローチがある。
1. キューに入れるときに挿入ソートのようなことをして、常に配列の中身をソート済みにする。取り出しは端から順に
2. キューの配列は未ソート。取り出すときに最小なり最大を選択して取り出す。取り出し時に配列のスライドが必要
3. ヒープを使う
1は取り出しがO(1)で高速だし、2は入れるほうがO(1)だけど、その逆の操作ではそれぞれO(n)の計算量が必要になる。一方、ヒープを使えば出し入れともO(log n)で済む。
珠玉のプログラミングに丁寧な解説とC++のコード例あった。なるほどなと、すんなり分かったつもりで本を閉じてRubyで書き始めけど、やっぱり一発では動かなかった。siftupまでは動いたけど、siftdownが動かなかった。トップに置いたノードをsiftdownで下に落としていくときの場合分けは、抽象的なタンゴの入れ替えの「操作イメージ」としては自明だけど、ステップバイステップの「処理」としては、そこまで自明ということはない。で、紙に簡単な図を書いて、それを見ながらやると、劇的にコーディングが楽になることを発見した。
class MyHeap def initialize(size) @max = size + 1 # we don't use q[0] for practical reasons @q = Array.new(@max, 0) # heap / pre-ordered queue @n = 0 # number of items end def insert(i) if @n + 1 > @max then puts "full" return end @n += 1 @q[@n] = i siftup(@n) end def extract if @n == 0 then puts "empty" return end min = @q[1] @q[1] = @q[@n]; @q[@n] = 0 @n -= 1 siftdown(1) min end def siftup(n) while n > 1 if @q[n] < @q[n / 2] then @q[n], @q[n / 2] = @q[n / 2], @q[n] n = n / 2 else return end end end def siftdown(n) if n * 2 > @n then return elsif n * 2 == @n then if @q[n] > @q[n * 2] then @q[n], @q[n * 2] = @q[n * 2], @q[n] end elsif n * 2 + 1 <= @n then left = n * 2 right = n * 2 + 1 if (@q[left] < @q[right]) && (@q[n] > @q[left]) then @q[n], @q[left] = @q[left], @q[n] siftdown(left) end if (@q[right] < @q[left]) && (@q[n] > @q[right]) then @q[n], @q[right] = @q[right], @q[n] siftdown(right) end end end def to_s n = 1 while n < @n str = "" (n..n*2-1).each do |i| gap = (6 - Math::log(n,2)).to_i if gap <= 0 then gap = 1 end str += "#{@q[i]}"+" "*gap end print " "*((80 - str.size) / 2) ,"#{str}\n" n = n * 2 end end end if __FILE__ == $0 then h = MyHeap.new(20) a = []; 30.times{a << rand(30)}; a.uniq! # => [8, 9, 6, 18, 23, 19, 27, 11, 26, 16, 21, 17, 14, 29, 5, 15] a.each{|x|h.insert(x)} while true puts h gets print "extracted: #{h.extract}\n" end end
MyHeap#to_sが汚いし出力もイマイチだけど、結果はこんな感じ。
% irb
>> require "heap.rb"
=> true
>> h = MyHeap.new(20)
=> #<MyHeap:0x8377474 @max=21, @q=[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @n=0>
>> a = []; 30.times{a << rand(30)}; a.uniq!
=> [26, 17, 25, 15, 5, 12, 28, 11, 21, 7, 8, 0, 27, 20, 3, 16, 13, 29, 2, 6]
>> a.each{|x|h.insert(x)}
=> [26, 17, 25, 15, 5, 12, 28, 11, 21, 7, 8, 0, 27, 20, 3, 16, 13, 29, 2, 6]
>> h
=> #<MyHeap:0x8377474 @max=21, @q=[0, 0, 2, 3, 7, 6, 12, 5, 15, 13, 8, 11, 25, 27, 28, 20, 26, 16, 29, 21, 17], @n=20>
>> h.to_s
0
2 3
7 6 12 5
15 13 8 11 25 27 28 20
26 16 29 21 17
=> nil
>> 表示は見にくいけど、ちゃんとヒープになっている。
% ruby heap.rb
0
1 7
3 25 14 13
8 5 27 28 22 17 20 18
11 29 10 0 0
#<MyHeap:0x8265e00>
extracted: 0
1
3 7
5 25 14 13
8 10 27 28 22 17 20 18
11 29 0 0 0
#<MyHeap:0x8265e00>
extracted: 1
3
5 7
8 25 14 13
11 10 27 28 22 17 20 18
#<MyHeap:0x8265e00>
extracted: 3
5
8 7
10 25 14 13
11 29 27 28 22 17 20 18
#<MyHeap:0x8265e00>
extracted: 5
7
8 13
10 25 14 18
11 29 27 28 22 17 20 0
#<MyHeap:0x8265e00>
extracted: 7
8
10 13
11 25 14 18
20 29 27 28 22 17 0 0
#<MyHeap:0x8265e00>
extracted: 8
10
11 13
17 25 14 18
20 29 27 28 22 0 0 0
#<MyHeap:0x8265e00>
extracted: 10
11
17 13
20 25 14 18
22 29 27 28 0 0 0 0
#<MyHeap:0x8265e00>
extracted: 11
13
17 14
20 25 28 18
22 29 27 0 0 0 0 0
#<MyHeap:0x8265e00>
extracted: 13
14
17 18
20 25 28 27
22 29 0 0 0 0 0 0
#<MyHeap:0x8265e00>
extracted: 14
17
20 18
22 25 28 27
#<MyHeap:0x8265e00>
extracted: 17
18
20 27
22 25 28 29
#<MyHeap:0x8265e00>うまく動いてるように見えるけど、最後の方、to_sがバグってるっぽい。to_sはヒープとは直接関係ないけど。
21
22 25
26 24 0 0
#<MyHeap:0x8265e00>
extracted: 21
22
24 25
#<MyHeap:0x8265e00>
extracted: 22
24
26 25
#<MyHeap:0x8265e00>
extracted: 24
25
#<MyHeap:0x8265e00>
extracted: 25
#<MyHeap:0x8265e00>
extracted: 26
#<MyHeap:0x8265e00>
empty
extracted:
#<MyHeap:0x8265e00>
