Sort array by values in PHP mantaining relative positions - php

I'm using usort() and uasort(). I need to sort an array by values using a user-defined comparison function.
php.net/manual/en/function.usort.php
The doc says:
Note:
If two members compare as equal, their relative order in the sorted array is undefined.
The question is: is there any PHP function that mantain the relative position of the equal elements?

The short answer is that PHP does not have a built in function to do this, so you will have to write one. Most of the time, it does not matter if the sort moves the element up or down if it is considered equal to the adjoining element. An example would be any array of integers. If two are the same, who cares what order they are in as long as they are together.
For the cases where you DO need to maintain the order of the lists, Sreid has written a very good function for this. It is actually on the usort page at php.net. I am pasting it here for your convenience. Be aware that I am giving sreid full credit for this code, and I have already mentioned where his original code can be found in a public forum:
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;
}

Related

PHP Sort associative array

I have an associative array like this one:
$teams_name_points = array();
$name1 = "name1";
$name2 = "name2";
$teams_name_points[$name1] = 1;
$teams_name_points[$name2] = 2;
I want to sort this array by the key values, currently it's sorted alphabetically by the key.
I tried to implement my own sorting function, but I do not quite understand how it works.
usort($teams_name_points, 'cmp');
function cmp(array $a, array $b){
if ($a['foo'] < $b['foo']) {
return -1;
} else if ($a['foo'] > $b['foo']) {
return 1;
} else {
return 0;
}
}
How do I make the compare method work with my array?
use asort() to sort your array.
http://php.net/manual/en/function.asort.php
This function sorts an array such that array indices maintain their correlation with the array elements they are associated with. This is used mainly when sorting associative arrays where the actual element order is significant.
$teams_name_points = array();
$name1 = "name1";
$name2 = "name2";
$teams_name_points[$name1] = 2;
$teams_name_points[$name2] = 1;
print_r($teams_name_points);
asort($teams_name_points); // sort by value low to high
print_r($teams_name_points);
arsort($teams_name_points); // sort by value high to low
print_r($teams_name_points);
Simply enough, as per the manual, you'll only want to accept the values of the arrays as the arguments to cmp().
E.g.
cmp($earlier_array_value, $later_array_value){
...
}
Also, of course, be aware that usort() is only warranted when you're running a comparison that is custom and not-numerically-straightforward. Simple comparisons by > or < can be more easily done with existing native sort functions.

What's the most efficient way to array_pop() the last n elements in an array?

What's an efficient way to pop the last n elements in an array?
Here's one:
$arr = range(1,10);
$n = 2;
$popped_array = array();
for ($i=0; $i < $n; $i++) {
$popped_array[] = array_pop($arr);
}
print_r($popped_array); // returns array(10,9);
Is there a more efficient way?
Use array_splice():
If you're trying to remove the last n elements, use the following function:
function array_pop_n(array $arr, $n) {
return array_splice($arr, 0, -$n);
}
Demo
If you want to retrieve only the last n elements, then you can use the following function:
function array_pop_n(array $arr, $n) {
array_splice($arr,0,-$n);
return $arr;
}
Demo
It's important to note, looking at the other answers, that array_slice will leave the original array alone, so it will still contain the elements at the end, and array_splice will mutate the original array, removing the elements at the beginning (though in the example given, the function creates a copy, so the original array still would contain all elements). If you want something that literally mimics array_pop (and you don't require the order to be reversed, as it is in your OP), then do the following.
$arr = range(1, 10);
$n = 2;
$popped_array = array_slice($arr, -$n);
$arr = array_slice($arr, 0, -$n);
print_r($popped_array); // returns array(9,10);
print_r($arr); // returns array(1,2,3,4,5,6,7,8);
If you require $popped_array to be reversed, array_reverse it, or just pop it like your original example, it's efficient enough as is and much more direct.
Why not use array_slice. You can give a start and a length, so if you do 2 from the end you will get the last two items in the array:
$arr = range(1,10);
$n = 2;
$start = count($arr) - $n;
print_r(array_slice($arr, $start, $n));
Thanks for the array_slice comments. I don't know why that didn't immediately come to mind.
It looks (to me) like the easiest way is:
$arr = range(1,10);
$n = 2;
$popped_array = array_slice($arr,-$n);
print_r($popped_array); // returns array(10,9);

Combine and transpose multiple single-dimensional arrays into a single multi-dimensional array

I have three single-dimensional arrays and I need to combine them into one 3-dimensional array where each array in the new array contains one element of each of the three original arrays.
I know how to do this using a simple loop but I was wondering if there is a faster / built-in way of doing this. here is an example of doing this with a loop so you can understand what I'm looking for.
function combineArrays(array $array1, array $array2, array $array3) {
//Make sure arrays are of the same size
if(count($array1) != count($array2) || count($array2) != count($array3) || count($array1) != count($array3)) {
throw new Exception("combineArrays expects all paramters to be arrays of the same length");
}
//combine the arrays
$newArray = array();
for($count = 0; $count < count($array1); $count++) {
$newArray[] = array($array1[$count], $array2[$count], $array3[$count]);
}
return $newArray;
}
$result = array_map(null,$array1,$array2,$array3);

