1. ホーム
  2. スクリプト・コラム
  3. パール
  4. アプリケーションのヒント

PerlのSort関数の使い方まとめと使用例

2022-02-12 20:46:13

(A) ソート関数の使用方法

ソートリスト
ソートブロック一覧
サブネームリストのソート

sortは上記のように3通りの使い方があります。LISTをソートして、ソート後のリストを返します。SUBNAME や BLOCK を無視した場合、ソートは標準的な文字列比較順 (例えば ASCII 順) で実行されます。SUBNAMEが指定された場合、それは実際には2つのリスト要素を比較し、要素がソートされる順序(昇順、定数、降順)に応じて、0より小さい、等しい、または大きい整数を返すサブファンクションの名前である。BLOCKはSUBNAMEの代わりに匿名サブファンクションとして提供することもでき、同じ効果があります。

比較される2つの要素は、一時的に変数 $a と $b に代入されます。これらは参照渡しなので、$aや$bを変更しないようにしましょう。サブファンクションを使用する場合、再帰的であってはなりません。

(b) 使用例

1. 数値順に並べる

コピーコード コードは以下の通りです。

@array = (8, 2, 32, 1, 4, 16);
print join(' ', sort {$a <=> $b} @array), "\n";

印刷結果は   
コピーコード コードは以下の通りです。
1 2 4 8 16 32

と同じです。

コピーコード コードは以下の通りです。
sub numerically { $a <=> $b };
print join(' ', sort numerically @array), "\n";

これはわかりやすく、自然数の順にソートしているだけなので、詳細は省略します。

2.1 ASCII順(辞書順ではない)でのソート

コピーコード コードは以下の通りです。

@languages = qw(fortran lisp c c++ Perl python java);
print join(' ', sort @languages), "\n";

結果を印刷します。

コピーコード コードは以下の通りです。
Perl c c++ fortran java lisp python

と同等である。

コピーコード コードは以下の通りです。
print join(' ', sort { $a cmp $b } @languages), "\n";

ASCII順に並べると、あまりああいうことは言わない。

数字をASCII順に並べ替えると、結果が思い通りにならないことがあることに注意してください:。

コピーコード コードは以下の通りです。

print join(' ', sort 1 . 11), "\n";
1 10 11 2 3 4 5 6 7 8 9

2.2 辞書順のソート

コピーコード コードは以下の通りです。

use locale;
@array = qw(ASCII ascap at_large atlarge A ARP arp);
@sorted = sort { ($da = lc $a) =~ s/[/W_]+//g;
          ($db = lc $b) =~ s/[/W_]+//g;
          $da cmp $db;
          } @array;
print "@sorted\n";

プリントアウトした結果は  

コピーコード コードは以下の通りです。
A ARP arp ascap ASCII atlarge at_large

use localeはオプションです。use localeは、生データに国際文字が含まれている場合に、コードの互換性を向上させます。use localeは、cmp, lt, le, ge, gtおよび他のいくつかの関数の操作プロパティに影響します。詳細はperllocaleのマニュアルページを参照してください。

atlargeとat_largeのソート順は同じですが、出力では順序が逆になっていることに注意してください(sortの途中にあるサブ関数がat_largeの途中にあるアンダースコアを削除しているのです)。これは、このサンプルがperl 5.005_02で実行されているために起こります。perl 5.6 より前のバージョンでは、sort 関数は同じ値を持つキーの順序を保護しませんでした。

一時変数 $_ (sort の $a と $b) は map、grep、sort のいずれによっても変更されないように保護されていることに注意してください。
このコードでは、$aや$bをs/[/W_]+/gに置き換える前に、それらを$daと$dbに再代入し、置き換えによって元の要素が変更されないようにします。

3. 降順で並べ替える
降順ソートは比較的簡単で、cmpや<=>の前後でオペランドを入れ替えるだけです。

コピーコード コードは以下の通りです。
sort { $b <=> $a } @array;

または、中間ブロックやサブファンクションの戻り値のトークンを変更する場合。
コピーコード コードは以下の通りです。
sort { -($a <=> $b) } @array;

