按照自定义顺序对php数组中的数组进行排序

78

我有一个二维数组:

Array ( 
    [0] => Array (
        [id] = 7867867,
        [title] = 'Some Title'),
    [1] => Array (
        [id] = 3452342,
        [title] = 'Some Title'),
    [2] => Array (
        [id] = 1231233,
        [title] = 'Some Title'),
    [3] => Array (
        [id] = 5867867,
        [title] = 'Some Title')
)

如何按特定顺序进行排序?

  1. 3452342
  2. 5867867
  3. 7867867
  4. 1231233

我该怎么做呢?我以前曾经对数组进行过排序,并阅读了很多其他帖子,但它们总是基于比较(即valueA < valueB)。

感谢您的帮助。


你如何知道你的需求应该是什么顺序? - Telshin
3
@Telshin他只是知道而已,好吗? :) 例如,我在csv导出中有一个默认字段顺序。这个顺序有些随意(至少不是按字母顺序排列的)。但我仍然需要对其他数组进行排序以匹配它。 - Buttle Butkus
8个回答

170
您可以使用usort()来精确指定数组的排序方式。在这种情况下,$order数组可以在比较函数中使用。
下面的示例使用closure使生活更加轻松。
$order = array(3452342, 5867867, 7867867, 1231233);
$array = array(
    array('id' => 7867867, 'title' => 'Some Title'),
    array('id' => 3452342, 'title' => 'Some Title'),
    array('id' => 1231233, 'title' => 'Some Title'),
    array('id' => 5867867, 'title' => 'Some Title'),
);

usort($array, function ($a, $b) use ($order) {
    $pos_a = array_search($a['id'], $order);
    $pos_b = array_search($b['id'], $order);
    return $pos_a - $pos_b;
});

var_dump($array);
这可以实现的关键是将被比较的值作为$order数组中的id位置。
比较函数通过查找要比较的两个项目的id在$order数组中的位置来工作。如果$a['id']$order数组中位于$b['id']之前,则函数的返回值将为负数(即$a$小于$b$,所以“浮动”到顶部)。如果$a['id']$b['id']之后,则函数返回正数(即$a$大于$b$,所以“下沉”)。
最后,使用闭包没有特殊原因;这只是我快速编写这些一次性函数的首选方式。它同样可以使用普通命名函数。

1
太棒了。谢谢。现在如果我向数组中添加项目而不是排序,会发生什么?从逻辑上讲,只要它出现在我指定的项目之后,我不关心它们的顺序。我该如何处理这个问题? - Honus Wagner
如果我有一个简单的数组:$array = (0=> values, 1=>values,2=>values,etc),并给出了数组(5,2,4,1,3,0),最好的解决方案是什么? - John
2
好的,在短短5秒钟的搜索后,我找到了这个链接:https://dev59.com/f3RC5IYBdhLWcg3wSu97。 - John
如果您想按一个没有唯一值的键进行排序怎么办?例如,如果您按标题排序,但可能有多个相同的标题。在这种情况下可能还好,但可能会有其他键,如日期或作者。 - Matt
如果其中一个是可空的怎么办?比如id 7867867不存在,但你仍然想考虑顺序。 - Kamel Mili
这个方法对我来说完美运作,但它不能用于关联数组。我有一个简单的关联数组,像这样操作:usort($array, function ($a, $b) { return $a - abs($b); }); - Danish Mehmood

29

扩展salathe的答案,以满足以下额外需求:

现在,如果我将项目添加到数组而不是排序,会发生什么情况? 我不关心它们出现的顺序,只要它们出现在我指定的项目之后即可。

您需要在排序函数中添加两个附加条件:

  1. “不关心”项目必须被视为大于“自定义”项目
  2. 两个“不关心”项目必须被视为相等(您可以为此情况添加一个打破平局的条件)

因此,修改后的代码如下:

$order = array(
    3452342,
    5867867,
    7867867,
    1231233
);
$array = array(
    array("id" => 7867867, "title" => "Must Be #3"),
    array("id" => 3452342, "title" => "Must Be #1"),
    array("id" => 1231233, "title" => "Must Be #4"),
    array("id" => 5867867, "title" => "Must Be #2"),
    array("id" => 1111111, "title" => "Dont Care #1"),
    array("id" => 2222222, "title" => "Dont Care #2"),
    array("id" => 3333333, "title" => "Dont Care #3"),
    array("id" => 4444444, "title" => "Dont Care #4")
);

shuffle($array);  // for testing
var_dump($array); // before

usort($array, function ($a, $b) use ($order) {
    $a = array_search($a["id"], $order);
    $b = array_search($b["id"], $order);
    if ($a === false && $b === false) { // both items are dont cares
        return 0;                       // a == b (or add tie-breaker condition)
    } elseif ($a === false) {           // $a is a dont care
        return 1;                       // $a > $b
    } elseif ($b === false) {           // $b is a dont care
        return -1;                      // $a < $b
    } else {
        return $a - $b;                 // sort $a and $b ascending
    }
});
var_dump($array); // after

输出:

Before                         |  After
-------------------------------+-------------------------------
array(8) {                     |  array(8) {
  [0]=>                        |    [0]=>
  array(2) {                   |    array(2) {
    ["id"]=>                   |      ["id"]=>
    int(4444444)               |      int(3452342)
    ["title"]=>                |      ["title"]=>
    string(12) "Dont Care #4"  |      string(10) "Must Be #1"
  }                            |    }
  [1]=>                        |    [1]=>
  array(2) {                   |    array(2) {
    ["id"]=>                   |      ["id"]=>
    int(3333333)               |      int(5867867)
    ["title"]=>                |      ["title"]=>
    string(12) "Dont Care #3"  |      string(10) "Must Be #2"
  }                            |    }
  [2]=>                        |    [2]=>
  array(2) {                   |    array(2) {
    ["id"]=>                   |      ["id"]=>
    int(1231233)               |      int(7867867)
    ["title"]=>                |      ["title"]=>
    string(10) "Must Be #4"    |      string(10) "Must Be #3"
  }                            |    }
  [3]=>                        |    [3]=>
  array(2) {                   |    array(2) {
    ["id"]=>                   |      ["id"]=>
    int(1111111)               |      int(1231233)
    ["title"]=>                |      ["title"]=>
    string(12) "Dont Care #1"  |      string(10) "Must Be #4"
  }                            |    }
  [4]=>                        |    [4]=>
  array(2) {                   |    array(2) {
    ["id"]=>                   |      ["id"]=>
    int(5867867)               |      int(2222222)
    ["title"]=>                |      ["title"]=>
    string(10) "Must Be #2"    |      string(12) "Dont Care #2"
  }                            |    }
  [5]=>                        |    [5]=>
  array(2) {                   |    array(2) {
    ["id"]=>                   |      ["id"]=>
    int(2222222)               |      int(1111111)
    ["title"]=>                |      ["title"]=>
    string(12) "Dont Care #2"  |      string(12) "Dont Care #1"
  }                            |    }
  [6]=>                        |    [6]=>
  array(2) {                   |    array(2) {
    ["id"]=>                   |      ["id"]=>
    int(3452342)               |      int(3333333)
    ["title"]=>                |      ["title"]=>
    string(10) "Must Be #1"    |      string(12) "Dont Care #3"
  }                            |    }
  [7]=>                        |    [7]=>
  array(2) {                   |    array(2) {
    ["id"]=>                   |      ["id"]=>
    int(7867867)               |      int(4444444)
    ["title"]=>                |      ["title"]=>
    string(10) "Must Be #3"    |      string(12) "Dont Care #4"
  }                            |    }
}                              |  }

让我们再给这个问题增加一个难点。如果我的数组中没有 array("id" => 5867867, "title" => "Must Be #2"),但我仍然需要 array("id" => 7867867, "title" => "Must Be #3") 成为第三个,所以我需要第二个为空。 - Neve12ende12

16
其他使用反复调用array_search()方法的答案不够高效。通过重构/翻转“顺序”查找数组,您可以完全省略所有array_search()调用--使您的任务更加高效和简洁。我将使用最现代的“spaceship运算符”(<=>),但是早期的技术对于比较行也是有效的。 “null coalescing运算符”(??)将用于检查在查找数组中是否存在给定的id值,以与isset()相同的方式工作--这始终比使用array_search()in_array()更高效。
代码:(演示) (具有7.4箭头函数语法的演示) (低于PHP7的演示)
// restructure with values as keys, and keys as order (ASC)
$order = array_flip([3452342, 5867867, 7867867, 1231233]);
// generating $order = [3452342 => 0, 5867867 => 1, 7867867 => 2, 1231233 => 3];

