PHP で、連想配列の配列を効率よくソートする

PHPで、ファイル名を含む連想配列の配列があるとして、

<?php
$data = array(
  array('id'=>101, 'filename'=>'foo.jpg'),
  array('id'=>104, 'filename'=>'bar.jpg'),
  array('id'=>109, 'filename'=>'baz.jpg'),
);
?>

これをファイルサイズでソートしたいとする。
この場合、比較関数を定義して、その関数名をusort()に指定するのが一般的な方法である。

<?php
// 比較関数
function compare_by_filesize($item1, $item2) {
  $size1 = filesize($item1['filename']);
  $size2 = filesize($item2['filename']);
  if ($size1 < $size2) return -1;
  if ($size1 > $size2) return 1;
  return 0;
}
// 比較関数を指定してソートする
$sorted = $data;
usort($sorted, 'compare_by_filesize');
var_export($sorted);
?>

しかし、これだと比較のたびにfilesize()が2回も呼び出され、効率が悪い。*1

そこで、まず最初にすべてのファイル名についてfilesize()を呼び出し、あらかじめファイルサイズを求めておく。そうしてからソートしたほうが、filesize()の呼び出しが大幅に少なくなるので、実行効率がいい。

<?php
// 最初にファイルサイズを求めておく
$sizes = array();  // キーはfilesize、値は$dataのindex
foreach ($data as $i=>$item) {
    $size = filesize($item['filename']);
    $sizes[$size] = $i;
}
// ファイルサイズでソートする
ksort($sizes);      // キーの順番でソートする
// ソートした順番で$dataから取りだす
foreach ($sizes as $i) {
    $sorted[] = $data[$i];
}
var_export($sorted);
?>

ポイントとなるのは、ksort() である。これは連想配列をキーの順番でソートする。例えば:

<?php
$arr = array(
  'foo'=>10,
  'bar'=>20,
  'baz'=>30,
);
ksort($arr);
var_export($arr);
//// 結果:
//  array (
//    'bar' => 20,
//    'baz' => 30,
//    'foo' => 10,
//  )
?>

PHP連想配列は、Ruby1.8のHashやPythonのdictと違って、キーの順番が保存される。そのため、ksort()のような関数が用意されている。

ちなみに、上で紹介した方法は「シュワルツ変換(Schwartz Transform)」と呼ばれ、RubyPythonではそれ用のメソッドや関数が用意されている。

## Ruby
data = [
  {'id'=>101, 'filename'=>'foo.jpg'},
  {'id'=>104, 'filename'=>'bar.jpg'},
  {'id'=>109, 'filename'=>'baz.jpg'},
]
sorted = data.sort_by {|item| File.size(item['filename']) }
p sorted
## Python
data = [
  {'id':101, 'filename':'foo.jpg'},
  {'id':104, 'filename':'bar.jpg'},
  {'id':109, 'filename':'baz.jpg'},
]
from os.path import getsize
sorted = sorted(data, key=lambda item: getsize(item['filename']))
print repr(sorted)

PHP では、そのものズバリという関数は用意されていないが、array_multisort() が使える。

<?php
// 最初にファイルサイズを求めておく
foreach ($data as $item) {
    $sizes[] = filesize($item['filename']);
}
$sorted = $data;
// $sizesをソートするときに、その順番で$sortedも入れ替える
array_multisort($sizes, $sorted);
var_export($sorted);
?>

む、こっちのほうが簡単だったか。

*1:実は、PHPではfilesize()の結果はキャッシュされるので、この方法でもそんなに効率が悪いわけではない。たぶん。