$ sudo teach IT
МОДУЛЬ 4 · УРОК 4

Вложенные циклы: цикл внутри цикла

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

⏱ ~25 минут 🎓 Для новичков 🟨 JavaScript

🧩 Идея: два измерения — два цикла

Представьте, что вам нужно обойти все клетки шахматной доски 3×3. С одним циклом мы можем обойти один ряд. А чтобы обойти все ряды — нам нужен ещё один цикл снаружи.

Аналогия из жизни: вы проверяете все карманы в куртке. Куртка — внешний цикл (каждый карман). Внутри каждого кармана — внутренний цикл (достаёте каждую вещь). Сначала полностью обшариваете первый карман, потом второй, и так далее.

// Простейший вложенный цикл
for (let i = 1; i <= 3; i++) {       // внешний: строки
  for (let j = 1; j <= 3; j++) {     // внутренний: столбцы
    console.log(`(${i}, ${j})`);
  }
}

// Результат:
// (1, 1)  (1, 2)  (1, 3)
// (2, 1)  (2, 2)  (2, 3)
// (3, 1)  (3, 2)  (3, 3)

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

  • Внешний цикл устанавливает i = 1
  • Внутренний пробегает j от 1 до 3 полностью: (1,1), (1,2), (1,3)
  • Внешний переходит к i = 2
  • Внутренний снова пробегает j от 1 до 3: (2,1), (2,2), (2,3)
  • И так для каждого значения i

🔑 Ключевое правило: при каждой итерации внешнего цикла внутренний цикл выполняется полностью — от начала до конца.

🧮 Сколько итераций? Умножаем!

Вот очень важная вещь, которую нужно понять про вложенные циклы: количество итераций перемножается.

1️⃣
Один цикл N=10

10

итераций

2️⃣
Вложенный 10×10

100

итераций

3️⃣
Три уровня 10×10×10

1000

итераций

⚠️
При N=1000: 1000×1000

1 000 000

итераций — уже медленно!

Вот почему важно понимать, что происходит внутри вложенных циклов. Для маленьких N (десятки, сотни) это незаметно. Но когда N растёт — скорость падает квадратично: в два раза больше данных → в четыре раза дольше работает.

✖️ Классика: таблица умножения

Самый классический пример вложенных циклов — таблица умножения. Строки — один цикл, столбцы — другой:

// Таблица умножения от 1 до 5
for (let i = 1; i <= 5; i++) {
  let row = "";
  for (let j = 1; j <= 5; j++) {
    // padStart выравнивает число до 4 символов
    row += String(i * j).padStart(4, " ");
  }
  console.log(row);
}

// Результат:
//    1   2   3   4   5
//    2   4   6   8  10
//    3   6   9  12  15
//    4   8  12  16  20
//    5  10  15  20  25

Разбор: для каждой строки (значения i) мы собираем строку row, пробегая по всем столбцам (значениям j). Метод padStart(4, " ") добавляет пробелы в начало, чтобы числа выровнялись.

🗂️ Матрица: двумерный массив

В JavaScript матрица — это массив массивов. Каждый элемент внешнего массива — это строка (тоже массив). Обращаться к элементу: matrix[строка][столбец].

// Матрица 3×3
const matrix = [
  [1, 2, 3],   // строка 0
  [4, 5, 6],   // строка 1
  [7, 8, 9]    // строка 2
];

// Как обратиться к элементу:
console.log(matrix[0][0]); // 1 (строка 0, столбец 0)
console.log(matrix[1][2]); // 6 (строка 1, столбец 2)
console.log(matrix[2][1]); // 8 (строка 2, столбец 1)

Чтобы обойти все элементы матрицы, нужны вложенные циклы — внешний по строкам, внутренний по столбцам:

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

let sum = 0;

for (let i = 0; i < matrix.length; i++) {        // перебор строк
  for (let j = 0; j < matrix[i].length; j++) {   // перебор столбцов
    sum += matrix[i][j];
  }
}

console.log("Сумма всех элементов:", sum); // 45

matrix.length — количество строк. matrix[i].length — количество столбцов в строке i (это важно: строки могут быть разной длины в JavaScript, хотя в математических матрицах обычно одинаковы).

Давайте создадим матрицу программно — заполним её так, что элемент [i][j] равен произведению i * j:

const n = 4;
const matrix = [];

for (let i = 0; i < n; i++) {
  matrix[i] = []; // создаём пустую строку
  for (let j = 0; j < n; j++) {
    matrix[i][j] = i * j;
  }
}

// matrix выглядит так:
// [[0, 0, 0, 0],
//  [0, 1, 2, 3],
//  [0, 2, 4, 6],
//  [0, 3, 6, 9]]

⚡ Почему вложенные циклы могут быть медленными

Представьте: у вас есть список из 1000 студентов. Вам нужно найти все пары студентов с одинаковым именем. Наивное решение — сравнить каждого с каждым:

// Поиск дубликатов: O(n²) — медленный способ
for (let i = 0; i < students.length; i++) {
  for (let j = i + 1; j < students.length; j++) {  // j начинается с i+1
    if (students[i].name === students[j].name) {
      console.log("Совпадение:", students[i].name);
    }
  }
}
// При 1000 студентов: ~500 000 сравнений!

