使用PHP的uasort进行排序时保留键的顺序(稳定排序)

25

这个问题其实是受到SO上另一个问题的启发,我想对其进行一些扩展。

在PHP中有一个关联数组,是否可以使用PHP内置的排序函数之一(或多个)对其值进行排序,但当值相等时保留原始键顺序?

这是我用来测试可能解决方案的脚本(没有找到任何解决方案):

<?php
header('Content-type: text/plain');
for($i=0;$i<10;$i++){
    $arr['key-'.$i] = rand(1,5)*10;
}
uasort($arr, function($a, $b){
    // sort condition may go here //
    // Tried: return ($a == $b)?1:($a - $b); //
    // Tried: return $a >= $b; //
});
print_r($arr);
?>

陷阱:由于键在原始数组中是有序的,请不要尝试建议通过键排序来恢复到原始顺序。我将它们排序后进行示例,以便更容易地在输出中检查它们的顺序。


9
换句话说,这个问题的解决方案是一个“稳定”的排序算法,而PHP的任何排序算法都不是稳定的,至少表面上看是这样。 - BoltClock
2
只使用内置函数有什么理由吗? - shamittomar
1
如果这些排序函数中的任何一个将两个成员评估为相等,则顺序是未定义的(排序不稳定)。请参考:http://www.php.net/manual/zh/array.sorting.php - ajreal
3
请查看这个链接:http://notmysock.org/blog/php/schwartzian-transform.html,它解决了我的问题。 - eisberg
1
整个页面现在已经过时。请查看https://wiki.php.net/rfc/stable_sorting#:~:text=A%20stable%20sort%20guarantees%20that,some%20part%20of%20that%20data. 稳定排序已经实现了3年。 - mickmackusa
显示剩余2条评论
6个回答

28

由于在 PHP 4.1.0 之后,PHP 不支持稳定排序(stable sort), 因此您需要编写自己的函数。

这似乎可以满足您的要求:http://www.php.net/manual/en/function.usort.php#38827

正如手册所说,“如果两个成员相等,则它们在排序后的数组中的顺序是未定义的。” 这意味着使用的排序不是“稳定的”(stable),并且可能更改比较相等的元素的顺序。

有时您确实需要进行稳定排序。 例如,如果您按一个字段对列表进行排序,然后再按另一个字段进行排序,但不想失去先前字段的排序顺序。 在这种情况下,最好使用带有比较函数的 usort,该函数考虑了两个字段,但如果您无法执行此操作,则使用下面的函数。 它是合并排序(merge sort),保证复杂度为 O(n*log(n)),这意味着即使使用更大的列表,它也保持相当快速(与气泡排序和插入排序不同,它们是 O(n^2))。

<?php
function mergesort(&$array, $cmp_function = 'strcmp') {
    // Arrays of size < 2 require no action.
    if (count($array) < 2) return;
    // Split the array in half
    $halfway = count($array) / 2;
    $array1 = array_slice($array, 0, $halfway);
    $array2 = array_slice($array, $halfway);
    // Recurse to sort the two halves
    mergesort($array1, $cmp_function);
    mergesort($array2, $cmp_function);
    // If all of $array1 is <= all of $array2, just append them.
    if (call_user_func($cmp_function, end($array1), $array2[0]) < 1) {
        $array = array_merge($array1, $array2);
        return;
    }
    // Merge the two sorted arrays into a single sorted array
    $array = array();
    $ptr1 = $ptr2 = 0;
    while ($ptr1 < count($array1) && $ptr2 < count($array2)) {
        if (call_user_func($cmp_function, $array1[$ptr1], $array2[$ptr2]) < 1) {
            $array[] = $array1[$ptr1++];
        }
        else {
            $array[] = $array2[$ptr2++];
        }
    }
    // Merge the remainder
    while ($ptr1 < count($array1)) $array[] = $array1[$ptr1++];
    while ($ptr2 < count($array2)) $array[] = $array2[$ptr2++];
    return;
}
?>

另外,您可能会发现这个论坛帖子很有趣。


1
论坛帖子不太有趣,但是你回答的其他部分很有意思。 - Alin Purcaru
1
@Alin,这就是为什么我写了“可能会觉得有趣...” :) - shamittomar
1
@Alin,实际上这得出了一个结论,即所有在那里描述的方法都不起作用。因此,在某种程度上,我们知道不要尝试这些方法。从这个意义上说是有帮助的。 - shamittomar
我认为你应该注明你的来源。我在这里发现了重复的方法:https://dev59.com/nGfWa4cB1Zd3GeqPkcpe#12163551。 - Tyler Collier
请查看@Martijn的答案,该答案使用装饰器和uasort与包装的比较函数,并且只需要1/5的时间,而这个答案(虽然是一个很好的例子,但效率低下)。 - Mike
如果没有这样的键,$array2[0]会失败。似乎这种解决方案有一个需要说明的限制。 - ChrisJJ

11

