Can the for loop be eliminated from this piece of PHP code?
Asked Answered
P

10

6

I have a range of whole numbers that might or might not have some numbers missing. Is it possible to find the smallest missing number without using a loop structure? If there are no missing numbers, the function should return the maximum value of the range plus one.

This is how I solved it using a for loop:

$range = [0,1,2,3,4,6,7];

// sort just in case the range is not in order
asort($range);
$range = array_values($range);

$first = true;
for ($x = 0; $x < count($range); $x++)
{
    // don't check the first element
    if ( ! $first )
    {
        if ( $range[$x - 1] + 1 !== $range[$x])
        {
            echo $range[$x - 1] + 1;
            break;
        }
    }

    // if we're on the last element, there are no missing numbers
    if ($x + 1 === count($range))
    {
        echo $range[$x] + 1;
    }
    $first = false;
}

Ideally, I'd like to avoid looping completely, as the range can be massive. Any suggestions?

Polynesia answered 15/8, 2013 at 21:55 Comment(11)
a huge array with all numbers then array_diff() but that still uses a loop internally. iterating over a range=loop(even if handled internally). seen a few "i don't want a loop" questions lately, who is teach you that loop=bad ?Bik
Tried your code. According to your $range array, it should return 5 (which is missing). Instead, it returns 8. So it's not even working properly.Maryjanemaryjo
@cuewizchris Whoops! I left out the last line ($first = false). Thanks for catching that.Polynesia
The code wasn't compiling cause the $range was defined as: $range = [0,1,2,3,4,6,7]; instead of: $range = array(0,1,2,3,4,6,7); - maybe there are other issues as well - I didn't check the rest.Yet
@alfasin Whoops! Sorry, I'm using PHP 5.4Polynesia
@BenHarold ohhh - my bad then :)Yet
What about [0, 1, 2, 2, 3]? Is that valid?Erastian
are you certain that the values are positive and ordered?Cocke
@Jack Yes, it is valid. php.net/manual/en/migration54.new-features.phpPolynesia
I didn't mean "is this syntax valid?" but rather, would that array of non-descending numbers be valid? :)Erastian
@jack My bad! The $range is essentially a set of keys which are on a unique index, so repeats must not occur (in the parlance of RFC 2119).Polynesia
O
9

EDIT: NOTE
This question is about performance. Functions like array_diff and array_filter are not magically fast. They can add a huge time penalty. Replacing a loop in your code with a call to array_diff will not magically make things fast, and will probably make things slower. You need to understand how these functions work if you intend to use them to speed up your code.

This answer uses the assumption that no items are duplicated and no invalid elements exist to allow us to use the position of the element to infer its expected value.

This answer is theoretically the fastest possible solution if you start with a sorted list. The solution posted by Jack is theoretically the fastest if sorting is required.

In the series [0,1,2,3,4,...], the n'th element has the value n if no elements before it are missing. So we can spot-check at any point to see if our missing element is before or after the element in question.

So you start by cutting the list in half and checking to see if the item at position x = x

[ 0 | 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 ]
                  ^

Yup, list[4] == 4. So move halfway from your current point the end of the list.

[ 0 | 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 ]
                          ^

Uh-oh, list[6] == 7. So somewhere between our last checkpoint and the current one, one element was missing. Divide the difference in half and check that element:

[ 0 | 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 ]
                      ^

In this case, list[5] == 5

So we're good there. So we take half the distance between our current check and the last one that was abnormal. And oh.. it looks like cell n+1 is one we already checked. We know that list[6]==7 and list[5]==5, so the element number 6 is the one that's missing.

Since each step divides the number of elements to consider in half, you know that your worst-case performance is going to check no more than log2 of the total list size. That is, this is an O(log(n)) solution.

If this whole arrangement looks familiar, It's because you learned it back in your second year of college in a Computer Science class. It's a minor variation on the binary search algorithm--one of the most widely used index schemes in the industry. Indeed this question appears to be a perfectly-contrived application for this searching technique.

You can of course repeat the operation to find additional missing elements, but since you've already tested the values at key elements in the list, you can avoid re-checking most of the list and go straight to the interesting ones left to test.

