大学の課題

標準入力から取り込んだテキストファイル中に含まれる単語の数を数え,
単語と出現回数を出現頻度順に(降順に)ソートして出力しろ.
ただし,単語の大文字,小文字は区別せず,出力時には全て小文字で出力すること.

こんな課題が大学の講義で出されました.perlを使ってCGIを作る練習のようです.
教官のコメントには

・最短なら10行程度で書ける。

とあります.挑戦してみましょう.

  • 注意
    信州大学のティーチングアシスタントの方から
    「このサイトのコードをコピペする学生が沢山いる」
    という情報をいただきました.やめましょう.
    ぱくるなら,オリジナリティーを追加してからどうぞ^^
    記念にコメントもどうぞ
  • 丁寧な解説に感謝感謝です。ぱくりません! -- 信大生50号 2009-06-23 10:56:21 (火)
  • 丁寧にありがとうございます! -- 信大生 2014-06-24 11:04:43 (火)
  • 丁寧にありがとうございます! -- 信大生 2014-06-24 12:04:05 (火)
  • この課題まだでてるのか。。。 -- Naoki 2014-09-25 19:37:36 (木)
  • ありがとうございます -- ほおーー 2016-06-22 00:59:44 (水)
  • 参考にさせていただきます -- 信大生 2016-06-24 11:56:48 (金)

お名前:

まずは素直に

最初に書いたプログラムはこんな感じ(だった気がする)

while(<STDIN>){
    while(/([a-z0-9']+)/gi){
        $wc{lc($1)}++;
    }
}
@sorted=sort {$wc{$b}<=>$wc{$a}} keys(%wc);
foreach(@sorted){
    print "$_ $wc{$_}\n";
}

私がこのプログラムを書く為に最初に調べたのは標準出力に文字を出す関数でした^^;
久しぶりのperlなので不安です.

解説

while(<STDIN>){
    while(/([a-z0-9']+)/gi){
        $wc{lc($1)}++;
    }
}

最初のループでは標準入力を1行ずつ見ていきます.
その中のループでは正規表現を使って単語を抜き出していきます.
簡単簡単. 残すはソートと出力です.
ソートは

@sorted=sort {$wc{$b}<=>$wc{$a}} keys(%wc);

で簡単にできます.友人で10行以上かかった人達はこれが思い付かなかったそうです.

連想配列のソート

sort関数は配列をソートした結果を返してくれます.

@ret=sort @arg;

これが一番簡単な使い方です.argをそれっぽく昇順でソートしてくれます.
今回は連想配列の値でソートしたいので,連想配列の値を配列として取り出すvalues関数を使い

 @sorted=sort values(%wc);

としてみます.これで大丈夫でしょうか?
このようにすると,sortedにはソートされた連想配列の値(単語の出現数)が入ることになります.
しかし,今回必要なのは,(出現数でソートされた)単語です.これを作るにはsort関数の別の使い方を学ぶ必要があります.

sort {比較の仕方} 対象の配列;

比較の仕方は2つの変数($aと$bになります)を比較する時に,どちらの変数を前に置くかを決めてやります. 比較の仕方を書くブロックでは,$aを前にする時は0未満の値,$bを前にする時は0より大きい値,同じ時は0を返してやります.
この比較の仕方を工夫すれば値でキーをソートしてやることができます.こんな感じです.

{ $wc{$b}<=>$wc{$a} }

keys関数は連想配列の鍵を配列にして返します.比較される変数aとbには,
鍵が入ってるので,$wc{$a}として値を取り出します.で,それらを比較してやればOKです.~ つまり,連想配列「wc」を鍵でソートするには

@sorted=sort {$wc{$b}<=>$wc{$a}} keys(%wc);

としてやりましょう.これでソートも完了です.

表示

これで@sortedには出現頻度の多い順に単語が入ることになります.
後はこれを出現回数と一緒に表示してやれば終わりです.

foreach(@sorted){
    print "$_ $wc{$_}\n";
}

簡単簡単.

1行プログラミングに挑戦

最初に書いたプログラムですぐに教官の言う最短は達成できましたが,まだまだ短くできそうな雰囲気です.
と言う事で1行を目指して見ましょう.
1行と言っても,全ての改行を消した状態で1行に収めるのが目標です.
ちなみにUNIXの標準的なターミナルが1行に表示可能な文字数は80文字なので,これを1行とします.
まずは,とにかく短くする為にちょっといじります.

  • @sortedを使わず直接foreachに突っ込む
  • 正規表現を簡略化
  • <STDIN>を<>にする(ちょっと動作が違うけれど)
  • 数値の比較は引き算で済ませる
  • 変数名を一文字にする
  • 関数の括弧をはずす

これで

while(<>){
    while(/\w+/g){
        $w{lc$&}++
    }
}
foreach(sort{$w{$b}-$w{$a}}keys%w){
    print"$_ $w{$_}\n"
}

こうなります.これで改行やタブを消してやれば90文字になります.
あとはループが多くて気になります.
置換演算子s///のeオプションを使って,

while(/\w+/g){
	$w{lc$&}++
}

これを

s/\w+/$w{lc$&}++/ge

これだけにする事ができます.\w+にマッチすると,それを$w{lc$&}++に置き換えようとしますが,
この時eオプションを使うと,この部分を文字列ではなく式として評価してくれるので,
この部分が実行されるのです.よって,全ての\w+にマッチした部分に対して処理をする事ができます.すると

while(<>){
    s/\w+/$w{lc$&}++/ge
}
foreach(sort{$w{$b}-$w{$a}}keys%w){
    print"$_ $w{$_}$/"
}

かなりすっきりしました.これが6文字の節約になって84文字です.
あと少し・・・ だったのですが,ここが私の限界でした.
しかし,ばりばりperlのプログラマーK-Kさんが1行に収めたそうです.
それがこれ(私の記憶の中にある物ですが^^; ちょっと違うかも)

map{s/\w+/$w{lc$&}++/ge}<>;
map{print"$_ $w{$_}\n"}sort{$w{$b}-$w{$a}}keys%w

76文字です.1行に収まります.
mapという関数は配列の要素それぞれに対して処理をする事のできる関数だそうです.
foreachみたいな事ができる関数みたいです.勉強になりました.
1行に収まった物でも意外と可読性があるように感じるのは気のせいでしょうか?
可読性がある内は短くできる気がします.もっとすごい物募集中です(笑

コメント

  • ココに書き込めるかテストです.スミマセン. -- KW 2009-06-25 20:01:15 (木)
  • s///eオプションが目から鱗でした。有難うございます。 -- 08T 2010-06-13 02:08:55 (日)
  • 一行にまとめるのが通ですね -- とおりがかり 2012-10-31 18:07:49 (水)
  • 参考にします! -- 14T 2016-06-08 15:09:41 (水)

お名前: