【问题标题】:Sort array by object property in PHP?在PHP中按对象属性对数组进行排序?
【发布时间】:2009-09-22 20:46:53
【问题描述】:

如果我有这样的对象:

class Person {
  var $age;
  function __construct($age) {
    $this->age = $age;
  }
}

我有任何Persons 数组

$person1 = new Person(14);
$person2 = new Person(5);
$people = array($person1, $person2);

有没有一种简单的方法可以通过Person->age 属性对$people 数组进行排序?

【问题讨论】:

  • 我试图避免使用 usort(),因为随着我的人员数组的增长,它的调用成本太高。假设我在 $people 中有 15,000 个条目。
  • 您认为 usort 有哪些低效率问题,您可以通过其他任何方法避免这些问题? usort 将就地排序,应该非常高效。
  • 每次调用都会生成一个函数,这使得它在大型数据集中效率低下。
  • 我已经发布了一些基准测试详细信息 - usort 还不错,但您确实可以使用非递归快速排序更快。
  • 你在做什么需要一次对 15,000 个对象进行排序?

标签: php sorting


【解决方案1】:

问题是因为调用比较回调的开销导致使用usort 效率低下。这个答案着眼于使用内置排序函数和非递归快速排序实现之间的区别。

随着 PHP 自 2009 年以来的发展,答案随着时间的推移而改变,所以我一直在更新它。旧材料虽然不再相关,但仍然很有趣!

TL;DR:从 php 7.0.1 开始,非递归快速排序不再比使用带有回调的 usort 更快。并非总是如此,这就是为什么要详细说明下面进行有趣的阅读。真正的收获是,如果您对问题进行基准测试并尝试其他方法,您会得出令人惊讶的结果。

2016 年 1 月更新

我们已经发布了 php 7.0 和 7.1!最后,对于这个数据集,内置的 usort 比以往任何时候都快快!

+-----------+------------+------------+------------+------------+------------+
| Operation | HHVM       | php7.0.1   | php5.6.3   | 5.4.35     | 5.3.29     |
+-----------+------------+------------+------------+------------+------------+
| usort     | *0.0445    | *0.0139    |  0.1503    |  0.1388    |  0.2390    |
| quicksort |  0.0467    |  0.0140    | *0.0912    | *0.1190    | *0.1854    |
|           | 5% slower  | 1% slower  | 40% faster | 15% faster | 23% faster |
+-----------+------------+------------+------------+------------+------------+

2015 年 1 月更新

当我在 2009 年最初回答这个问题时,我将使用 usort 与非递归快速排序进行了比较,看看是否有区别。事实证明,存在显着的差异,快速排序的运行速度提高了 3 倍。

因为现在是 2015 年,我认为重新审视它可能会很有用,所以我采用了使用 usort 和 quicksort 对 15000 个对象进行排序的代码,并在 3v4l.org 上运行它,它在许多不同的 PHP 版本上运行。完整结果在这里:http://3v4l.org/WsEEQ

+-----------+------------+------------+------------+------------+------------+
| Operation | HHVM       | php7alpha1 | php5.6.3   | 5.4.35     | 5.3.29     |
+-----------+------------+------------+------------+------------+------------+
| usort     | *0.0678    |  0.0438    |  0.0934    |  0.1114    |  0.2330    |
| quicksort |  0.0827    | *0.0310    | *0.0709    | *0.0771    | *0.1412    |
|           | 19% slower | 30% faster | 25% faster | 31% faster | 40% faster |
+-----------+------------+------------+------------+------------+------------+

2009 年的原始笔记

我尝试了usort,并在大约 1.8 秒内对 15000 个 Person 对象进行了排序。

由于您担心调用比较函数的效率低下,我将它与非递归的Quicksort 实现进行了比较。这实际上运行了大约三分之一的时间,大约 0.5 秒。

这是我对这两种方法进行基准测试的代码

