Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

【Ruby入門】配列・ハッシュをソートする方法を徹底解説!sort/sort_byの使い分けから応用テクまで

Rubyで配列をソートするのはとても簡単です。

望みの結果を得るために、複雑なアルゴリズムを自前で書く必要は一切ありません

では、何が必要なのでしょうか?

答えは「Rubyに組み込まれているソートメソッドの理解」です。

主なメソッドは次の3つです。

  • sort
  • sort_by
  • sort!

これらのメソッドはどう動作し、なぜ違いがあるのでしょうか?

この記事では、その疑問にお答えします。

まず基本となる sort メソッドから始め、複数の値を基準とした高度なソートが可能な sort_by、さらに逆順ソートやハッシュのソートなど、さまざまなテクニックを学んでいきましょう。

それでは始めましょう!

sortメソッドとsort!メソッドの基本的な使い方

最も基本的なソートは、Enumerableモジュールで定義されているRubyの sort メソッドによって提供されます。

実際の例を見てみましょう

nnumbers = [5,3,2,1]

numbers.sort
# [1,2,3,5]

注目すべき点として、sort メソッドは新しい配列を生成して結果を返します。元の配列は変更されません。

返ってくるのは、きちんと並べ替えられた要素を持つ配列です!

また、sort! メソッドを使えば「破壊的」(in-place)なソートも可能です。

これは、新しい配列を作る代わりに元の配列そのものを書き換えるという意味で、パフォーマンス面で有利になる場合があります。

sort_byによるカスタマイズされたソート

sort_by メソッドを使うと、より高度で柔軟なソートが実現できます。

たとえば次のようなことが可能になります

  • 文字列の長さでソートする
  • 文字列の中身に基づいてソートする
  • 数値が偶数か奇数かでソートする

これらはすべて、sort_by メソッドとRubyのブロックを組み合わせることで実現できます。

例を見てみましょう

strings = %w(foo test blog a)

strings.sort_by(&:length)

# ["a", "foo", "test", "blog"]

なぜこれが動くのでしょうか?

sort_by メソッドはブロック内で数値を返すことを期待しています。length メソッドは文字列の長さ(整数)を返すため、正しく動作するのです。

この仕組みさえ理解できれば、いろいろな応用が可能になります。たとえば、「大文字で始まる単語を先頭に集め、それ以外は元の順序のまま維持する」といったソートも簡単に書けます。

具体的にはこんな感じです

def sort_by_capital_word(text)
  text
    .split
    .sort_by { |w| w[0].match?(/[A-Z]/) ? 0 : 1 }
    .join(" ")
end

sort_by_capital_word("calendar Cat tap Lamp")

# "Cat Lamp calendar tap"

もちろん、通常の sort メソッドにブロックを渡してカスタムソートを行うこともできます。

以下がその例です

strings = %w(foo test blog a)

strings.sort { |a,b| a.length <=> b.length }

# ["a", "foo", "test", "blog"]

ポイント<=> という記号は「宇宙船演算子(UFO演算子)」とも呼ばれ、自分のクラスに実装できるメソッドです。比較の結果として、大きければ 1、等しければ 0、小さければ -1 を返すことが求められます。

個人的には、意図が明確で読みやすく、速度面でもわずかに有利なため、sort_by メソッドの方をおすすめします。

降順(逆順)でソートする方法

逆順にソートしたい場合はどうすればいいでしょうか?

方法は2つあります。ひとつはソート後に reverse メソッドを使う方法、もうひとつはブロック内でソート対象の値にマイナス記号を付ける方法です。

例を見てみましょう

strings = %w(foo test blog a)

strings.sort_by { |str| -str.length }

# ["blog", "test", "foo", "a"]

地味なテクニックですが、シンプルで確実に動きますよ!

英数字を含む文字列の自然順ソート

数値を含む文字列のリストを、数値として正しくソートしたい場面もありますよね。

たとえばこんなケースです

music = %w(21.mp3 10.mp3 5.mp3 40.mp3)

デフォルトのままでは、期待通りの順序にはなりません。

例を見てみましょう

music.sort

# ["10.mp3", "21.mp3", "40.mp3", "5.mp3"]

文字列として比較されるため、「5」よりも「10」や「21」が先に来てしまうのです。

この問題は sort_by を使えば解決できます。

music.sort_by { |s| s.scan(/\d+/).first.to_i }

# ["5.mp3", "10.mp3", "21.mp3", "40.mp3"]

ここでは、正規表現(\d+)で文字列中の数値部分を抽出し、最初にマッチした数値を取り出して(first)、整数オブジェクトへ変換(to_i)しています。こうすることで数値として正しく比較され、直感的な順序になります。

Rubyでハッシュをソートする方法

ソートできるのは配列だけではありません。ハッシュもソートすることができます。

例を見てみましょう

hash = {coconut: 200, orange: 50, bacon: 100}

hash.sort_by(&:last)

# [[:orange, 50], [:bacon, 100], [:coconut, 200]]

これは値(value)を基準にソートした例ですが、ここで興味深い点があります。戻り値はハッシュではないのです。

