Перейти к содержимому
Теория · Условия и циклы · урок 11 из 13

Вложенные циклы

Бегун готовится к соревнованиям, и у тренера план на несколько дней: каждый день пробежать по три круга. В блокноте бегун отмечает пары «день — круг»: день 1, круг 1; день 1, круг 2; и так далее. Когда круги дня закончились, наступает следующий день, и счёт кругов начинается заново.

Такой план — цикл внутри цикла. Внешний цикл перебирает дни, а вложенный цикл — круги внутри одного дня:

C#
for (int day = 1; day <= 2; day++)
{
    for (int lap = 1; lap <= 3; lap++)
    {
        Console.WriteLine(day + " " + lap);
    }
}
// 1 1
// 1 2
// 1 3
// 2 1
// 2 2
// 2 3

Выражение day + " " + lap склеивает число, пробел и второе число в одну строку. Давайте разберём, что происходит:

  1. Внешний цикл начинает с day = 1.
  2. Внутренний цикл проходит lap от 1 до 3, и программа печатает 1 1, 1 2, 1 3.
  3. Внутренний цикл закончился, и внешний выполняет свой шаг: day становится равной 2.
  4. Внутренний цикл начинается заново, с lap = 1, и печатает 2 1, 2 2, 2 3.
  5. Шаг внешнего цикла даёт day = 3, условие day <= 2 ложно, и оба цикла позади.

Внутренний цикл проходит целиком на каждой итерации внешнего. Поэтому его тело выполняется 2 * 3, то есть шесть раз: число итераций внешнего цикла, умноженное на число итераций внутреннего.

На собеседовании это свойство обсуждают, когда говорят о скорости программы. Если оба цикла проходят по n значений, тело выполнится n * n раз: при n = 1000 это уже миллион итераций.

Внутренний цикл зависит от внешнего

Условие внутреннего цикла может использовать переменную внешнего. Так рисуют треугольник из звёздочек, где в строке с номером row ровно row звёздочек:

C#
for (int row = 1; row <= 4; row++)
{
    for (int k = 1; k <= row; k++)
    {
        Console.Write("*");
    }
    Console.WriteLine();
}
// *
// **
// ***
// ****

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

  1. Внешний цикл перебирает строки: row от 1 до 4.
  2. Внутренний цикл печатает звёздочки через Console.Write, и все они остаются в одной строке. Условие k <= row даёт число звёздочек, равное номеру строки.
  3. Console.WriteLine() с пустыми скобками стоит во внешнем цикле, после внутреннего, и переводит строку.

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

break во вложенном цикле

break завершает только ближайший цикл, тот, в котором стоит. Если он внутри вложенного цикла, внешний цикл выполнит остаток своего тела после внутреннего и перейдёт к следующей итерации.

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

Чтобы выйти сразу из обоих циклов, заводят переменную-флаг типа bool и проверяют её в условии внешнего цикла. Найдём первую пару чисел от 1 до 3, произведение которых равно 6:

C#
bool found = false;

for (int a = 1; a <= 3 && !found; a++)
{
    for (int b = 1; b <= 3; b++)
    {
        if (a * b == 6)
        {
            Console.WriteLine(a + " " + b); // 2 3
            found = true;
            break;
        }
    }
}

Разберём, как выходят оба цикла:

  1. Пока пара не найдена, found равна false, и условие внешнего цикла зависит только от a.
  2. При a = 2 и b = 3 произведение равно 6: программа выводит 2 3 и записывает в found значение true.
  3. break завершает внутренний цикл.
  4. Внешний цикл выполняет шаг a++ и проверяет условие: !found теперь ложно, и внешний цикл тоже заканчивается.

Без && !found внешний цикл перешёл бы к a = 3 и вывел бы ещё пару 3 2. На собеседовании иногда спрашивают, как выйти сразу из двух циклов, и флаг в условии внешнего цикла — один из ответов.

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

Во вложенном цикле взяли то же имя переменной, что и во внешнем:

C#
for (int i = 1; i <= 2; i++)
{
    for (int i = 1; i <= 3; i++) // ✗
    {
        Console.Write(i);
    }
}
TEXT
error CS0136: A local or parameter named 'i' cannot be declared in this scope because that name is used in an enclosing local scope to define a local or parameter

Программа не компилируется. Внутренний цикл стоит внутри внешнего, а там переменная i уже есть. Второе объявление с тем же именем сделало бы непонятным, о какой i идёт речь, и C# его запрещает. Дайте переменной внутреннего цикла своё имя, например j.

Главное

  • Вложенный цикл проходит целиком на каждой итерации внешнего: при 2 днях по 3 круга его тело выполнится 2 * 3 раз.
  • Условие внутреннего цикла может использовать переменную внешнего.
  • break завершает только ближайший цикл; чтобы выйти из обоих, проверяют флаг в условии внешнего.
  • У переменной вложенного цикла должно быть своё имя: повторить имя переменной внешнего цикла нельзя.