Also note that this solution assumes a sorted list. If the list isn't sorted then obviously you sort it first. Except, binary searching has some notable properties in common with quicksort. It's quite possible that you can combine the process of sorting with the process of finding the missing element and do both in a single operation, saving yourself some time.

Finally, to sum up the list, that's just a stupid math trick thrown in for good measure. The sum of a list of numbers from 1 to N is just N*(N+1)/2. And if you've already determined that any elements are missing, then obvously just subtract the missing ones.

Overrun answered 16/8, 2013 at 6:38 Comment(8)
In terms of runtime, asort + binary chop is the fastest algo as tylerl explains. Yes, it involves a loop, but only log N iterations at most with no function calls in the loop so it will be fast to execute in PHP.Rois
So it's the fastest way to go if the array's min value is 0, contains no duplicates, no strings and no null's. To be sure, you'll also need to pass the array through array_filter and array_unique, then sort it, too. And of course, check for the min and max values. Then you'll be good and readyGarnes
@EliasVanOotegem using tools like array_filter and array_unique defeats the purpose, since both will add a huge penalty. Duplicates and nulls were not part of the problem description, therefore we can assume they are not present. If the base value is not zero (which it is, per the problem description), you can simply subtract the value at position 0 before doing the comparison with no significant performance penalty. Checking min and max is redundant.Overrun
@tylerl: I know they add a huge penalty. The base value is not said to be zero ("I have a range of whole numbers, that might or might not have some numbers missing"), only the array in the example has zero as min. There is no mention of null, or duplicates being possible, but absence of evidence is not evidence of absence. I prefer a more defensive approach...Garnes
@EliasVanOotegem assuming these restrictions are not in place, then the fastest solution is the one he posted. It touches each element only once. The only way to speed it up is to do something that doesn't touch every array element (hence this answer). All the other answers posted are slower than the one in the question -- many of them significantly slower.Overrun
My answer is actually faster because it does away with the asort() with a memory compromise. Just FYI :)Erastian
@Jack Ah. You posted a bin sort example; you can't do faster than that if you assume sorting must be done.Overrun
Yeah, if there's no need to sort your answer is the fastest, as mentioned in an earlier comment :)Erastian
D
13

Algo solution

There is a way to check if there is a missing number using an algorithm. It's explained here. Basically if we need to add numbers from 1 to 100. We don't need to calculate by summing them we just need to do the following: (100 * (100 + 1)) / 2. So how is this going to solve our issue ?

We're going to get the first element of the array and the last one. We calculate the sum with this algo. We then use array_sum() to calculate the actual sum. If the results are the same, then there is no missing number. We could then "backtrack" the missing number by substracting the actual sum from the calculated one. This of course only works if there is only one number missing and will fail if there are several missing. So let's put this in code:

  $range = range(0,7);  // Creating an array
  echo check($range) . "\r\n"; // check
  unset($range[3]); // unset offset 3
  echo check($range); // check
    
  function check($array){
    if($array[0] == 0){
      unset($array[0]); // get ride of the zero
    }
    sort($array); // sorting
    $first = reset($array); // get the first value
    $last = end($array); // get the last value
    $sum = ($last * ($first + $last)) / 2; // the algo
    $actual_sum = array_sum($array); // the actual sum
    if($sum == $actual_sum){
      return $last + 1; // no missing number
    }else{
      return $sum - $actual_sum; // missing number
    }
  }

Output

8
3

Online demo

If there are several numbers missing, then just use array_map() or something similar to do an internal loop.


Regex solution

Let's take this to a new level and use regex ! I know it's nonsense, and it shouldn't be used in real world application. The goal is to show the true power of regex :)

So first let's make a string out of our range in the following format: I,II,III,IIII for range 1,3.

$range = range(0,7);
if($range[0] === 0){ // get ride of 0
  unset($range[0]);
}

$str = implode(',', array_map(function($val){return str_repeat('I', $val);}, $range));
echo $str;

The output should be something like: I,II,III,IIII,IIIII,IIIIII,IIIIIII.

I've come up with the following regex: ^(?=(I+))(^\1|,\2I|\2I)+$. So what does this mean ?