array_multisort 非常有用,只需将有序范围作为第二个数组即可($order 只是临时变量,它用于按原始顺序对第一个数组的等效项进行排序):

$a = [
  "key-0" => 5,
  "key-99" => 3,
  "key-2" => 3,
  "key-3" => 7
];

$order = range(1,count($a));
array_multisort($a, SORT_ASC, $order, SORT_ASC);

var_dump($a);

输出

array(4) {
  ["key-99"]=>
  int(3)
  ["key-2"]=>
  int(3)
  ["key-0"]=>
  int(5)
  ["key-3"]=>
  int(7)
}

我使用了未排序的键测试数据来展示它可以正确工作。尽管如此,这是你的测试脚本输出:

Array
(
    [key-1] => 10
    [key-4] => 10
    [key-5] => 20
    [key-8] => 20
    [key-6] => 30
    [key-9] => 30
    [key-2] => 40
    [key-0] => 50
    [key-3] => 50
    [key-7] => 50
)

缺点

它只能使用预定义的比较方法,无法使用自己的比较函数。可能的取值(array_multisort() 的第二个参数)包括:

排序类型标志位

  • SORT_ASC - 升序排列。
  • SORT_DESC - 降序排列。
  • SORT_REGULAR - 普通比较(不更改类型)。
  • SORT_NUMERIC - 数字比较。
  • SORT_STRING - 字符串比较。
  • SORT_LOCALE_STRING - 基于当前区域设置进行字符串比较。它使用可以使用 setlocale() 更改的区域设置。
  • SORT_NATURAL - 使用“自然排序”比较字符串,类似于 natsort()
  • SORT_FLAG_CASE - 可以与 SORT_STRINGSORT_NATURAL 结合(按位或)以对字符串进行不区分大小写的排序。

您还可以使用array_sort($a,SORT_ASC,array_keys($a),SORT_NATURAL)进行类似的稳定排序。这将会把[ 'Sick' => 8, 'Vacation' => 12, 'Other' => -4, 'Holiday' => 0, 'Bereavement' => 0 ]变成[ 'Other' => -4, 'Bereavement' => 0, 'Holiday' => 0, 'Sick' => 8, 'Vacation' => 12 ] - Will B.

5

为了完整起见,您还应该查看施瓦茨变换

// decorate step
$key = 0;
foreach ($arr as &$item) {
        $item = array($item, $key++); // add array index as secondary sort key
}

// sort step
asort($arr); // sort it

// undecorate step
foreach ($arr as &$item) {
    $item = $item[0]; // remove decoration from previous step
}

PHP的默认排序算法对于数组来说表现良好,这是因为:
array(1, 0) < array(2, 0); // true
array(1, 1) < array(1, 2); // true

如果您想使用自己的排序标准,也可以使用uasort()

// each parameter is an array with two elements
// [0] - the original item
// [1] - the array key
function mysort($a, $b)
{
    if ($a[0] != $b[0]) {
        return $a[0] < $b[0] ? -1 : 1;
    } else {
        // $a[0] == $b[0], sort on key
        return $a[1] < $b[1] ? -1 : 1; // ASC
    }
}

请注意问题中的“陷阱”。您不应该依赖原始数组中的键值有序。 - Alin Purcaru
1
@AlinPurcaru 好的,没问题。已经编辑好了 :) - Ja͢ck

1

这是一种解决方案,可以在usort函数中实现稳定排序。

public function sortBy(array &$array, $value_compare_func)
{
    $index = 0;
    foreach ($array as &$item) {
        $item = array($index++, $item);
    }
    $result = usort($array, function($a, $b) use ($value_compare_func) {
        $result = call_user_func($value_compare_func, $a[1], $b[1]);
        return $result == 0 ? $a[0] - $b[0] : $result;
    });
    foreach ($array as &$item) {
        $item = $item[1];
    }
    return $result;
}

0

作为稳定排序的解决方法:

<?php
header('Content-type: text/plain');
for ($i = 0;$i < 10;$i++)
{
    $arr['key-' . $i] = rand(1, 5) * 10;
}
uksort($arr, function ($a, $b) use ($arr)
{
    if ($arr[$a] === $arr[$b]) return array_search($a, array_keys($arr)) - array_search($b, array_keys($arr));
    return $arr[$a] - $arr[$b];
});
print_r($arr);

比较器中的4个嵌套的array_search/array_keys调用从时间复杂度的角度来看确实会带来很大的影响。这个答案本质上是在排序之前一次编码索引,然后在之后剥离它们,这两个操作都是线性的,不会破坏底层的复杂度。另一方面,像这里展示的每次比较都遍历数组,将其提升到O(n * n * log(n))——比冒泡排序还要糟糕,但如果只有几个元素,那就没问题了。 - ggorlen

-1

只是为了补充一些非常具体的情况。如果$array的数组键是默认值,则简单的array_values(asort($array))就足够了(这里举例是升序)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接