2012/03/08

sort_byメソッドの定義をひとつひとつ理解してみる

sortよりもsort_byの方が処理が軽いらしい。なんで?と思ったので、yield、ブロックの勉強も兼ねて調べてみた。

sort_byメソッドの定義を確認

class Array
  def sort_by
    self.collect { |i| [yield(i), i] }.
      sort {|a,b| a[0] <=> b[0] }.
      collect! {|i| i[1]}
  end
end
ほっほう。何やら複雑そうだ。yieldも入っていてなんだかわかりにくいので、処理の流れに沿って1つ1つ見ていく。

まずはsort_byメソッドが呼ばれるところから

sort_byメソッドは下記のような感じで呼ばれる。
ary = [1,2,3]
ary.sort_by do |num|
  num
end
もちろん下記でも同じ。
ary = [1,2,3]
ary.sort_by { |num| num }
この時にプログラムの気持ちになると、、
「実行しようとしたらsort_byメソッドが呼ばれたから呼び出してみよう!」
と思ってsort_byメソッドの定義を参照。
class Array
  def sort_by
    self.collect { |i| [yield(i), i] }.
      sort {|a,b| a[0] <=> b[0] }.
      collect! {|i| i[1]}
  end
end
「あれ?sort_byメソッドの中にyieldって書いてある!何したらいいかわからんからもう一回呼び出し元のブロックの中身を見に行かなきゃ!」

で、もう一回sort_byの呼び出し元を見てブロック要素を発見
do |num|
  num
end
yieldを実行すると、呼び出し時に与えられたこのブロックが実行されることになる。
イメージとしては下記のような感じに呼び出されたブロック内が実行される。
def yield(num)
  num
end
この場合は単純に引数をそのまま返すだけの処理。
これでようやくsort_byメソッド内でやるべきことがわかった。

ここからはメソッド内部の話。

最初から順番に見ていく。
self.collect { |i| [yield(i), i] }
まず、self.collectとあるので、メソッドのレシーバであるary配列に対してcollectすることがわかる。
ary配列の各要素を取り出し、[yield(i), i]の左側の部分で先程定義されたyield関数が実行され、右側のiにはそのまま配列から取り出した要素が渡される。
それをary配列分繰り返すと下記のような配列が生成される。
[[3, 3],[2, 2][1, 1]]
そしてこの配列に対して次の処理がそのまま実行される。つまり、
[[3, 3],[2, 2][1, 1]].sort{ |a, b| a[0] <=> b[0] }
となる。
sortメソッドがする処理を日本語で書くと、
「配列の中の各要素の内、左側の数字を2つ(たとえば、[3, 3]の左側の3と[2, 2]の左側の2)を持ってきて <=>で比較して並び替える。この処理をすべての要素の並び順が整うまで実行する」となる。

結果は
[[1, 1],[2, 2],[3, 3]]
となる。
これをさらに
[[1, 1],[2, 2],[3, 3]].collect! { |i| i[1] }
今度は各要素の右側の要素を抜き出して並べる処理。結果は
[1, 2, 3]
こうなる。完成!

で、結局なんでsortよりもsort_byのほうが処理が軽いのか

並び替えの操作自体はそれほど重い処理ではないので、大きな差は生まれない。ただ、並び替えをする前に処理の重い文字列操作などが入ると大きな差になる、ということ。

たとえば、暗号化をしてから並び替えを実施する場合などには、sortの場合には比較する度に暗号化の操作が必要になるため、最大でn^2(nは比較の要素数)の処理が発生することになる。
これに比べて、sort_byの場合は最大で要素数n回の処理で済む。

サンプルで示したような単純な並び替えであればそれほど処理速度に差は生まれないが、後から操作を追加したりすることも想定されるので、できるだけsort_byを使うようにしておくほうがよい、ということになる。

sort_by、よく考えられていますね!

参考URL

0 件のコメント:

コメントを投稿