Это работает, но медленно. Если список вырастет до 10 000 студентов — будет 50 миллионов сравнений. Компьютер справится, но начнёт заметно тормозить.

💡 O(n²) — квадратичная сложность. Количество операций растёт как квадрат от размера данных:

  • Данных в 10 раз больше → операций в 100 раз больше
  • Данных в 100 раз больше → операций в 10 000 раз больше

Для больших данных часто есть более умные алгоритмы (например, сортировка + один цикл). Но для новичков главное — осознавать, что вложенные циклы умножают итерации, и не использовать их там, где достаточно одного.

Когда вложенные циклы — норм
  • Обход матрицы N×M
  • Таблица умножения
  • Рисование сетки/паттерна
  • Небольшие данные (N < 1000)
Когда стоит задуматься
  • Сравнение каждого с каждым
  • Большие массивы (тысячи эл-тов)
  • Три уровня вложенности
  • Видимые тормоза в браузере

🛑 break во вложенных циклах: только из внутреннего!

Вот важная вещь, которая удивляет многих новичков: break во вложенных циклах выходит только из внутреннего цикла, а не из всей конструкции.

for (let i = 1; i <= 3; i++) {
  console.log("Внешний:", i);
  for (let j = 1; j <= 5; j++) {
    if (j === 3) break; // выходим только из внутреннего!
    console.log("  Внутренний:", j);
  }
}

// Результат:
// Внешний: 1
//   Внутренний: 1
//   Внутренний: 2
// Внешний: 2
//   Внутренний: 1
//   Внутренний: 2
// Внешний: 3
//   Внутренний: 1
//   Внутренний: 2

Внутренний цикл останавливается при j = 3. Но внешний цикл продолжает работать! Каждая итерация i заново запускает внутренний цикл, который снова останавливается на j = 3.

Что если нужно выйти из обоих циклов сразу? Один из способов — использовать флаг:

let found = false;
const target = 6;

for (let i = 1; i <= 5 && !found; i++) {
  for (let j = 1; j <= 5; j++) {
    if (i * j === target) {
      console.log(`Нашли: ${i} × ${j} = ${target}`);
      found = true;
      break; // выходим из внутреннего
    }
  }
}
// При found = true условие внешнего цикла "i <= 5 && !found" станет false
// Внешний цикл тоже остановится

⚠️ Помните: break всегда выходит только из ближайшего цикла. Для выхода из нескольких уровней нужен флаг или вынести код в функцию с return.

🎨 Практика: рисуем паттерн из звёздочек

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

// Треугольник из звёздочек
const n = 5;

for (let i = 1; i <= n; i++) {
  let row = "";
  for (let j = 1; j <= i; j++) {
    row += "* ";
  }
  console.log(row);
}

// Результат:
// *
// * *
// * * *
// * * * *
// * * * * *

Заметьте: внутренний цикл идёт j <= i, то есть на первой строке 1 звёздочка, на второй 2, на третьей 3... Именно поэтому получается треугольник.

// Прямоугольник
for (let i = 1; i <= 3; i++) {
  let row = "";
  for (let j = 1; j <= 5; j++) {
    row += "# ";
  }
  console.log(row);
}

// Результат:
// # # # # #
// # # # # #
// # # # # #

И пример с матрицей — создадим и посчитаем сумму всех элементов матрицы N×N, где элемент [i][j] = i * j:

function matrixSum(n) {
  let total = 0;
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      total += i * j;
    }
  }
  return total;
}

console.log(matrixSum(3)); // 0+0+0 + 0+1+2 + 0+2+4 = 9
console.log(matrixSum(4)); // 36

✅ Итоги урока

  • Вложенные циклы — это цикл внутри цикла. Внутренний выполняется полностью при каждой итерации внешнего
  • Количество итераций перемножается: внешний N × внутренний M = N×M операций всего
  • Матрица в JS — массив массивов. Доступ: matrix[строка][столбец]
  • Обход матрицы: внешний цикл по строкам (i), внутренний по столбцам (j)
  • O(n²) — квадратичная сложность: данных в 10 раз больше → операций в 100 раз больше
  • break во вложенном цикле выходит только из внутреннего, не из внешнего
  • Для выхода из нескольких циклов сразу — флаг или вынос в функцию с return

Поздравляем! Вы освоили все основные виды циклов в JavaScript. Теперь вы умеете повторять действия, управлять потоком и работать с двумерными данными. 🚀

Тест по вложенным циклам

7 вопросов

Таблица умножения N×N

Напишите функцию multiplicationTable(n), которая принимает число n и возвращает строку с таблицей умножения от 1 до n, где строки разделены символом новой строки '\\n', а числа в строке разделены пробелом. Используйте вложенные циклы.

Сумма элементов матрицы

Напишите функцию sumMatrix(matrix), которая принимает двумерный массив (матрицу) чисел и возвращает сумму всех её элементов, используя вложенные циклы.