// Non-recurive Quicksort for an array of Person objects
// adapted from http://www.algorithmist.com/index.php/Quicksort_non-recursive.php
function quickSort( &$array )
{
 $cur = 1;
 $stack[1]['l'] = 0;
 $stack[1]['r'] = count($array)-1;

 do
 {
  $l = $stack[$cur]['l'];
  $r = $stack[$cur]['r'];
  $cur--;

  do
  {
   $i = $l;
   $j = $r;
   $tmp = $array[(int)( ($l+$r)/2 )];

   // partion the array in two parts.
   // left from $tmp are with smaller values,
   // right from $tmp are with bigger ones
   do
   {
    while( $array[$i]->age < $tmp->age )
     $i++;

    while( $tmp->age < $array[$j]->age )
     $j--;

    // swap elements from the two sides
    if( $i <= $j)
    {
     $w = $array[$i];
     $array[$i] = $array[$j];
     $array[$j] = $w;

     $i++;
     $j--;
    }

   }while( $i <= $j );

 if( $i < $r )
   {
    $cur++;
    $stack[$cur]['l'] = $i;
    $stack[$cur]['r'] = $r;
   }
   $r = $j;

  }while( $l < $r );

 }while( $cur != 0 );


}


// usort() comparison function for Person objects
function personSort( $a, $b ) {
    return $a->age == $b->age ? 0 : ( $a->age > $b->age ) ? 1 : -1;
}


// simple person object    
class Person {
  var $age;
  function __construct($age) {
    $this->age = $age;
  }
}

//---------test internal usort() on 15000 Person objects------

srand(1);
$people=array();
for ($x=0; $x<15000; $x++)
{
     $people[]=new Person(rand(1,100));
}


$start=microtime(true);
usort( $people, 'personSort' );
$total=microtime(true)-$start;

echo "usort took $total\n";


//---------test custom quicksort on 15000 Person objects------

srand(1);
$people=array();
for ($x=0; $x<15000; $x++)
{
     $people[]=new Person(rand(1,100));
}


$start=microtime(true);
quickSort( $people );
$total=microtime(true)-$start;

echo "quickSort took $total\n";

一个有趣的建议是在类中添加一个__toString 方法并使用sort(),所以我也尝试了。麻烦的是,您必须将 SORT_STRING 作为第二个参数传递给 sort 才能真正调用魔术方法,这具有执行字符串而不是数字排序的副作用。为了解决这个问题,您需要用零填充数字以使其正确排序。最终结果是这比 usort 和自定义 quickSort 都慢

sort 10000 items took      1.76266698837
usort 10000 items took     1.08757710457
quickSort 10000 items took 0.320873022079

这是使用 __toString() 进行 sort() 的代码:

$size=10000;

class Person {
  var $age;
  function __construct($age) {
    $this->age = $age;
    $this->sortable=sprintf("%03d", $age);
  }


  public function __toString()
  {
     return $this->sortable;
  }
}

srand(1);
$people=array();
for ($x=0; $x<$size; $x++)
{
     $people[]=new Person(rand(1,100));
}


$start=microtime(true);
sort( $people, SORT_STRING);
$total=microtime(true)-$start;

echo "sort($size) took $total\n"

【讨论】:

  • 您是否检查过该算法的正确性?您需要将$array[$i] &lt; $tmp 更改为$array[$i]-&gt;age &lt; $tmp-&gt;age,将$tmp &lt; $array[$j] 更改为$tmp-&gt;age &lt; $array[$j]-&gt;age,将$i-&gt;age &lt;= $j-&gt;age 更改为$i &lt;= $j
  • 哦,如果你想比较这两种算法,你应该在相同的数据上运行它们,而不仅仅是在相同大小但完全不同特征的数据上运行。生成一次人员数组,并使用两种算法对相同数据的副本进行排序。
  • 用同样的数据进行测试的那句话呢?在比较算法时,您仍在使用不同的测试数据。
  • 您还应该验证每个函数的结果是否相同。
  • @Gumbo,我在每次测试之前播种了随机数生成器,以便重复运行测试是相同的。我也在结果中添加了一个健全性检查,但从这个答案中省略了它。
【解决方案2】:

您可以使用usortheap

 class SortPeopleByAge extends SplMaxHeap
  {
      function compare($person1, $person2)
      {
          return $person1->age - $person2->age;
      }
  }

  $people = array(new Person(30), new Person(22), new Person(40));  
  $sorter = new SortPeopleByAge;
  array_map(array($sorter, 'insert'), $people);
  print_r(iterator_to_array($sorter)); // people sorted from 40 to 22

请注意,堆的目的是始终拥有有序集合,而不是替换usortFor large collections (1000+), a heap will be faster and less memory intensive though.

拥有堆的另一个好处是能够使用它们的比较函数来回调其他排序函数,例如usort。你只需要记住比较的顺序是颠倒的,所以任何使用堆进行的比较都会导致usort中的颠倒顺序。

