roy > naoya > 基礎プログラミングII[月2] > (5)ハッシュ[2]

(5) 11/05の授業内容:ハッシュ[2]

ハッシュの利点と問題点

ハッシュは配列のインデックスとは異なり、keyに任意の名称を与えることができる。また、単一のkeyに対して複数のvalueを結びつけて保存することが可能であり、配列の場合とは異なり取り扱う変数の数を減らすことができるという利点がある。

一方で問題点もあった。前回の授業でも多少触れたが、次のプログラムを見ながらもう一度確認してみよう。

これは、ある大学で行われたソフトボール大会の結果である。6チームで136試合を行い、最も勝率が高いチームが優勝となる(softball.txt)。

チーム         勝  負  引
アンツ         73  53  10
スネイルズ       52  82   2
ドラゴンフライズ    78  51   7
モスキートーズ     65  65   6
クリケッツ       50  78   8
ブックウォームス    75  48  13

チーム名をkey、勝数、負数、引分数をvalueとしてハッシュ変数softに代入し、勝率を求めて結果を表示するプログラムを書いてみると以下のようになる(softball.rb)

#!/usr/koeki/bin/ruby

soft = Hash.new

while line = gets
  if /(\S+)\s+(\d+)\s+(\d+)\s+(\d+)/ =~ line
    # 1個目の() (\S+)→チーム名が入る
    # 2個目の() (\d+)→勝ち数が入る
    # 3個目の() (\d+)→負け数が入る
    # 4個目の() (\d+)→引分数が入る
    soft[$1] = [$2.to_f,$3.to_f,$4] # 配列を代入
  end
end

print "--チーム名------------+-勝ち-+-負け-+-引分--+-勝率---\n"
for team, data in soft
  # data には、[勝ち数, 負け数, 引分数] という配列が入っている
  printf("%s %d %d %d %f\n", team, data[0], 
  data[1], data[2],data[0]/(data[0]+data[1]))
  # dataの第0要素が勝ち数、第1要素が負け数、第2要素が引分数
end
irsv{c10xxxx}% ruby soft.rb softball.txt[Return]
--チーム名------------+-勝ち-+-負け-+-引分--+-勝率---
ブックウォームス         75     48     13     0.610
クリケッツ            50     78      8     0.391
スネイルズ            52     82      2     0.388
ドラゴンフライズ         78     51      7     0.605
モスキートーズ          65     65      6     0.500
アンツ              73     53     10     0.579

結果を表示すると確かに勝率が追加されている。しかし、通常勝敗を示す場合は勝率が高い順に表示したい。ハッシュの場合、結果を表示する順番は代入した順番にすらならない。値を格納するという点ではハッシュは便利であるが、結果を表示する上では使い勝手が悪いという問題がある。

今日は、ハッシュを用いた際に結果を昇順(小さい順)や降順(大きい順)に並び替えて表示する方法について確認していこう。

出席課題

softball.txtには6チームの結果が記載されているが、ここに2チーム追加しよう。チーム名・勝・負・引分数は自由に決めてよい。
その後、

printf("%s %d %d %d %f\n", team, data[0], data[1], data[2], data[0]/(data[0]+data[1]))

の行の%sや%dに幅指定の数字を追加し、上の実行結果と同じように結果をそろえて表示できるようにしてみよう。

制限時間は10分程度。出席点は2点。提出要領は下記の通り。

  • 提出先:naoya@e.koeki-u.ac.jp
  • メールのSubject:attend05
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降に修正したprintfの行を貼り付け、実行結果をその下に貼り付ける。

Tips:emacsでの日本語入力のオンオフはCtrl-oです

Tips:Mewによるメールの送り方はMewコマンドを参照

配列における並び替え

ハッシュにおける結果の並び替えを考えるにあたり、まず配列の並び替え方法を確認しよう。

例えば、a=[192,168,0,255]という配列を仮定する。この配列内の4つの値を昇順(小さい順)に並び替えることを考えてみる。この場合、配列処理メソッドのsortを使用すればよい。sortは昇順(小さい順)に並び替えるメソッドである。

b = a.sort

とすることで、配列aの4つの値を小さい順に並び替えた配列bが完成する。すなわち配列bは、b=[0,168,192,255]となっている。

今度は配列内の値を降順(大きい順)に並び替えることを考えよう。この場合はsortメソッドに加えてreverseメソッドを使用する。reverseメソッドは配列内の値を逆順にするメソッドである。

b = a.reverse

とすると、配列aの中の値が逆順に並び替えられ、b=[255,0,168,192]となる。このままでは降順ではないが、sortメソッドと組み合わせればよい。まずsortメソッドで昇順に並び替えてから、reverseメソッドで逆順に並び替えると降順にした結果が得られる。すなわち下記のように書く。

b = a.sort.reverse

これにより、配列bb=[255,192,168,0]となる。b=a.reverse.sortのように順番を逆にすると昇順になってしまうので気をつけよう。