あるいは逆関数を使う(ちょっと非効率ですが、読みやすいかもしれません)。
コピーコード コードは以下の通りです。
reverse sort { $a <=> $b } @array;

4. ソートに複数のキーを使用する
複数のキーでソートするには、サブファンクションでリンクされている比較演算をすべて入れればよい。主要な比較演算を最初に、マイナーな比較演算を2番目に配置します。

コピーコード コードは以下の通りです。

# An array of references to anonymous hashes
@employees = (
  { FIRST => 'Bill', LAST => 'Gates',
    SALARY => 600000, AGE => 45 }
  { FIRST => 'George', LAST => 'Tester'
    SALARY => 55000, AGE => 29 }
  { FIRST => 'Steve', LAST => 'Ballmer',
    SALARY => 600000, AGE => 41 }
  { FIRST => 'Sally', LAST => 'Developer',
    SALARY => 55000, AGE => 29 }
  { FIRST => 'Joe', LAST => 'Tester',
    SALARY => 55000, AGE => 29 }
);
sub seniority {
  $b->{SALARY} <=> $a->{SALARY}
  or $b->{AGE} <=> $a->{AGE}
  or $a->{LAST} cmp $b->{LAST}
  or $a->{FIRST} cmp $b->{FIRST}
}
@ranked = sort seniority @employees;
foreach $emp (@ranked) {
  print "$emp->{SALARY}/t$emp->{AGE}/t$emp->{FIRST}
    $emp->{LAST}\n";
}

印刷結果は

コピーコード コードは以下の通りです。
600000 45 Bill Gates
600000 41 Steve Ballmer
55000 29 Sally Developer
55000 29 George Tester
55000 29 Joe Tester

上のコードは複雑に見えますが、実はとても簡単に理解できます。例えば、$employees[0]-> {SALARY}のようにすると、最初の匿名ハッシュのSALARYに対応する値にアクセスすることができます。つまり、上記の比較は明確です。まずSALARYの値を比較し、次にAGEの値を比較し、次にLASTの値を比較し、最後にFIRSTの値を比較しています。最初の2つの比較は降順で、最後の2つは昇順なので、混乱しないように注意してください。

5. 新しい配列の並べ替え

コピーコード コードは以下の通りです。