// using $people array and $sorter
usort($people, array($sorter, 'compare'));
print_r($people); // people sorted from 22 to 40

usort 适用于中小型集合,您将在最后进行一次排序。当然,你不必有一个堆来使用usort。您也可以为排序添加任何其他有效的回调。

【讨论】:

  • 关于大数据的最正确答案。迭代器每次运行只占用当前值的内存,而数组占用所有条目的内存。堆形式的迭代器资源效率更高。
【解决方案3】:

我刚刚编写了这个代码。它应该比usort 更快,因为它不依赖于大量的函数调用。

function sortByProp($array, $propName, $reverse = false)
{
    $sorted = [];

    foreach ($array as $item)
    {
        $sorted[$item->$propName][] = $item;
    }

    if ($reverse) krsort($sorted); else ksort($sorted);
    $result = [];

    foreach ($sorted as $subArray) foreach ($subArray as $item)
    {
        $result[] = $item;
    }

    return $result;
}

用法:

$sorted = sortByProp($people, 'age');

哦,它使用ksort,但即使许多$people 是相同的$age,它也能工作。

【讨论】:

    【解决方案4】:

    对于该特定场景,您可以使用 usort() 函数对其进行排序,您可以在其中定义自己的函数来比较数组中的项目。

    <?php
    
    class Person {
      var $age;
      function __construct($age) {
        $this->age = $age;
      }
    }
    
    function personSort( $a, $b ) {
        return $a->age == $b->age ? 0 : ( $a->age > $b->age ) ? 1 : -1;
    }
    
    $person1 = new Person(14);
    $person2 = new Person(5);
    $person3 = new Person(32);
    $person4 = new Person(150);
    $person5 = new Person(39);
    $people = array($person1, $person2, $person3, $person4, $person5);
    
    print_r( $people );
    
    usort( $people, 'personSort' );
    
    print_r( $people );
    

    【讨论】:

    • 我试图避免使用 usort(),因为随着我的人员数组的增长,它的调用成本太高。假设我在 $people 中有 15,000 个条目
    • 我认为你可以像这样更简单地实现 personSort():return $a->age - $b->age;
    • 呵呵,真的吗?我只是习惯于这样做,以至于我并没有真正从数学上考虑过。
    • @DonKirkby return $a-&gt;age - $b-&gt;age; 使用浮点数产生错误的结果,您可以从 php 7.0 开始使用 spaceship 运算符,它适用于浮点数 return $a-&gt;age &lt;=&gt; $b-&gt;age;
    【解决方案5】:

    您只需要编写一个自定义比较函数,然后使用usort 之类的东西来进行实际排序。例如,如果成员变量是myVar,则可以按如下方式排序:

    function cmp($a, $b)
    {
        if ($a->myVar == $b->myVar) {
            return 0;
        }
        return ($a->myVar < $b->myVar) ? -1 : 1;
    }
    
    usort($myArray, "cmp");
    

    【讨论】:

      【解决方案6】:

      我不建议在您的示例中使用我的解决方案,因为它会很丑陋(而且我没有对其进行基准测试),但它可以工作....并且根据需要,它可能会有所帮助。 :)

      class Person
      {
        public $age;
      
        function __construct($age)
        {
          $this->age = $age;
        }
      
        public function __toString()
        {
          return $this->age;
        }
      }
      
      $person1 = new Person(14);
      $person2 = new Person(5);
      
      $persons = array($person1, $person2);
      asort($persons);
      

      【讨论】:

      • 我认为缓慢来自这样一个事实,即这仍然会导致每个对象调用一个函数,它现在只是在类本身中。
      【解决方案7】:

      这是一个 stable Radix Sort 值 0...256 的实现:

      function radixsort(&$a)
      {
          $n = count($a);
          $partition = array();
          for ($slot = 0; $slot < 256; ++$slot) {
              $partition[] = array();
          }
          for ($i = 0; $i < $n; ++$i) {
              $partition[$a[$i]->age & 0xFF][] = &$a[$i];
          } 
          $i = 0;
          for ($slot = 0; $slot < 256; ++$slot) {
              for ($j = 0, $n = count($partition[$slot]); $j < $n; ++$j) {
                  $a[$i++] = &$partition[$slot][$j];
              }
          }
      }
      

      这仅花费 O(n),因为基数排序是一种非比较排序算法。

      【讨论】:

        【解决方案8】:

        一个观察结果是,如果数据源来自数据库,则使用 SQL 进行排序可能比使用 PHP 进行排序更快。如果数据源来自 CSV 或 XML 文件,这当然是没有实际意义的。

        【讨论】:

          【解决方案9】:

          我采用以下方法:创建一个接受对象数组的函数,然后在函数内部创建一个关联数组,使用该属性作为数组的键,然后使用 ksort 对它们的数组键进行排序:

          class Person {
              var $age;
              function __construct($age) {
                $this->age = $age;
              }
          }
          
          function sortPerson($persons = Array()){
              foreach($persons as $person){
                  $sorted[$person->age] = $person;
              }
              ksort($sorted);
              return array_values($sorted);
          }
          
          $person1 = new Person(14);
          $person2 = new Person(5);
          
          $persons = array($person1, $person2);
          $person = sortPerson($persons);
          
          echo $person[0]->age."\n".$person[1]->age;
          /* Output:
          5
          14
          */
          

          【讨论】:

            【解决方案10】:

            您可以使用ouzo goodies

            $result = Arrays::sort(array($person1, $person2), Comparator::compareBy('age'));
            

            http://ouzo.readthedocs.org/en/latest/utils/comparators.html

            【讨论】:

              【解决方案11】:

              usort()uasort() /* to maintain index association if you were using an associative array */

              【讨论】:

                【解决方案12】:

                是的。如果你在你的person对象中实现spl ArrayObject,所有普通的php数组函数都可以正常工作。

                【讨论】:

                  【解决方案13】:

                  试试 usort:http://www.php.net/manual/en/function.usort.php

                  例子:

                  <?php
                  function cmp($obja, $objb)
                  {
                      $a = $obja->sortField;
                      $b = $objb->sortField;
                      if ($a == $b) {
                          return 0;
                      }
                      return ($a < $b) ? -1 : 1;
                  }
                  
                  $a = array( /* your objects */ );
                  
                  usort($a, "cmp");
                  
                  ?>
                  

                  【讨论】:

                    【解决方案14】:

                    如果保证所有有问题的成员变量都是不同的,那么创建一个由这些值索引的新集合然后ksort it:

                     foreach($obj_list as $obj)
                        $map[$obj->some_var] = $obj;
                     ksort($map);
                     /// $map now contains the sorted list
                    

                    如果存在重复值,您仍然可以通过利用 sort 的一个鲜为人知的特性来避免 usort,即数组的数组按第一个标量成员的值排序。

                     foreach($obj_list as $obj)
                        $map[] = array($obj->some_var, $obj);
                     sort($map); // sorts $map by the value of ->some_var
                    

                    我想这仍然会比 usort 快 10000000 倍

                    【讨论】:

                    • 1000 次是无底洞的夸张。
                    • 如果两个或多个实例的两个或多个属性相同怎么办?
                    【解决方案15】:

                    这是一个考虑以下因素的选项:

                    • 命名空间
                    • 私有财产
                    • 使用 getter 和 setter 方法
                    • 作为参数的排序属性

                    PHP

                    namespace Dummy;
                    
                    class Person {
                    
                        private $age;
                    
                        function __construct($age) {
                            $this->setAge($age);
                        }
                    
                        public function getAge()
                        {
                            return $this->age;
                        }
                    
                        public function setAge($age)
                        {
                            $this->age = $age;
                        }
                    }
                    
                    class CustomSort{
                    
                        public $field = '';
                    
                        public function cmp($a, $b)
                        {
                            return strcmp($a->{'get'.ucfirst($this->field)}(), $b->{'get'.ucfirst($this->field)}());
                        }
                    
                        public function sortObjectArrayByField($array, $field)
                        {
                            $this->field = $field;
                            usort($array, array("Dummy\CustomSort", "cmp"));
                            return $array;
                        }
                    }
                    
                    $robert = new Person(20);
                    $peter = new Person(12);
                    $robin = new Person(44);
                    $people = array($robert, $peter, $robin);
                    
                    var_dump( $people );
                    
                    $customSort = new CustomSort();
                    $people = $customSort->sortObjectArrayByField($people, 'age');
                    
                    var_dump( $people );
                    

                    【讨论】:

                      猜你喜欢
                      • 2014-09-17
                      • 2021-10-21
                      • 2019-01-12
                      • 1970-01-01
                      • 1970-01-01
                      • 2011-09-06
                      • 1970-01-01
                      • 2012-02-20
                      相关资源
                      最近更新 更多