^                   # match begin of string
(?=                 # positive lookahead, we use this to not "eat" the match
    (I+)            # match I one or more times and put it in group 1
)                   # end of lookahead
(                   # start matching group 2
    ^\1             # match begin of string followed by what's matched in group 1
        |           # or
    ,\2I            # match a comma, with what's matched in group 2 (recursive !) and an I
        |           # or
    \2I             # match what's matched in group 2 and an I
)+                  # repeat one or more times
$                   # match end of line

Let's see what's actually happening ....

I,II,III,IIII,IIIII,IIIIII,IIIIIII
^
(I+) do not eat but match I and put it in group 1

I,II,III,IIII,IIIII,IIIIII,IIIIIII
^
^\1 match what was matched in group 1, which means I gets matched

I,II,III,IIII,IIIII,IIIIII,IIIIIII
 ^^^ ,\2I match what was matched in group 1 (one I in thise case) and add an I to it

I,II,III,IIII,IIIII,IIIIII,IIIIIII
    ^^^^ \2I match what was matched previously in group 2 (,II in this case) and add an I to it

I,II,III,IIII,IIIII,IIIIII,IIIIIII
        ^^^^^ \2I match what was matched previously in group 2 (,III in this case) and add an I to it

We're moving forward since there is a + sign which means match one or more times,
this is actually a recursive regex.
We put the $ to make sure it's the end of string
If the number of I's don't correspond, then the regex will fail.

See it working and failing. And Let's put it in PHP code:

$range = range(0,7);
if($range[0] === 0){
  unset($range[0]);
}

$str = implode(',', array_map(function($val){return str_repeat('I', $val);}, $range));
if(preg_match('#^(?=(I*))(^\1|,\2I|\2I)+$#', $str)){
  echo 'works !';
}else{
  echo 'fails !';
}

Now let's take in account to return the number that's missing, we will remove the $ end character to make our regex not fail, and we use group 2 to return the missed number:

$range = range(0,7);
if($range[0] === 0){
  unset($range[0]);
}
unset($range[2]); // remove 2

$str = implode(',', array_map(function($val){return str_repeat('I', $val);}, $range));
preg_match('#^(?=(I*))(^\1|,\2I|\2I)+#', $str, $m); // REGEEEEEX !!!

$n = strlen($m[2]); //get the length ie the number
$sum = array_sum($range); // array sum

if($n == $sum){
  echo $n + 1; // no missing number
}else{
  echo $n - 1; // missing number
}

Online demo

Doralynne answered 15/8, 2013 at 22:30 Comment(2)
Your algo approach is ok, but you should use array_unique, and perhaps consider negative numbers in the array... Besides, instead of sort , end and reset, using min and max makes much more sense, to me. Since you opened a bounty, perhaps check my answer. 3 lines of code, does what it says on the tin. Nice and simpleGarnes
@EliasVanOotegem Yeah I just checked your answer +1Doralynne
O
9

EDIT: NOTE
This question is about performance. Functions like array_diff and array_filter are not magically fast. They can add a huge time penalty. Replacing a loop in your code with a call to array_diff will not magically make things fast, and will probably make things slower. You need to understand how these functions work if you intend to use them to speed up your code.

This answer uses the assumption that no items are duplicated and no invalid elements exist to allow us to use the position of the element to infer its expected value.

This answer is theoretically the fastest possible solution if you start with a sorted list. The solution posted by Jack is theoretically the fastest if sorting is required.

In the series [0,1,2,3,4,...], the n'th element has the value n if no elements before it are missing. So we can spot-check at any point to see if our missing element is before or after the element in question.

So you start by cutting the list in half and checking to see if the item at position x = x

[ 0 | 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 ]
                  ^

Yup, list[4] == 4. So move halfway from your current point the end of the list.

[ 0 | 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 ]
                          ^

Uh-oh, list[6] == 7. So somewhere between our last checkpoint and the current one, one element was missing. Divide the difference in half and check that element:

[ 0 | 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 ]
                      ^

In this case, list[5] == 5

So we're good there. So we take half the distance between our current check and the last one that was abnormal. And oh.. it looks like cell n+1 is one we already checked. We know that list[6]==7 and list[5]==5, so the element number 6 is the one that's missing.

Since each step divides the number of elements to consider in half, you know that your worst-case performance is going to check no more than log2 of the total list size. That is, this is an O(log(n)) solution.