sortメソッドへのソート基準ブロックの付加

sortメソッドには、ソート基準を決めるブロックを付加することができる。具体的には次のように書く。{ }内がソート基準ブロックに相当する。

配列.sort{|x,y| xyの比較式}

ソート基準ブロックを用いて昇順(小さい順)、降順(大きい順)に並び替えを行う場合、それぞれ以下のように書く。

  • 昇順:配列.sort{|a,b| a<=>b} (| |内と後ろのabの順が同じ)
  • 降順:配列.sort{|a,b| b<=>a} (| |内と後ろのabの順が異なる)

|a, b|は、配列内の値(要素)を順次abに代入し左側をa、右側をbとするという意味である。<=>は宇宙船演算子とも呼ばれ、本来の働きは左辺が大きければ1、等しければ0、右辺が大きければ-1を返すというものである。ここでは、右が大きければ順番はそのまま、左が大きければ順番を入れ替えるという働きをする。これを配列内の要素について繰り返し行うことで、最終的に右側が大きくなる。昇順の場合は| |内と後ろのabの順が同じであるが、降順の場合は順番が異なっている。abの順番を入れ替えて右側に大きな値を入れると、結果的に大きい順に並ぶことになる。

ちなみに、単にsortとすると昇順ソートになるが、これはソート基準ブロックに {|a, b| a<=>b} を指定したことと同義になる。sortやsort.reverseを使用する場合と比べ、ソート基準ブロックを使用する方法は複雑であるが、ハッシュのソートを行う際は必要不可欠となる。

なお、ソート基準ブロックの中でabという2つの変数を使っているが、これはxyでもjkでも何でも構わない。

ソート基準ブロックの基本形

  • 昇順:配列.sort{|a,b| a<=>b} (| |内と後ろのabの順が同じ)
  • 降順:配列.sort{|a,b| b<=>a} (| |内と後ろのabの順が異なる)

ここではabという2つの変数を使用しているが変数は任意の名称でよい。例えばxyでも良いし、hogepiyoでもよい。

ハッシュのソート

ハッシュのソートの考え方

以下はテストの得点を示したものである。名前をkey、得点をvalueとするハッシュ変数testに代入し、得点の低い順に並び替えることを考えてみよう。

氏名   得点
一郎    72
二郎    48
三郎    96
五郎    33

まずハッシュ変数testに上記のデータを初期値として代入する。

test  = {
  "一郎" => 72,
  "二郎" => 48,
  "三郎" => 96,
  "五郎" => 33,
}

ハッシュの並び替えを行う場合、keyを並べ替えることを考える。keyの順番が決まれば、あとはkeyとvalueを対で表示するだけである。ただし、keyを並べ替える際の基準はvalueとなる。つまりvalueの小さい順(もしくは大きい順)にkeyを並べておき、keyとvalueを対で表示するという方策が考えられる。

valueに基づいてkeyを並べる

ハッシュからkeyを取り出すためには

ハッシュ.keys

とする。keyではなくkeysであることに注意しよう。参考までにvalueのみを取り出す場合は

ハッシュ.values

となる。ここでもvalueではなくvaluesとなる。

ハッシュ変数testは、keyに名前、valueに得点が代入されているのでpメソッドでkeyを表示すると

p test.keys #=>["三郎","二郎","一郎","五郎"]

となる。順番はハッシュへの代入時とは異なっていることを確認しよう。次にkeyをvalueに基づいて昇順に並び替える。このためsortメソッドのソート基準ブロックではvalueを比較する。

p test.keys.sort {|x,y| test[x] <=> test[y]}
#=>["五郎", "二郎", "一郎", "三郎"]

test[x]test[y]xyをkeyとするvalueに相当する。test.keys.sortでハッシュ変数testのkeyを取り出して並べ替えを行おうとしている。一方、ソート基準ブロックの比較式ではtest[x]test[y]というようにvalueが使用されている。こうすることでvalueに基づいてkeyを並び替えることが可能になる。なお、|x,y|とtest[x] <=> test[y]の部分でxyの順番が同じであるためkeyが昇順に並べ替えられる。

これをプログラムとして書いたものが、以下のsort.rbである。for-endでハッシュの値を取り出す際にソート基準ブロックを用いて、valueが小さい順に取り出している。

#!/usr/koeki/bin/ruby
test  = {
  "一郎" => 72,
  "二郎" => 48,
  "三郎" => 96,
  "五郎" => 33,
}
for item in test.keys.sort{|a, b| test[a] <=> test[b]}
  printf("%s は %d点です。\n", item, test[item])
end

実行結果は下記の通りとなる。

irsv{c10xxxx}% ruby sort.rb[Return]
五郎は33点です。
二郎は48点です。
一郎は72点です。
三郎は96点です。