$default = count($order);
// generating $default = 4


usort($array, function($a, $b) use($order, $default) {
    return ($order[$a['id']] ?? $default) <=> ($order[$b['id']] ?? $default);
});

var_export($array);

3

更高效的解决方案

$dict = array_flip($order);
$positions = array_map(function ($elem) use ($dict) { return $dict[$elem['id']] ?? INF; }, $array);
array_multisort($positions, $array);

不要在每一次比较中重新计算位置

当数组很大或获取id代价较高时,使用usort()可能会变得很糟糕,因为您需要在每次比较中重新计算id。请尝试使用预先计算的位置(见下面示例中的mediumsortfastsort)使用array_multisort(),这并不复杂。

此外,每次比较都在顺序数组中搜索id(如接受的答案中所述)并不能提高性能,因为您需要在每次比较中遍历它。请在一开始就计算它。

在下面的代码段中,您可以看到主要的三个排序函数:

  • slowsort
    接受的答案。在每次比较中搜索位置。
  • mediumsort
    通过预先计算位置改进了slowsort
  • fastsort
    通过完全避免搜索,改进了mediumsort
请注意,这些处理元素的id不按照给定顺序提供回退值INF。如果您的顺序数组与原始数组的id一一匹配,则避免排序并只需将元素插入正确位置即可。我添加了一个名为cheatsort的函数,可以完美实现此功能。
您还可以通过权重对数组进行更一般的排序(请参见示例中的weightedsort)。确保仅计算一次权重,以获得良好的性能。
性能(针对长度为1000的数组):
fastsort     about  1 ms
mediumsort   about  3 ms
slowsort     about 60 ms

提示:对于更大的数组,差异会变得更加明显。

排序函数比较

<?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');

“slowsort()”,“mediumsort()”和“cheatsort()”并不适用于带有缺失值的待排序输入数组。 - mickmackusa

2
没有排序,你也可以得到它。
  1. If there are no duplicate ids and $order contains all id values from $array and the id column in $array contains all values in $order, you can achieve the same result by flipping the values into keys in $order, then assign temporary first level keys to array, then either merge or replace $array into $order.

    $order = array(3452342, 5867867, 7867867, 1231233);
    $array = array(
        array('id' => 7867867, 'title' => 'Some Title'),
        array('id' => 3452342, 'title' => 'Some Title'),
        array('id' => 1231233, 'title' => 'Some Title'),
        array('id' => 5867867, 'title' => 'Some Title'),
    );
    
    $order = array_flip($order);
    $array = array_column($array,null,"id");
    $result = array_replace($order,$array);
    var_dump(array_values($result));
    
  2. With potentially duplicate ids in $array:

    $order = array(3452342, 5867867, 7867867, 1231233);
    $array = array(
        array('id' => 7867867, 'title' => 'Some Title'),
        array('id' => 3452342, 'title' => 'Some Title'),
        array('id' => 1231233, 'title' => 'Some Title'),
        array('id' => 5867867, 'title' => 'Some Title'),
    );
    
    $order_dict = array_flip($order);
    $order_dict = array_combine($order, array_fill(0, count($order), []));
    foreach($array as $item){
        $order_dict[$item["id"]][] = $item;
    }
    //$order_dict = array_filter($order_dict);  // if there is empty item on some id in $order array
    $result = [];
    foreach($order_dict as $items){
        foreach($items as $item){
            $result[] = $item;
        }
    }
    var_dump($result);
    

array_flip()在第2行被立即覆盖,它的意义何在?为什么要使用array_combine(array_fill())?你是不是想用array_fill_keys()?第二个片段使用了6种迭代技术--你会在实际项目中这样做吗? - mickmackusa

1

@salathe 对于那些难以理解salathe的usort在做什么的人:

$array中的每个项目都是比赛中的“冠军”,要成为新数组开头的一个(除了不是第一名,他们想成为第0名)。

$a是主场冠军,$b是比赛对手冠军。

回调函数中的$pos_a和$pos_b是用于争夺冠军a和b的属性。在这种情况下,该属性是冠军id在$order中的索引。

然后是返回值的比赛。现在我们要查看是否拥有更多或更少的属性更好。在usort战斗中,主场冠军希望得到负数,这样他可以更早地出现在数组中。客场冠军希望得到正数。如果有0,则是平局。