Preserve key order (stable sort) when sorting with PHP's uasort

This question is actually inspired from another one here on SO and I wanted to expand it a bit.
Having an associative array in PHP is it possible to sort its values, but where the values are equal to preserve the original key order, using one (or more) of PHP's built in sort function?
Here is a script I used to test possible solutions (haven't found any):
<?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);
?>
Pitfall: Because the keys are ordered in the original array, please don't be tempted to suggest any sorting by key to restore to the original order. I made the example with them ordered to be easier to visually check their order in the output.
Since PHP does not support stable sort after PHP 4.1.0, you need to write your own function.
This seems to do what you're asking: http://www.php.net/manual/en/function.usort.php#38827
As the manual says, "If two members compare as equal, their order in the sorted array is undefined." This means that the sort used is not "stable" and may change the order of elements that compare equal.
Sometimes you really do need a stable sort. For example, if you sort a list by one field, then sort it again by another field, but don't want to lose the ordering from the previous field. In that case it is better to use usort with a comparison function that takes both fields into account, but if you can't do that then use the function below. It is a merge sort, which is guaranteed O(n*log(n)) complexity, which means it stays reasonably fast even when you use larger lists (unlike bubblesort and insertion sort, which are 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;
}
?>
Also, you may find this forum thread interesting.
array_multisort comes in handy, just use an ordered range as second array ($order is just temporary, it serves to order the equivalent items of the first array in its original 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);
Output
array(4) {
["key-99"]=>
int(3)
["key-2"]=>
int(3)
["key-0"]=>
int(5)
["key-3"]=>
int(7)
}
I used test data with not-ordered keys to demonstrate that it works correctly. Nonetheless, here is the output your test script:
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
)
Downside
It only works with predefined comparisons, you cannot use your own comparison function. The possible values (second parameter of array_multisort()) are:
Sorting type flags:
SORT_ASC - sort items ascendingly.
SORT_DESC - sort items descendingly.
SORT_REGULAR - compare items normally (don't change types)
SORT_NUMERIC - compare items numerically
SORT_STRING - compare items as strings
SORT_LOCALE_STRING - compare items as strings, based on the current locale. It uses the locale, which can be changed using
setlocale()
SORT_NATURAL - compare items as strings using "natural ordering" like natsort()
SORT_FLAG_CASE - can be combined (bitwise OR) with SORT_STRING or SORT_NATURAL to sort strings case-insensitively
For completeness sake, you should also check out the Schwartzian transform:
// 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
}
The default sort algorithm of PHP works fine with arrays, because of this:
array(1, 0) < array(2, 0); // true
array(1, 1) < array(1, 2); // true
If you want to use your own sorting criteria you can use uasort() as well:
// 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
}
}
This is a solution using which you can achieve stable sort in usort function
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;
}
Just to complete the responses with some very specific case. If the array keys of $array are the default one, then a simple array_values(asort($array)) is sufficient (here for example in ascending order)
As a workaround for stable sort:
<?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);

Interleaving multiple arrays into a single array

I need to merge several arrays into a single array. The best way to describe what I'm looking for is "interleaving" the arrays into a single array.
For example take item one from array #1 and append to the final array. Get item one from array #2 and append to the final array. Get item two from array #1 and append...etc.
The final array would look something like this:
array#1.element#1
array#2.element#1
.
.
.
The "kicker" is that the individual arrays can be of various lengths.
Is there a better data structure to use?
for example,
function array_zip_merge() {
$output = array();
// The loop incrementer takes each array out of the loop as it gets emptied by array_shift().
for ($args = func_get_args(); count($args); $args = array_filter($args)) {
// &$arg allows array_shift() to change the original.
foreach ($args as &$arg) {
$output[] = array_shift($arg);
}
}
return $output;
}
// test
$a = range(1, 10);
$b = range('a', 'f');
$c = range('A', 'B');
echo implode('', array_zip_merge($a, $b, $c)); // prints 1aA2bB3c4d5e6f78910
If the arrays only have numeric keys, here's a simple solution:
$longest = max( count($arr1), count($arr2) );
$final = array();
for ( $i = 0; $i < $longest; $i++ )
{
if ( isset( $arr1[$i] ) )
$final[] = $arr1[$i];
if ( isset( $arr2[$i] ) )
$final[] = $arr2[$i];
}
If you have named keys you can use the array_keys function for each array and loop over the array of keys instead.
If you want more than two arrays (or variable number of arrays) then you might be able to use a nested loop (though I think you'd need to have $arr[0] and $arr[1] as the individual arrays).
I would just use array_merge(), but that obviously depends on what exactly you do.
This would append those arrays to each other, while elements would only be replaced when they have the same non-numerical key. And that might not be a problem for you, or it might be possible to be solved because of attribute order, since the contents of the first arrays' elements will be overwritten by the later ones.
If you have n arrays, you could use a SortedList, and use arrayIndex * n + arrayNumber as a sort index.

Categories