valueに基づいてkeyを並び替える

  • 昇順:hoge.keys.sort{|x,y| hoge[x]<=>hoge[y]} (| |内と後ろのxyの順が同じ)
  • 降順:hoge.keys.sort{|x,y| hoge[y]<=>hoge[x]} (| |内と後ろのxyの順が異なる)

ただしhogeはハッシュ変数とする。

valueが配列の場合のハッシュのソート

本日の冒頭で取り上げたソフトボールの結果を、勝ち数の多い順に並べ変えてみよう。今度はsort.rbとは異なりvalueが配列になっている。配列内には勝ち数、負け数、引分数の3つの値が代入され、1つのkeyに結び付けられている。並び替えを行う場合には、sort.rbと同様にソート基準ブロックを用いるが、3つあるvalueのうちの勝ち数に基づいてソートをするということで若干書き方が異なってくる。

具体的には以下のように書く。

soft.keys.sort {|a, b| soft[b][0] <=> soft[a][0]}

ここで変数abにはkeyであるチーム名が入る。soft[a]soft[b]はkeyに対応するvalueである。valueは3つの値を持ち、第0要素の勝ち数を基準とするため、soft[a][0]soft[b][0]として勝ち数を指定している。なお|a, b|と後半のabの順番が逆であるため降順に並べ替えが行われる。

これを踏まえてプログラムを書き直すと以下の通りとなる(softball2.rb)。

#!/usr/koeki/bin/ruby

soft = Hash.new

while line = gets
  if /(\S+)\s+(\d+)\s+(\d+)\s+(\d+)/ =~ line
    # 1個目の( ) (\S+)→チーム名が入る
    # 2個目の( ) (\d+)→勝ち数が入る
    # 3個目の( ) (\d+)→負け数が入る
    # 4個目の( ) (\d+)→引分数が入る
    soft[$1] = [$2.to_f,$3.to_f,$4] # 配列を代入
  end
end

print "--チーム名------------+-勝ち-+-負け-+-引分--+-勝率---\n"

for team in soft.keys.sort {|a, b| soft[b][0] <=> soft[a][0]}

  printf("%s%d%d%d%f\n", team, soft[team][0], soft[team][1], 
  soft[team][2], soft[team][0]/(soft[team][0]+soft[team][1]))
end

このプログラムを実行すると以下のように、勝ち数の大きい順に結果が出力される。

irsv{c10xxxx}% ruby softball2.rb softball.txt[Return]
--チーム名------------+-勝ち-+-負け-+-引分--+-勝率---
ドラゴンフライズ         78     51      7     0.605
ブックウォームス         75     48     13     0.610
アンツ              73     53     10     0.579
モスキートーズ          65     65      6     0.500
スネイルズ            52     82      2     0.388
クリケッツ            50     78      8     0.391

レポート課題

以下のうちいずれかを選んで解答する。

問題1(7点満点):softball2.rbをベースに勝ち3点、引き分け1点、負け0点として、これらを合計して算出した勝ち点の大きい順に結果を表示できるようにする(softball.txtは出席課題を行ったことにより8チームに増加している)。例えば5勝3敗2引き分けの場合、勝ち点は5(勝)×3(点)+2(引き分け)×1(点)=17点となる。結果を表示する際、printfの書式制御文字に数字をいれ、桁揃えして表示されるようにすること。

問題2(9点満点):課題2の問題2をハッシュを用いて書きなさい。


  • 提出先:naoya@e.koeki-u.ac.jp
  • 提出期限:11/11(日)23:59
  • メールのSubject:課題4
  • 本文の構成:1行目で学籍番号、氏名を記載する。2行目以降は下記の構成とする
  1. 改良したsoftball.txt
  2. 作成したプログラム
  3. プログラムの実行結果(問題2を選んだ場合も1つでよい)
  4. プログラムの説明
  5. 感想

  • 採点基準:期限内提出点(2点)、メールの体裁(1点)、プログラム(2or4点)、プログラムの説明(2点)
  • プログラムの説明:元のプログラムからの変更点について説明すればよいが、単に「AをBに変更した」というように変更箇所を明記するだけでは不十分である。変更した理由が分かるように説明する。説明箇所は少ないが、その分深さが求められる。
  • わかりにくい説明や、Webページを単にコピー&ペーストしただけの説明は減点することがある。一度読み直してから提出すること。
  • 驚異的に良くできているレポートについては満点を超える得点をつけることがある。
  • よくできていたレポートは、他の人の参考になるよう、本人が特定できないような形で掲載する。掲載してほしくない場合はメールでの課題提出時にその旨記載すること。

Tips:emacsでの日本語入力のオンオフはCtrl-oです

Tips:ktermでのプログラムの実行結果をメールに貼り付けるには、コピーしたい箇所をマウスで選択し、emacs(Mew)上でマウスの真ん中ボタンをクリックする

Tips:Mewによるメールの送り方はMewコマンドを参照