@x = qw(matt elroy jane sally);
@rank[sort { $x[$a] cmp $x[$b] } 0 ... $#x] = 0 ... $#x;
print "@rank\n";

印刷結果は   

コピーコード コードは以下の通りです。
2 0 1 3

ちょっとわかりにくいですか?よく見ると、はっきりしますよ。0 ... $#x は @x 配列の添え字を値とするリストで、この例では 0 1 2 3 です。つまり、sort の結果は、@x の添え字をソートしたリストを返します。ソートの基準は、その添え字に対応する@xの要素のASCII順である。
sortが何を返すのか、まだ理解していないのでしょうか?まず、@xの要素のASCII順を出力してみましょう。

コピーコード コードは以下の通りです。

@x = qw(matt elroy jane sally);
print join ' ',sort { $a cmp $b } @x;

印刷結果は 

コピーコード コードは以下の通りです。
elroy jane matt sally

それらに対応する@xの添え字は1 2 0 3なので、上記のソートは1 2 0 3のリストを返します。rank[1 2 0 3] = 0 ... $#x は単純な配列の代入操作です。
つまり、@rankの結果は(2 0 1 3)です。

6. ハッシュをキーでソートする

コピーコード コードは以下の通りです。

%hash = (Donald => Knuth, Alan => Turing, John => Neumann);
@sorted = map { { ($_ => $hash{$_}) } } sort keys %hash;
foreach $hashref (@sorted) {
  ($key, $value) = each %$hashref;
  print "$key => $value\n";
}

印刷結果は

コピーコード コードは以下の通りです。
Alan => Turing
Donald => Knuth
John => Neumann

上のコードは難しいものではありません。 sort keys %hash は %hash による ASCII 順のキーのリストを返し、それを計算するために map を使います。ここで map は double {{}} を使っていることに注意してください。
この{}は匿名ハッシュで、つまりmapの結果は匿名ハッシュリストなんだよ、わかる?
つまり、@sorted配列の要素は個々の匿名ハッシュであり、%$hashrefでバックリファレンスすることでそのキー/バリューの値にアクセスすることができます。

7. ハッシュを値でソートする

コピーコード コードは以下の通りです。

%hash = ( Elliot => Babbage,
      Charles => Babbage,
      Grace => Hopper,
      Herman => Hollerith
    );
@sorted = map { { ($_ => $hash{$_}) } }
        sort { $hash{$a} cmp $hash{$b}
              or $a cmp $b
            } keys %hash;
foreach $hashref (@sorted) {
  ($key, $value) = each %$hashref;
  print "$key => $value\n";
}

印刷結果は

コピーコード コードは以下の通りです。
Charles => Babbage
Elliot => Babbage
Herman => Hollerith
Grace => Hopper

ハッシュキーと違って、ハッシュ値の一意性を保証することはできない。ハッシュを値だけでソートすると、他の値を追加したり削除したりしたときに、同じ値を持つ2つの要素のソート順が変わってしまう可能性がある。安定した結果を得るためには、値をマスターソートし、キーをスレーブソートする必要があります。

ここでは、{ $hash{$a} cmp $hash{$b} or $a cmp $b } で、値によるソートとキーによるソートを2回行います。ソートの結果はキーのソートされたリストで、それをmapに渡して計算させ、匿名ハッシュリストを返します。アクセス方法は前回と同じなので、詳細は省略します。

8. ファイル内の単語をソートし、重複を削除する。

コピーコード コードは以下の通りです。

perl -0777ane '$, = "\n"; @uniq{@F} = (); print sort keys %uniq' file

この使い方は、私にもよく理解できません。
uniq{@F} = () は、ハッシュスライスを使用して、ファイル内の単語のみをキーとするハッシュを作成します。
この使い方は、意味的には $uniq{ $F[0], $F[1], ... $F[$#F] } と等価です。= ()

オプションは以下のように記述されています。

コピーコード コードは以下の通りです。
-0777 - Read the whole file, not a single line
-a - auto split mode, splitting lines into @F arrays
-e - read and run scripts from the command line
-n - Iterate through the file line by line: while (<>) { ... }
$, - the output field splitter of the print function
file - the name of the file

9. 効率的なソート OrcishアルゴリズムとSchwartzian変換

sortのサブファンクションは通常、各キーに対して複数回呼び出されます。もし、sort の実行時間を非常に気にするのであれば、Orcish アルゴリズムか Schwartzian 変換を使って、各キーが一回だけ計算されるようにすることができます。
次の例では、ファイルのリストを変更日に基づいてソートしています。

コピーコード コードは以下の通りです。
# Forced algorithm - multiple accesses to disk for each file
@sorted = sort { -M $a <=> -M $b } @filenames;

# Orcish algorithm - create keys in the hash
@sorted = sort { ($modtimes{$a} ||= -M $a) <=>
          ($modtimes{$b} ||= -M $b)
          } @filenames;


巧妙なアルゴリズムですね?とても賢いアルゴリズムでしょう?なぜなら、ファイルの修正日はスクリプト実行中は基本的に一定なので、一度-M操作をした後は、保存すれば完了です。
シュワルツェン変換の使い方はこうです。

コピーコード コードは以下の通りです。
@sorted = map( { $_->[0] }
          sort( { $a->[1] <=> $b->[1] }
              map({ [$_, -M] } @filenames)
            )
        );

map({ [$_, -M] } @filenames) はリストを返します。その要素は無名配列で、最初の値はファイル名、2番目の値はファイルの更新日です。

sort( { $a->[1] <=> $b->[1] }...。次に、上で生成された匿名配列リストを、ファイルの修正日を基準にソートします。
sortが返す結果は、ソートされた無名配列である。

一番外側のmap( { $_->[0] }... はシンプルで、上のsortで生成された無名配列からファイル名を取り出しています。このファイル名は、修正日ヤに基づいてソートされたもので、1ファイルにつき1回だけ実行されます -M。
これは海外のperlユーザーの間で有名な使い方であるSchwartzian変換です