Перейти к содержимому
Теория · Циклы · урок 6 из 6

Вложенные циклы и типичные ошибки

Тренер ставит задачу: три подхода, в каждом — по три круга. Бегун пробегает три круга первого подхода, потом три круга второго, потом третьего. Всего получается три на три, то есть девять кругов.

Цикл внутри другого цикла называют вложенным. Внутренний цикл проходит все свои витки на каждом витке внешнего. Так удобно строить таблицы — например, таблицу умножения:

PHP
<?php

for ($row = 1; $row <= 3; $row++) {
    $line = '';
    for ($col = 1; $col <= 3; $col++) {
        $line .= $row * $col . ' ';
    }
    echo $line, "\n";
}
// 1 2 3
// 2 4 6
// 3 6 9

Давайте разберём, в каком порядке работают циклы:

  1. Внешний цикл перебирает ряды: $row от 1 до 3.
  2. В начале каждого ряда $line = '' заводит пустую строку.
  3. Внутренний цикл перебирает столбцы: $col от 1 до 3. На каждом витке он дописывает в $line произведение и пробел.
  4. Когда внутренний цикл закончился, echo печатает готовый ряд, а внешний цикл переходит к следующему.

Тело внутреннего цикла здесь выполняется девять раз. На собеседовании часто спрашивают, сколько раз выполнится тело вложенного цикла: число витков внешнего умножают на число витков внутреннего.

Здесь картинка с тренировкой неточна: бегун устаёт, а цикл нет, и вложенность умножает работу незаметно. Два цикла по тысяче витков — это уже миллион выполнений тела.

Ряд в одной строке

Ряд собирается в переменной $line по кусочкам. Как вы уже знаете, оператор .= дописывает строку в конец: после первого столбца в $line лежит '1 ', после второго — '1 2 '.

Важно, где стоит $line = '': внутри внешнего цикла, перед внутренним. Так каждый ряд начинается с пустой строки. Если поставить $line = '' перед внешним циклом, ряды склеятся и второй echo напечатает 1 2 3 2 4 6.

Собирать ряд в строку удобнее, чем печатать каждое число отдельным echo. Перевод строки "\n" нужен только один раз, в конце ряда.

Выход из двух циклов

break во внутреннем цикле завершает только его, и внешний цикл переходит к следующему ряду. Чтобы выйти сразу из обоих, пишут break 2: число говорит, из скольких вложенных циклов выйти. Этот код ищет в таблице первую клетку со значением 6:

PHP
for ($row = 1; $row <= 3; $row++) {
    for ($col = 1; $col <= 3; $col++) {
        if ($row * $col === 6) {
            break 2;
        }
    }
}
echo $row, ' ', $col, "\n"; // 2 3

Разберём по шагам:

  1. В первом ряду значения 1, 2 и 3, шестёрки нет.
  2. Во втором ряду при $col, равном 3, произведение равно 6, и break 2 завершает оба цикла.
  3. Шаги $col++ и $row++ уже не выполняются, поэтому echo печатает найденную клетку: 2 3.

С обычным break внешний цикл продолжил бы поиск. Он нашёл бы ещё 3 * 2, закончился бы с $row, равным 4, и программа напечатала бы 4 2.

Забытый шаг счётчика

Во вложенных циклах два счётчика, и шаг иногда пишут не тому счётчику. В заголовке for ($col = 1; $col <= 3; $row++) шаг меняет $row вместо $col. Тогда $col навсегда остаётся 1, и внутренний цикл не заканчивается.

В таблице умножения такая программа долго не проработает: тело без конца дописывает в $line, и строка растёт, пока не займёт всю память, которую PHP отводит программе. Тогда PHP останавливает программу сообщением Fatal error: Allowed memory size of 134217728 bytes exhausted. Проверяйте заголовок: во всех трёх частях for стоит одно и то же имя.

Частая ошибка

Во внутреннем цикле написали < вместо <=:

PHP
for ($row = 1; $row <= 3; $row++) {
    $line = '';
    for ($col = 1; $col < 3; $col++) { // ✗
        $line .= $row * $col . ' ';
    }
    echo $line, "\n";
}

Программа работает без сообщений, но в каждом ряду два числа вместо трёх: 1 2, 2 4 и 3 6. Условие $col < 3 ложно уже при $col, равном 3, и третий столбец пропадает. Такую ошибку называют ошибкой на единицу: цикл делает на один виток больше или меньше, чем нужно.

Проверить границы помогают первый и последний виток:

  • $i = 1; $i <= $n проходит $n витков, от 1 до $n;
  • $i = 0; $i < $n тоже проходит $n витков, от 0 до $n - 1: так обходят индексы списка.

Главное

  • Внутренний цикл проходит все свои витки на каждом витке внешнего; чтобы узнать, сколько витков всего, число витков внешнего умножают на число витков внутреннего.
  • Ряд удобно собирать в строку через .= и печатать один раз, а каждый ряд начинать с $line = ''.
  • break выходит только из внутреннего цикла, break 2 — сразу из двух.
  • < и <= отличаются на один виток: проверяйте первый и последний.