先日プログラムを書いていたら、こんな問題に出くわした。(実際の件数とかは別)
あるデータ10個が0点から5点の範囲で得点をとります。
この時上位5件のデータを取得すること。ただし、同点のデータがあれば、ランダムに選ぶこと。
出題されたとおりに真面目にプログラムすると、こんなコードになる。
use strict;
my $nSelect = 5;
my @aryData = (
{ name => "DATA A", point => 2 },
{ name => "DATA B", point => 4 },
{ name => "DATA C", point => 1 },
{ name => "DATA D", point => 0 },
{ name => "DATA E", point => 5 },
{ name => "DATA F", point => 2 },
{ name => "DATA G", point => 2 },
{ name => "DATA H", point => 3 },
{ name => "DATA I", point => 3 },
{ name => "DATA J", point => 0 },
);
# 選択先の配列を用意
my @arySelect;
# 降順に並べる
my @arySort = sort{$b->{point} <=> $a->{point}} @aryData;
# 選択データが一杯でなければ
while( scalar(@arySelect) < $nSelect){
my $nPoint = $arySort[0]->{point};
my @aryTemp = grep{$_->{point} == $nPoint;} @arySort;
# 同じ値の一群が全部入りきるなら
if( scalar(@aryTemp) < $nSelect - scalar(@arySelect)){
# 選択用の配列に追加
push @arySelect, @aryTemp;
# 元の配列から削除
splice @arySort, 0, scalar(@aryTemp);
}
else {
while( scalar(@arySelect) < $nSelect){
# 候補をランダムに選ぶ
my $idx = int(rand(@aryTemp));
push @arySelect, $aryTemp[$idx];
splice @aryTemp, $idx, 1
}
}
}
しかしこれでは、効率も悪いしコード量も結構な行数で、コメントを外した状態でも16行もある。
そこで次のように考える。
まず、それぞれのデータに乱数でサブになるポイントを付ける。得点でソートして、さらに、乱数のポイントでソートします。このリストの先頭5件を選択します。
# randデータを追加する
map { $_->{rand} = int(rand(1000))} @aryData;
# ポイントと乱数データを降順にソート
my @arySort = sort{
# まず得点で比較
if(( $b->{point} != $a->{point} ){
( $b->{point} <=> $a->{point}
}
#
else {
$b->{rand} <=> $a->{rand};
}
}@aryData;
# 上位5位まで選択
my @arySelect = @arySort[0..$nSelect-1];
こうすると、10行くらいになります。
さらに一歩進めて、ポイントと乱数によるポイントを1回で決めてしまおう。ポイントを1000倍して、乱数を0から999までの乱数を加える。こうすることでことで、比較は1回で済むことになり、たった3行になる。
# rankデータを追加する
map { $_->{rank} = $_->{point}*1000+ int(rand(1000))} @aryData;
# ランクを降順にソート
my @arySort = sort{$b->{rank} <=> $a->{rank}}@aryData;
# 上位5位まで選択
my @arySelect = @arySort[0..$nSelect-1];
プログラムは、誰が書いても同じにならない。ちょっとしたアイディアの差が出るのです。