当使用$ order中的索引减去客队冠军属性时,按照这个类比,客队冠军属性越大,获得正数胜利的可能性就越小。但是,如果您反转属性的使用方式,现在将客队冠军的属性减去主队冠军的属性。在这种情况下,客队冠军的更大数字更有可能使他以正数结束比赛。
代码如下:
注意:代码会多次运行,就像真实的锦标赛有许多战斗一样,以决定谁获得第一名(即数组开始处的0)。
//tournament with goal to be first in array
    usort($champions, function ($home, $away) use ($order) {
        $home_attribute = array_search($a['id'], $order);
        $away_attribute = array_search($b['id'], $order);
        //fight with desired outcome for home being negative and away desiring positive
        return $home_attribute - $away_attribute;
    });

希望这不会让人们更加困惑 ;) - Jason Basanese
我认为这只会增加混乱:“_争取主场负面结果和客场渴望积极结果_”我根本不认为这种说法准确。 - mickmackusa

0
你需要定义自己的比较函数,并使用 usortuasort,如果你想保持索引关联。

1
这个部分解决方案是在问题发布后仅5分钟就出现的。它的价值不高,因为它基本上只提供了两个超链接到手册,并且最好作为评论发布。五年多以后,这个答案被完整的答案所取代,使得这篇文章变得无用。请考虑删除。(不会对此帖进行投票) - mickmackusa

0

我遇到了相同的问题,@mickmackusa 给出了我需要的答案。所选答案在存在NULL值时不进行排序。例如:

$order = array(3, 2, 10);
$array = array(
    array('id' => NULL, 'title' => 'any order since null but not top'),
    array('id' => NULL, 'title' => 'any order since null but not top'),
    array('id' => NULL, 'title' => 'any order since null but not top'),
    array('id' => 2, 'title' => 'should be top'),
);
usort($array, function ($a, $b) use ($order) {
    $pos_a = array_search($a['id'], $order);
    $pos_b = array_search($b['id'], $order);
    return $pos_a - $pos_b;
});

以上结果将显示输出:

array(4) {
  [0]=>
  array(2) {
    ["id"]=>
    NULL
    ["title"]=>
    string(32) "any order since null but not top"
  }
  [1]=>
  array(2) {
    ["id"]=>
    NULL
    ["title"]=>
    string(32) "any order since null but not top"
  }
  [2]=>
  array(2) {
    ["id"]=>
    NULL
    ["title"]=>
    string(32) "any order since null but not top"
  }
  [3]=>
  array(2) {
    ["id"]=>
    int(2)
    ["title"]=>
    string(13) "should be top"
  }
}

在 @mickmackusa 的回答中,它不仅在排序中消除了 null,而且还根据顺序基础将第一个可用的放在首位。因此,由于数组中唯一可用的是 2,所以它会排在最前面。
虽然它在 PHP 5.6 中无法工作。所以我将其转换为 PHP 5.6 兼容版本。这就是我得到的。
usort($array, function($a, $b) use($order, $default) {
    $a = (isset($order[$a['id']]) ? $order[$a['id']] : $default);
    $b = (isset($order[$b['id']]) ? $order[$b['id']] : $default);

    if($a == $b) return 0;
    elseif($a > $b) return 1;
    return -1;
});

上述排序的结果将是:
array(4) {
  [0]=>
  array(2) {
    ["id"]=>
    int(2)
    ["title"]=>
    string(13) "should be top"
  }
  [1]=>
  array(2) {
    ["id"]=>
    NULL
    ["title"]=>
    string(32) "any order since null but not top"
  }
  [2]=>
  array(2) {
    ["id"]=>
    NULL
    ["title"]=>
    string(32) "any order since null but not top"
  }
  [3]=>
  array(2) {
    ["id"]=>
    NULL
    ["title"]=>
    string(32) "any order since null but not top"
  }
}

我希望我的代码转换能够帮助那些使用低版本PHP的过时服务器上工作的开发人员。


听起来需要升级一下了。 :) 函数体中的最后一行(elseif($a < $b) return -1;)可以简化为return -1;,因为它没有选择,只能是true - mickmackusa
是的,抱歉@mickmackusa,我刚刚编辑了我的答案以提供更详细的解释。 - rochiey

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