If this whole arrangement looks familiar, It's because you learned it back in your second year of college in a Computer Science class. It's a minor variation on the binary search algorithm--one of the most widely used index schemes in the industry. Indeed this question appears to be a perfectly-contrived application for this searching technique.

You can of course repeat the operation to find additional missing elements, but since you've already tested the values at key elements in the list, you can avoid re-checking most of the list and go straight to the interesting ones left to test.

Also note that this solution assumes a sorted list. If the list isn't sorted then obviously you sort it first. Except, binary searching has some notable properties in common with quicksort. It's quite possible that you can combine the process of sorting with the process of finding the missing element and do both in a single operation, saving yourself some time.

Finally, to sum up the list, that's just a stupid math trick thrown in for good measure. The sum of a list of numbers from 1 to N is just N*(N+1)/2. And if you've already determined that any elements are missing, then obvously just subtract the missing ones.

Overrun answered 16/8, 2013 at 6:38 Comment(8)
In terms of runtime, asort + binary chop is the fastest algo as tylerl explains. Yes, it involves a loop, but only log N iterations at most with no function calls in the loop so it will be fast to execute in PHP.Rois
So it's the fastest way to go if the array's min value is 0, contains no duplicates, no strings and no null's. To be sure, you'll also need to pass the array through array_filter and array_unique, then sort it, too. And of course, check for the min and max values. Then you'll be good and readyGarnes
@EliasVanOotegem using tools like array_filter and array_unique defeats the purpose, since both will add a huge penalty. Duplicates and nulls were not part of the problem description, therefore we can assume they are not present. If the base value is not zero (which it is, per the problem description), you can simply subtract the value at position 0 before doing the comparison with no significant performance penalty. Checking min and max is redundant.Overrun
@tylerl: I know they add a huge penalty. The base value is not said to be zero ("I have a range of whole numbers, that might or might not have some numbers missing"), only the array in the example has zero as min. There is no mention of null, or duplicates being possible, but absence of evidence is not evidence of absence. I prefer a more defensive approach...Garnes
@EliasVanOotegem assuming these restrictions are not in place, then the fastest solution is the one he posted. It touches each element only once. The only way to speed it up is to do something that doesn't touch every array element (hence this answer). All the other answers posted are slower than the one in the question -- many of them significantly slower.Overrun
My answer is actually faster because it does away with the asort() with a memory compromise. Just FYI :)Erastian
@Jack Ah. You posted a bin sort example; you can't do faster than that if you assume sorting must be done.Overrun
Yeah, if there's no need to sort your answer is the fastest, as mentioned in an earlier comment :)Erastian
T
6

Technically, you can't really do without the loop (unless you only want to know if there's a missing number). However, you can accomplish this without first sorting the array.

The following algorithm uses O(n) time with O(n) space:

$range = [0, 1, 2, 3, 4, 6, 7];

$N = count($range);
$temp = str_repeat('0', $N); // assume all values are out of place

foreach ($range as $value) {
    if ($value < $N) {
        $temp[$value] = 1; // value is in the right place
    }
}

// count number of leading ones
echo strspn($temp, '1'), PHP_EOL;

It builds an ordered identity map of N entries, marking each value against its position as "1"; in the end all entries must be "1", and the first "0" entry is the smallest value that's missing.

Btw, I'm using a temporary string instead of an array to reduce physical memory requirements.

Tish answered 19/8, 2013 at 7:17 Comment(0)
G
5

I honestly don't get why you wouldn't want to use a loop. There's nothing wrong with loops. They're fast, and you simply can't do without them. However, in your case, there is a way to avoid having to write your own loops, using PHP core functions. They do loop over the array, though, but you simply can't avoid that.
Anyway, I gather what you're after, can easily be written in 3 lines:

function highestPlus(array $in)
{
    $compare = range(min($in), max($in));
    $diff = array_diff($compare, $in);
    return empty($diff) ? max($in) +1 : $diff[0];
}

Tested with:

echo highestPlus(range(0,11));//echoes 12
$arr = array(9,3,4,1,2,5);
echo highestPlus($arr);//echoes 6

And now, to shamelessly steal Pé de Leão's answer (but "augment" it to do exactly what you want):

