Можно ли исключить цикл 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;
}

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


person Ben Harold    schedule 15.08.2013    source источник
comment
огромный массив со всеми числами, затем array_diff (), но он все еще использует цикл внутри. повторение цикла range = (даже при внутренней обработке). В последнее время видел несколько вопросов. Я не хочу задавать вопросы. Кто научил вас этому?   -  person    schedule 16.08.2013
comment
Пробовал ваш код. Согласно вашему массиву $ range, он должен вернуть 5 (которого нет). Вместо этого он возвращает 8. Значит, он даже не работает должным образом.   -  person ciruvan    schedule 16.08.2013
comment
@cuewizchris Ой! Я пропустил последнюю строку ($ first = false). Спасибо, что уловили это.   -  person Ben Harold    schedule 16.08.2013
comment
Код не компилировался, потому что диапазон $ был определен как: $range = [0,1,2,3,4,6,7]; вместо: $range = array(0,1,2,3,4,6,7); - возможно, есть и другие проблемы - остальное я не проверял.   -  person Nir Alfasi    schedule 16.08.2013
comment
@alfasin Ой! Извините, я использую PHP 5.4   -  person Ben Harold    schedule 16.08.2013
comment
@BenHarold оххх - тогда мне плохо :)   -  person Nir Alfasi    schedule 16.08.2013
comment
А что насчет [0, 1, 2, 2, 3]? Это действительно так?   -  person Ja͢ck    schedule 19.08.2013
comment
Вы уверены, что ценности положительны и упорядочены?   -  person Hrishi    schedule 21.08.2013
comment
@Jack Да, это действительно так. php.net/manual/en/migration54.new-features.php   -  person Ben Harold    schedule 22.08.2013
comment
Я не имел в виду, действителен ли этот синтаксис? скорее, будет ли действителен этот массив неубывающих чисел? :)   -  person Ja͢ck    schedule 22.08.2013
comment
@jack Мое плохое! $range - это, по сути, набор ключей, которые находятся в уникальном индексе, поэтому повторения не должны происходить (на языке RFC 2119).   -  person Ben Harold    schedule 23.08.2013


Ответы (7)


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

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

Этот ответ теоретически является самым быстрым из возможных решений, если вы начнете с отсортированного списка. Решение , опубликованное Джеком, теоретически является самым быстрым, если требуется сортировка.

В ряду [0,1,2,3,4, ...] элемент 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

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

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

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

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

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

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

person tylerl    schedule 16.08.2013
comment
С точки зрения времени выполнения, asort + binary chop - самый быстрый алгоритм, как объясняет Тайлерл. Да, он включает в себя цикл, но не более записывать N итераций без каких-либо вызовов функций в цикле, поэтому его можно будет быстро выполнить в PHP. - person TerryE; 16.08.2013
comment
Так что это самый быстрый способ перейти, если минимальное значение массива равно 0, не содержит дубликатов, строк и null. Конечно, вам также нужно передать массив через array_filter и array_unique, а затем sort тоже. И, конечно, проверьте значения min и max. Тогда ты будешь в порядке и будешь готов - person Elias Van Ootegem; 20.08.2013
comment
@EliasVanOotegem, использующий такие инструменты, как array_filter и array_unique, не дает цели, поскольку оба добавят огромный штраф. Дубликаты и пустые значения не были частью описания проблемы, поэтому мы можем предположить, что их нет. Если базовое значение не равно нулю (а оно равно, согласно описанию проблемы), вы можете просто вычесть значение в позиции 0 перед выполнением сравнения без значительного снижения производительности. Проверка min и max избыточна. - person tylerl; 20.08.2013
comment
@tylerl: Я знаю, что они добавляют огромный штраф. Базовое значение не считается равным нулю (у меня есть диапазон целых чисел, в которых могут отсутствовать или не быть некоторые числа), только массив в примере имеет ноль как min. Нет никаких упоминаний о null или возможном дублировании, но отсутствие доказательств не является доказательством отсутствия. Я предпочитаю более оборонительный подход ... - person Elias Van Ootegem; 20.08.2013
comment
@EliasVanOotegem, если этих ограничений нет, то самым быстрым решением будет то, что он опубликовал. Он касается каждого элемента только один раз. Единственный способ ускорить это - сделать что-то, что не касается каждого элемента массива (отсюда и этот ответ). Все остальные опубликованные ответы медленнее, чем ответ в вопросе - многие из них значительно медленнее. - person tylerl; 20.08.2013
comment
Мой ответ на самом деле быстрее, потому что он устраняет asort() с компромиссом памяти. Просто к вашему сведению :) - person Ja͢ck; 23.08.2013
comment
@ Джек А. Вы разместили пример сортировки по корзине; вы не можете сделать быстрее, если считаете, что сортировка должна выполняться. - person tylerl; 23.08.2013
comment
Да, если нет необходимости сортировать, ваш ответ самый быстрый, как упоминалось в предыдущем комментарии :) - person Ja͢ck; 23.08.2013

