More Efficient Solution
$dict = array_flip($order);
$positions = array_map(function ($elem) use ($dict) { return $dict[$elem['id']] ?? INF; }, $array);
array_multisort($positions, $array);
Don't recalculate positions on every comparison
When your array is large or getting the id is costlier, using usort()
can get bad, because you recalculate the id for each comparison. Try array_multisort()
with pre-calculated positions (see mediumsort
or fastsort
in the example below), which isn't any more complicated.
Also searching for the id in the order array on each comparison (like in the accepted answer) doesn't improve performance, since you iterate over it every comparison. Calculate it once.
In the code snippet below you can see the main three sorting functions:
slowsort
The accepted answer. Searches the position on every comparison.
mediumsort
Improved slowsort
by calculating the positions in advance
fastsort
Improved mediumsort
by avoiding searching alltogher.
Note that these handle elements with an id not in given in the order by providing a fallback value of INF
. If your order array matches the ids of the original array 1-to-1, then avoid sorting alltogether and just inserting the elements in the right position is the way to go. I added a function cheatsort
that does exactly that.
You can more generally sort an array by a weight (see weightedsort
in the example). Make sure to calculate the weight only once, to achieve good performance.
Performance (for an array of length 1000)
fastsort about 1 ms
mediumsort about 3 ms
slowsort about 60 ms
Hint: For larger arrays the difference gets worse.
Sorting Function Comparison
<?php
/**
* accepted answer
*
* re-evaluate position in order on each comparison
*/
function slowsort(&$array, $order, $key = 'id')
{
usort($array, function ($a, $b) use ($order, $key) {
$pos_a = array_search($a[$key], $order);
$pos_b = array_search($b[$key], $order);
return $pos_a - $pos_b;
});
}
/**
* calculate element positions once
*/
function mediumsort(&$array, $order, $key = 'id')
{
$positions = array_map(function ($elem) use ($order, $key) {
return array_search($elem[$key], $order);
}, $array);
array_multisort($positions, $array);
}
/**
* calculate positions without searching
*/
function fastsort(&$array, $order, $key = 'id')
{
$dict = array_flip($order);
$positions = array_map(function ($elem) use ($dict, $key) {
return $dict[$elem[$key]] ?? INF;
}, $array);
array_multisort($positions, $array);
}
/**
* when each order element gets used exactly once, insert elements directly
*/
function cheatsort(&$array, $order, $key = 'id')
{
$dict = array_flip($order);
$copy = $array;
foreach ($copy as $elem) {
$pos = $dict[$elem[$key]];
$array[$pos] = $elem;
}
}
/**
* Sort elements in $array by their weight given by $weight_func
*
* You could rewrite fastsort and mediumsort by replacing $position by a weight function
*/
function weightedsort(&$array, $weight_func)
{
$weights = array_map($weight_func, $array);
array_multisort($weights, $array);
}
/**
* MEASUREMENTS
*/
/**
* Generate the sorting problem
*/
function generate($size = 1000)
{
$order = array();
$array = array();
for ($i = 0; $i < $size; $i++) {
$id = random_int(0, PHP_INT_MAX);
$order[] = $id;
$array[] = array('id' => $id);
}
shuffle($order);
return [$array, $order];
}
/**
* Time $callable in ms
*/
function time_it($callable)
{
$then = microtime(true);
$callable();
$now = microtime(true);
return 1000 * ($now - $then);
}
/**
* Time a sort function with name $sort_func
*/
function time_sort($sort_func)
{
echo "Timing $sort_func", PHP_EOL;
[$array, $order] = generate();
echo time_it(function () use ($sort_func, &$array, $order) {
$sort_func($array, $order);
}) . ' ms' . PHP_EOL;
}
time_sort('cheatsort');
time_sort('fastsort');
time_sort('mediumsort');
time_sort('slowsort');