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

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

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

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

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

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

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

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

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

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

Uh-oh, 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)) .

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

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

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

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

Решение Algo

Существует способ проверить, есть ли недостающее число, используя алгоритм. Это объясняется здесь . В принципе, если нам нужно добавить числа от 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 } } с  $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 } } с  $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 🙂

Поэтому сначала сделаем строку из нашего диапазона в следующем формате: 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; с $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 !'; } с $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 !'; } 

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

Демо-версия

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

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

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

Я честно не понимаю, почему вы не хотите использовать цикл. В петлях нет ничего плохого . Они быстры, и вы просто не можете обойтись без них. Однако в вашем случае есть способ избежать необходимости писать свои собственные циклы, используя основные функции 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 страница array_diff
  • Функции max и min
  • Хороший Ол, конечно …
  • Функция array_filter
  • Функция array_map может стоить взгляда
  • Так же, как array_sum может быть
 $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 ) );