Алго-решение

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

Мы собираемся получить первый элемент массива и последний. Рассчитываем сумму с помощью этого алгоритма. Затем мы используем 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() или что-то подобное, чтобы создать внутренний цикл.


Решение с регулярным выражением

Давайте перейдем на новый уровень и воспользуемся регулярным выражением! Я знаю, что это ерунда, и ее не следует использовать в реальных приложениях. Цель - показать истинную мощь регулярного выражения :)

Итак, сначала давайте сделаем строку из нашего диапазона в следующем формате: 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+))(^\1|,\2I|\2I)+$. Так что это значит?

^                   # 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

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

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.

Посмотрите, как он работает и не работает. И давайте поместим это в код 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*))(^\1|,\2I|\2I)+$#', $str)){
  echo 'works !';
}else{
  echo 'fails !';
}

Теперь давайте учтем, чтобы вернуть пропущенное число, мы удалим конечный символ $, чтобы наше регулярное выражение не сработало, и мы используем группу 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*))(^\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
}

Интернет-демонстрация

person HamZa    schedule 15.08.2013
comment
Ваш подход к алгоритму в порядке, но вы должны использовать array_unique и, возможно, учитывать отрицательные числа в массиве ... Кроме того, вместо sort, end и reset использование min и max для меня имеет гораздо больший смысл. Поскольку вы открыли награду, возможно, проверьте мой ответ. 3 строчки кода, делает то, что написано на жестяной коробке. Красиво и просто - person Elias Van Ootegem; 18.08.2013
comment
@EliasVanOotegem Да, я только что проверил ваш ответ +1 - person HamZa; 18.08.2013

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

Следующий алгоритм использует время 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» - это наименьшее пропущенное значение.

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

person Ja͢ck    schedule 19.08.2013

Честно говоря, я не понимаю, почему вы не хотите использовать цикл. В циклах нет ничего неправильного. Они быстрые, и без них просто не обойтись. Однако в вашем случае есть способ избежать написания собственных циклов с использованием основных функций 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];
}

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

  • Справочная страница array_diff
  • max и _ 10_ функции
  • Старый добрый range, конечно ...
  • Функция array_filter
  • Возможно, стоит взглянуть на функцию array_map
  • Так же, как может быть array_sum
person Elias Van Ootegem    schedule 18.08.2013

Простой

$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]; 
person Yatin Trivedi    schedule 19.08.2013

вы можете использовать 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;

?>
person Lucifer    schedule 21.08.2013

person    schedule
comment
Это не вернет наименьшее пропущенное число, но все пропущенные числа. Технически неправильный ответ ... - person ciruvan; 16.08.2013
comment
@cuewizchris - наименьшее число находится в $ diff [0] (если оно существует). - person Nir Alfasi; 16.08.2013
comment
Следует отметить, что вышеизложенное предполагает, что допустимый диапазон - это диапазон, начинающийся с 0. Не работает для проверки непрерывности диапазона, начинающегося с произвольного числа. - person lafor; 16.08.2013
comment
@la, вы правы - я бы добавил это к вопросу, если требуется дальнейшее обобщение. - person Nir Alfasi; 16.08.2013
comment
@lafor @alfasin Если первое значение не равно нулю, похоже, что мы можем переопределить $range = array_combine(range(min($range), count($range)), array_values($range)); и $indexes = range(min($range), count($range));, а затем найти min($diff) для ответа. - person Ben Harold; 16.08.2013
comment
@BenHarold, чтобы использовать array_combine(), вам нужны два массива одинаковой длины - и если там пропущен номер - это может быть не так. - person Nir Alfasi; 16.08.2013
comment
Этот ответ будет работать, только если наименьшее значение в $range === 0. Если данный массив равен array(2,3,4), он будет сравниваться с array(0,1,2) - person Elias Van Ootegem; 18.08.2013
comment
@EliasVanOotegem, вы правы - лафор уже упоминал об этом здесь, в комментариях. - person Nir Alfasi; 19.08.2013