Rubyで上位n件を求める

最近は仕事先以外でコードを書くことが多くなっている。
特にここ1・2ヶ月はパズルとかの問題を解くために実験的に多くの組み合わせの計算をRubyにさせることが多い。このときにしばしば欲しくなるのが「上位n件を求める」機能。配列に設定済みのデータであれば、Array#sortを呼び出した後に配列の先頭を見れば良いのだけど、以下の点が気になる。

  • 上位10件を求めるときに100万件のデータを貯めておいて完全にソートするのはメモリと時間の無駄
  • Arrayで扱わない無限個のデータには対応できない

例えば、一万個の乱数を求めてその値の上位5件を表示させたいのなら、こんな感じで簡単に取り出したい。(以下、コードはすべてRuby 1.9

topn = TopN.new(5)
1.upto(10000).each do
  topn << Random.rand
end
topn.each_with_index do |data,i|
  puts "Rank#{i+1}:value=#{data}"
end

ついでに、個々のデータに付加情報があるのであれば、データから値を取り出す処理をコンストラクタにブロックとして与えればうまく動くようにしたい。例えば、「値が何回目に出現したか」を扱いたい時はこんな感じで書けたらなぁ...と。

topn = TopN.new(5) { |data| data[:value] }
1.upto(10000).each do |count|
  topn << {value: Random.rand,description: count}
end
topn.each_with_index do |data,i|
  puts "Rank#{i+1}:value=#{data[:value]} count=#{data[:description]}"
end

探してみても相当するライブラリが見つからなかったので、TopNクラスのコードを書いてみた。
(ちゃんと探せば実は有る??)

最初の例を実行すると以下のように出力され、

Rank1:value=0.9999727375403678
Rank2:value=0.9999709710675848
Rank3:value=0.9998929520002925
Rank4:value=0.9998796385129795
Rank5:value=0.9998750822342006

二つ目の例であれば、以下のように出力される。

Rank1:value=0.9999711913114 count=3520
Rank2:value=0.9999658723206534 count=4642
Rank3:value=0.9999289720505727 count=9733
Rank4:value=0.9998648527153264 count=916
Rank5:value=0.9998544367073094 count=3624

数値に限らず、<や>で比較可能なオブジェクトであれば何でも対象に出来る。
以下、コード。

# 上位n件のソートされたデータ
class TopN
  attr_reader :top_data

  def initialize(n,&b)
    @n,@sel_v,@top_data = n,b,[]
  end

  def each
    @top_data.each { |v| yield v }
  end

  def each_with_index
    @top_data.each_with_index { |v,i| yield v,i }
  end

  def <<(target)
    update(target) if (@top_data.size < @n || to_v(@top_data[-1]) < to_v(target))
  end

  protected
  def to_v(v)
    @sel_v ? @sel_v.call(v) : v
  end

  def update(target)
    if @top_data.empty?
      @top_data << target
    else
      dest_data = []
      @top_data.each do |source|
        if target && to_v(target) > to_v(source)
          dest_data << target 
          target = nil
        end
        break if dest_data.size == @n
        dest_data << source
      end
      @top_data = dest_data
    end
  end
end