Можно ли исключить цикл for из этого фрагмента PHP-кода?

у меня есть диапазон целых чисел, которые могут или не могут иметь некоторые номера отсутствуют. Можно ли найти наименьшее отсутствующее число без использования структуры цикла? Если нет отсутствующих номеров, функция должна возвращать максимальное значение диапазона плюс один.

вот как я решил это с помощью for петли:

$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;
}

В идеале, я хотел бы избежать цикла полностью, так как диапазон может быть массивным. Есть предложения?

10 ответов


EDIT: ПРИМЕЧАНИЕ
Этот вопрос касается производительности. Функции как array_diff и array_filter не волшебно быстро. Они могут добавить огромный время казни. Замена цикла в коде вызовом array_diff не будет волшебным образом делать вещи быстро, и вероятно, замедлит. Вам нужно понять, как эти функции работают, если вы собираетесь использовать их для ускорения кода.

этот ответ использует предположение, что никакие элементы не дублируются и недопустимые элементы не существуют, позволяет нам использовать положение элемента для вывода его ожидаемого значения.

этот ответ теоретически является самым быстрым возможным решением если вы начинаете с отсортированный список. The решение опубликовано Jack теоретически является самым быстрым, если требуется сортировка.

в серии [0,1,2,3,4,...], the n' - й элемент имеет значение n если никакие элементы перед ним не отсутствуют. Таким образом, мы можем проверить в любой момент, чтобы увидеть, является ли наш недостающий элемент до или после элемент в вопрос.

Итак, вы начинаете с разрезания списка пополам и проверки, чтобы увидеть, если элемент в позиции x = x

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

да, list[4] == 4. Поэтому переместитесь на полпути от вашей текущей точки в конец списка.

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

ой-ой, list[6] == 7. Так что где-то между последней контрольно-пропускной пункт и текущий, один элемент отсутствовал. Разделите разницу пополам и проверьте этот элемент:

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

в этом случае list[5] == 5

так что у нас все хорошо. Поэтому мы берем половину расстояния между нашей текущей проверкой и последней, которая была ненормальной. И еще.. похоже на cell n+1 - это мы уже проверили. Мы знаем, что list[6]==7 и list[5]==5, так что элемент номер 6-это тот, который отсутствует.

так как каждый шаг делит количество элементов, которые нужно рассмотреть пополам, вы знаете, что ваша худшая производительность будет проверять не более log2 общего размера списка. То есть, это O (log (n)) решение.

если все это выглядит знакомым, это потому, что вы узнали это еще на втором курсе колледжа в классе информатики. Это незначительная вариация на алгоритм бинарного поиска--одна из наиболее широко используемых индексных схем в промышленность. Действительно, этот вопрос кажется совершенно надуманным приложением для этой техники поиска.

вы можете, конечно, повторить операцию, чтобы найти дополнительные недостающие элементы, но так как вы уже протестировали значения в ключевых элементах в списке, вы можете избежать повторной проверки большей части списка и перейти прямо к интересным слева для тестирования.

также обратите внимание, что это решение предполагает отсортированный список. Если список не отсортированный тогда очевидно, сначала разберешься. Кроме того, двоичный поиск имеет некоторые примечательные свойства, общие с quicksort. Вполне возможно, что вы сможете совместить процесс сортировки с процессом поиска недостающего элемента и сделать и то и другое за одну операцию, сэкономив себе некоторое время.

наконец, чтобы подвести итог списку, это просто глупый математический трюк, брошенный для хорошей меры. Сумма списка чисел от 1 до N просто N*(N+1)/2. И если вы уже определили, что элементы отсутствуют, то obvously просто вычесть недостающие.


решение Algo

существует способ проверить, есть ли недостающее число с помощью алгоритма. Это объясняется здесь. В основном, если нам нужно добавить числа от 1 до 100. Нам не нужно вычислять, суммируя их, нам просто нужно сделать следующее:(100 * (100 + 1)) / 2. И как это решит нашу проблему ?

мы собираемся, чтобы получить первый элемент массива и последний. Мы вычисляем сумму с помощью этого algo. Затем мы используем array_sum() для расчета фактическая сумма. Если результаты одинаковы, то пропущенного числа нет. Затем мы могли бы "отследить" недостающее число, вычитая фактическую сумму из вычисленной. Это, конечно, работает только в том случае, если отсутствует только одно число и потерпит неудачу, если их несколько. Итак, давайте поставим это в код:

  $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
    }
  }

выход

8
3

онлайн демо

если несколько номеров отсутствуют, тогда просто используйте array_map() или что-то похожее на внутренний цикл.


решение Regex

давайте перейдем на новый уровень и используем regex ! Я знаю, что это ерунда, и ее не следует использовать в реальном мире. Цель состоит в том, чтобы показать истинную силу regex:)

Итак, сначала давайте сделаем строку из нашего ассортимента в следующем формате: I,II,III,IIII на выбор 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;

на выход должно быть что-то например:I,II,III,IIII,IIIII,IIIIII,IIIIIII.

я придумал следующее регулярное выражение: ^(?=(I+))(^|,I|I)+$. Так что же это значит ?

^                   # 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
    ^             # match begin of string followed by what's matched in group 1
        |           # or
    ,I            # match a comma, with what's matched in group 2 (recursive !) and an I
        |           # or
    I             # match what's matched in group 2 and an I
)+                  # repeat one or more times
$                   # match end of line

давайте посмотрим, что происходит на самом деле ....

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
^
^ match what was matched in group 1, which means I gets matched

I,II,III,IIII,IIIII,IIIIII,IIIIIII
 ^^^ ,I 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
    ^^^^ I 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
        ^^^^^ I 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.

смотрите, как он работает и терпит неудачу. И давайте положим его в PHP-кода:

$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*))(^|,I|I)+$#', $str)){
  echo 'works !';
}else{
  echo 'fails !';
}

теперь давайте учитывать, чтобы вернуть номер, который отсутствует, мы удалим $ end символ, чтобы сделать наше регулярное выражение не терпит неудачу, и мы используем группу 2 для возврата пропущенный номер:

$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*))(^|,I|I)+#', $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
}

онлайн демо


технически вы не можете обойтись без цикла (если только вы не хотите знать только Если отсутствует номер). Тем не менее, вы можете выполнить это без первая сортировка массива.

следующий алгоритм использует O(n) время с O (n) пространством:

$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;

он строит упорядоченную карту идентификаторов из N записей, отмечая каждое значение против его позиции как "1"; в конце концов все записи должны быть "1" , а первая запись " 0 " является наименьшим значением этого не хватает.

кстати, я использую временную строку вместо массива для снижения требований к физической памяти.


я честно не понимаю, почему вы не хотите использовать цикл. Нет ничего неправильно с петель. Они быстрые, и без них просто не обойтись. Однако в вашем случае есть способ избежать необходимости писать собственные циклы, используя основные функции PHP. Они делают цикл по массиву, но вы просто не можете избежать этого.
Во всяком случае, я понимаю, что вам нужно, можно легко написать в 3 строках:

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

проверено с:

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

а теперь, чтобы бесстыдно украсть ответ Пе де Леао (но "пополнить" его делать именно то, что вы хотите):

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;
}

как работает:

$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];

вот и все, правда.
Конечно, если поставляемый массив может содержать null или falsy значения, или даже строки, и повторяющиеся значения, это может быть полезным "очистить" вход немного:

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];
}

полезное ссылки:


$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

echo min(array_diff(range(0, max($range)+1), $range));

простой

$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]; 

$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.
}

можно использовать array_diff() такой

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

?>

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