function highestPlus(array $range)
{//an unreadable one-liner... horrid, so don't, but know that you can...
     return min(array_diff(range(0, max($range)+1), $range)) ?: max($range) +1;
}

How it works:

$compare = range(min($in), max($in));//range(lowest value in array, highest value in array)
$diff = array_diff($compare, $in);//get all values present in $compare, that aren't in $in
return empty($diff) ? max($in) +1 : $diff[0];
//-------------------------------------------------
// read as:
if (empty($diff))
{//every number in min-max range was found in $in, return highest value +1
    return max($in) + 1;
}
//there were numbers in min-max range, not present in $in, return first missing number:
return $diff[0];

That's it, really.
Of course, if the supplied array might contain null or falsy values, or even strings, and duplicate values, it might be useful to "clean" the input a bit:

function highestPlus(array $in)
{
    $clean = array_filter(
        $in,
        'is_numeric'//or even is_int
    );
    $compare = range(min($clean), max($clean));
    $diff = array_diff($compare, $clean);//duplicates aren't an issue here
    return empty($diff) ? max($clean) + 1; $diff[0];
}

Useful links:

Garnes answered 18/8, 2013 at 17:42 Comment(0)
Y
3
$range = array(0,1,2,3,4,6,7);    
// sort just in case the range is not in order
asort($range);
$range = array_values($range);
$indexes = array_keys($range);
$diff = array_diff($indexes,$range);

echo $diff[0]; // >> will print: 5 
// if $diff is an empty array - you can print 
// the "maximum value of the range plus one": $range[count($range)-1]+1
Yet answered 15/8, 2013 at 22:10 Comment(8)
This will not return the smallest missing number, but all missing numbers. Technically, not a correct answer...Maryjanemaryjo
@cuewizchris - the smallest number is in $diff[0] (if it exists).Yet
It should be mentioned the above assumes a valid range is one starting with 0. Won't work for checking "continuity" of a range starting with an arbitrary number.Carboloy
@Carboloy you're right - I would add it to the question if further generalization is required.Yet
@Carboloy @alfasin If the first value is non-zero, it looks like we can redefine $range = array_combine(range(min($range), count($range)), array_values($range)); and $indexes = range(min($range), count($range)); then find the min($diff) for the answer.Polynesia
@BenHarold in order to use array_combine() you need two arrays in the same length - and if there's a missing number - it might not be the case.Yet
This answer will only work if the lowest value in $range === 0. If the given array is array(2,3,4), it'll be compared to array(0,1,2)Garnes
@EliasVanOotegem you're right - lafor already mentioned it here in the comments.Yet
O
1
echo min(array_diff(range(0, max($range)+1), $range));
Ordinate answered 16/8, 2013 at 11:34 Comment(0)
B
1

Simple

$array1 = array(0,1,2,3,4,5,6,7);// array with actual number series
$array2 = array(0,1,2,4,6,7); // array with your custom number series
$missing = array_diff($array1,$array2);
sort($missing);
echo $missing[0]; 
Bradney answered 19/8, 2013 at 16:44 Comment(0)
O
1
$range = array(0,1,2,3,4,6,7);

$max=max($range);

$expected_total=($max*($max+1))/2; // sum if no number was missing.

$actual_total=array_sum($range);  // sum of the input array.

if($expected_total==$actual_total){
   echo $max+1;      // no difference so no missing number, then echo 1+ missing number.
}else{
   echo $expected_total-$actual_total; // the difference will be the missing number.
}
Orphism answered 20/8, 2013 at 7:29 Comment(0)
A
1

you can use array_diff() like this

<?php
        $range = array("0","1","2","3","4","6","7","9");
        asort($range);

    $len=count($range);
    if($range[$len-1]==$len-1){
      $r=$range[$len-1];
   }
    else{
    $ref= range(0,$len-1);
    $result = array_diff($ref,$range);
    $r=implode($result);
}
echo $r;

?>
Ava answered 21/8, 2013 at 14:24 Comment(0)
L
1
function missing( $v ) {
    static $p = -1;
    $d = $v - $p - 1;
    $p = $v;
    return $d?1:0;
}

$result = array_search( 1, array_map( "missing", $ARRAY_TO_TEST ) );
Lovell answered 22/8, 2013 at 9:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.