ハッシュをソートすると、結果は多次元配列(配列の配列)として返されます。

この結果を再びハッシュに戻したい場合は、Array#to_h メソッドを使いましょう。

複数の条件(キー)でソートする方法

複数の属性を組み合わせてソートしたいこともあるでしょう。たとえば、まず日付でソートするものの、同じ日付のデータが複数あって順位が同着になってしまうようなケースです。

この同着を決めるためには、第2の属性(タイブレーカー)を使います。

例を見てみましょう

Event = Struct.new(:name, :date)
events = []

events << Event.new("book sale", Time.now)
events << Event.new("course sale", Time.now)
events << Event.new("new subscriber", Time.now)
events << Event.new("course sale", Time.now + 1.day)

events.sort_by { |event| [event.date, event.name] }

ここでの鍵は、sort_by ブロックの中にある配列です。

配列の第1要素に主要なソート基準(event.date)、第2要素に同着を解消するための副次的な基準(event.name) を指定しています。このように配列で渡せば、優先順位をつけた多段階ソートが実現できます。

クイックソートを自前で実装してみる

お遊びとして、自分自身でソートメソッドを実装してみましょう。組み込みのソートメソッドより遅くなりますが、コンピュータサイエンス好きなら興味深い演習になるはずです。

def quick_sort(list)
  return [] if list.empty?

  groups = list.group_by { |n| n <=> list.first }

  less_than    = groups[-1] || []
  first        = groups[0]  || []
  greater_than = groups[1]  || []

  quick_sort(less_than) + first + quick_sort(greater_than)
end

p quick_sort [3, 7, 2, 1, 8, 12]

# [1, 2, 3, 7, 8, 12]

クイックソートの考え方はシンプルです。まず適当に1つの数値(ピボット)を選び、ソート対象のリストを2つのグループに分割します。

片方のグループはピボットより小さい数値、もう片方のグループはピボットより大きい数値で構成されます。

あとは、リスト全体が整列するまでこの操作を繰り返すだけです。

各ソート方法のベンチマーク比較

最後に、これらのソート方法のパフォーマンスを比較してみましょう。

Ruby 2.4.0での計測結果

  sort!:                1405.8 i/s
  sort:                 1377.6 i/s - same-ish: difference falls within error
  sort_by reverse:      196.6  i/s - 7.15x  slower
  sort_by:              183.7  i/s - 7.65x  slower
  sort_by minus:        172.3  i/s - 8.16x  slower
  sort with block:      164.1  i/s - 8.57x  slower

ご覧のとおり、通常の sort メソッドは sort_by より大幅に高速です。ただし、柔軟性を求めるならブロックを併用する必要があります。「単純なソートならsort、条件付きソートならsort_by」という使い分けが賢明でしょう。

まとめ

この記事では、sortsort_by メソッドを使って、配列やハッシュをさまざまな方法でソートする方法を学びました。あわせてパフォーマンスの違いや、クイックソートの実装についても理解できましたね。

覚えておきたいポイントは次の3つです

  • 配列・ハッシュなどの Enumerable オブジェクトに対して sort メソッドを使うと、デフォルトの挙動(<=> 演算子に基づくソート)で並べ替えられる
  • sort にブロックと2つの引数を渡すと、オブジェクト同士の比較ルールを自由に定義できる(ブロックは 1、0、-1 のいずれかを返す)
  • sort_by にブロックと1つの引数を渡すと、各オブジェクトからソートの基準となる値(配列の長さ、オブジェクトの属性、インデックスなど)を抽出できる。ブロックは整数を返し、その値がソート後の位置を決定する

この記事が役に立ったら、ぜひ周りの人にもシェアしてくださいね。🙂

  1. Rubyでファイルを読み書きする方法を実例つきで徹底解説

    Rubyでファイルの読み書きができるようになると、ファイルからデータを抽出したり、新しいファイルを作成したり、必要な情報を素早く見つけたりできるようになります。この記事では、Rubyにおけるファイル操作の基本を、実際のコード例とともにわかりやすく解説します。 この記事で学べること Rubyでファイルを読み込む方法 Rubyでファイルに書き込む方法 覚えておきたいFileクラスのメソッド ディレクトリ操作の基礎 FileUtilsモジュールの使い方 まとめ それでは始めましょう! Rubyでファイルを読み込む方法 Rubyでファイルを読み込む基本的な流れは、次の3ステップです。 op

  2. Rubyの配列(Array)クラスの使い方徹底解説!基本操作と便利なメソッド一覧

    配列(Array)とは? 配列はRubyに組み込まれたクラスで、0個以上の要素をリスト形式で保持することができます。さらに、要素の追加・アクセス・繰り返し処理を簡単に行えるメソッドも多数用意されています。 もし配列が存在しなければ、値ごとに個別の変数を用意しなければならず、管理が非常に大変になります。 例: a = 1 b = 2 c = 3 しかし、配列を使えば次のように書けます: numbers = [1, 2, 3] さらに嬉しいことに、配列の中には何でも入れられます! 例えば: 数値 文字列 さらに別の配列!(これは多次元配列になります) それでは、配列を最大限に活用できる