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>