$ sudo teach IT
РАЗДЕЛ 11 · УРОК 1

Что такое алгоритм и сложность? Big O для людей

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

⏱ ~25 минут 📊 Теория + практика 🧠 Алгоритмы

🗺️ Что такое алгоритм

Слово «алгоритм» звучит серьёзно и научно. На самом деле вы используете алгоритмы каждый день, просто не называете их так.

Рецепт приготовления пасты — это алгоритм. Инструкция по сборке мебели из IKEA — алгоритм. Маршрут «дом → работа» — тоже алгоритм. Общее у них одно: конкретная последовательность шагов, которая берёт входные данные и даёт результат.

📖 Алгоритм — это конечный набор чётко определённых шагов для решения задачи. У него есть вход (данные), выход (результат) и гарантия, что он завершится.

В программировании алгоритмы — это способы решать типовые задачи: отсортировать массив, найти элемент, обойти граф, сжать данные. Хороший программист знает не просто как решить задачу, но и насколько эффективно.

Возьмём простой пример. Задача: найти имя «Алёна» в списке контактов из 1000 человек.

😩
Плохой алгоритм

Перебираем все 1000 контактов подряд с самого начала — каждый раз, на каждый поиск. В худшем случае — 1000 проверок.

😎
Хороший алгоритм

Список отсортирован по алфавиту, открываем середину, «А» — в начале, идём в первую половину. Повторяем. Находим за 10 шагов вместо 1000.

Разница — в 100 раз! И это для 1000 элементов. На миллионе данных разница станет катастрофической: плохой алгоритм будет работать секунды или минуты там, где хороший справляется мгновенно.

💡 Зачем это нужно программисту

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

🛒 Интернет-магазин

У вас 500 000 товаров. Пользователь ищет «наушники». Если перебирать все товары — это медленно. Если использовать индексы (хеш-таблицы) или правильный алгоритм поиска — мгновенно. Разница = разница между успешным бизнесом и уходом пользователей.

📱 Мобильное приложение

Фильтруете фотографии по дате, добавляете теги, ищете дубликаты. Если делать это наивно — телефон нагревается, батарея садится, пользователь удаляет приложение. Правильный алгоритм — быстрый отклик, довольный пользователь.

💰 Финтех и банки

Миллионы транзакций в день. Нужно выявить подозрительные, отсортировать по времени, найти паттерны мошенничества. Здесь плохой алгоритм = убытки и штрафы регуляторов.

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

🎯 Также алгоритмы — это стандарт технических интервью. Amazon, Google, Yandex, любой серьёзный работодатель спрашивает алгоритмы. Знать их = выше зарплата и более интересные вакансии.

📐 Big O: как измеряют скорость алгоритма

Когда программисты говорят «этот алгоритм быстрее», им нужна общая система измерения. Нельзя просто сказать «10 миллисекунд» — на вашем компьютере будет одно, на старом телефоне другое. Нужна метрика, которая не зависит от железа.

Big O (нотация «О большого») — это способ описать, как растёт время работы алгоритма при увеличении входных данных. Мы смотрим не на конкретное время, а на тенденцию: если данных стало в 10 раз больше, насколько медленнее стал алгоритм?

⚡ Ключевая идея: в Big O мы смотрим на худший случай и отбрасываем константы. Нас интересует только то, как масштабируется алгоритм — как он ведёт себя при больших n.

Давайте разберём четыре самые важные сложности: O(1), O(n), O(n²) и O(log n). Запомните их — и вы будете говорить на языке алгоритмов.

⚡ O(1) — Константное время

O(1) означает, что алгоритм выполняется за одинаковое время независимо от размера входных данных. Нет входных данных 10, или 10 000, или 10 миллионов — разницы нет.

Аналогия: вы знаете номер своего шкафчика в раздевалке. Чтобы найти его, не нужно проверять все шкафчики — вы идёте прямо к нужному. 100 шкафчиков или 10 000 — вам всё равно нужен один шаг.

// O(1) — получить первый элемент массива
function getFirst(arr) {
  return arr[0]; // всегда один шаг, неважно какой длины массив
}

// O(1) — проверка значения по ключу в объекте (хеш-таблице)
const prices = { apple: 50, banana: 30, mango: 120 };
console.log(prices["mango"]); // мгновенно, 3 позиции или 3 000 000 — без разницы

Доступ по индексу к массиву, чтение по ключу из объекта/Map — это классические примеры O(1). Это идеальная сложность.

📈 O(n) — Линейное время

O(n) означает, что время растёт пропорционально количеству данных. В 10 раз больше данных — в 10 раз дольше работает. Это интуитивно понятная сложность.

Аналогия: нужно пожать руку каждому на вечеринке. 10 гостей — 10 рукопожатий. 100 гостей — 100 рукопожатий. Количество действий равно количеству людей.

// O(n) — простой цикл по всему массиву
function findMax(arr) {
  let max = arr[0];
  for (let i = 1; i < arr.length; i++) { // n итераций
    if (arr[i] > max) {
      max = arr[i];
    }
  }
  return max;
}

// O(n) — линейный поиск
function linearSearch(arr, target) {
  for (let i = 0; i < arr.length; i++) { // в худшем случае — n шагов
    if (arr[i] === target) return i;
  }
  return -1;
}

Один цикл по массиву — это O(n). Найти максимум, посчитать сумму, отфильтровать данные через один проход — O(n). Хорошая сложность — вполне нормально для большинства задач.

🐢 O(n²) — Квадратичное время

O(n²) — это когда время растёт как квадрат от размера данных. В 10 раз больше данных — в 100 раз дольше. В 100 раз больше данных — в 10 000 раз дольше!

Аналогия: на вечеринке нужно познакомить каждого гостя с каждым. 10 гостей — 100 знакомств. 100 гостей — 10 000 знакомств. Масштабируется ужасно.

Классический признак O(n²) — вложенный цикл: цикл внутри цикла, оба по n элементов.

// O(n²) — вложенный цикл: сравниваем каждую пару
function hasDuplicates(arr) {
  for (let i = 0; i < arr.length; i++) {        // n итераций
    for (let j = i + 1; j < arr.length; j++) {  // n итераций (вложенный!)
      if (arr[i] === arr[j]) return true;
    }
  }
  return false;
}

// Это O(n²) — пузырьковая сортировка (подробнее в уроке 11.3)
function bubbleSort(arr) {
  for (let i = 0; i < arr.length; i++) {         // внешний цикл
    for (let j = 0; j < arr.length - i - 1; j++) { // внутренний цикл
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

⚠️ Осторожно! O(n²) — это предупреждающий знак. Для небольших массивов (до 1000 элементов) — терпимо. Для больших данных — катастрофа. Всегда думайте: «Могу ли я избежать вложенного цикла?»

🚀 O(log n) — Логарифмическое время

O(log n) — один из самых быстрых классов. Время растёт очень медленно: данных стало в 10 раз больше, а шагов добавилось лишь немного. Это магия!

Аналогия: угадать число от 1 до 1024, задавая вопросы «больше или меньше?». Каждый ответ отсекает половину вариантов. 1024 → 512 → 256 → ... За 10 вопросов — точный ответ!

Именно так работает бинарный поиск — он каждый шаг делит задачу пополам. Подробно разберём его в уроке 11.6.

// O(log n) — бинарный поиск (предварительный вид, детали в уроке 11.6)
function binarySearch(sortedArr, target) {
  let left = 0;
  let right = sortedArr.length - 1;

  while (left <= right) {
    const mid = Math.floor((left + right) / 2);
    if (sortedArr[mid] === target) return mid;
    if (sortedArr[mid] < target) left = mid + 1; // отсекаем левую половину
    else right = mid - 1;                         // отсекаем правую половину
  }

  return -1;
}

// 1 000 000 элементов → максимум ~20 шагов!
// log₂(1 000 000) ≈ 20

💡 Запомните: log₂(n) — это количество раз, которое можно поделить n на 2. log₂(1024) = 10. log₂(1 000 000) ≈ 20. Это невероятно мало для миллиона элементов!

📊 Как растёт время: наглядная таблица

Допустим, один шаг алгоритма занимает 1 мс. Посмотрим, сколько времени займут разные алгоритмы при разном количестве данных.

n (элементов) O(1) O(log n) O(n) O(n²)
10 1 мс 3 мс 10 мс 100 мс
100 1 мс 7 мс 100 мс 10 сек
1 000 1 мс 10 мс 1 сек ~17 мин
10 000 1 мс 13 мс 10 сек ~28 часов
1 000 000 1 мс 20 мс ~17 мин ~32 года 😱

Видите? O(n²) при миллионе элементов работает 32 года. Это не опечатка. Именно поэтому выбор алгоритма — это не академический вопрос, а вполне прикладной.

🗂️ Четыре сложности: шпаргалка

O(1)
Константное

Данных хоть миллиард — время не меняется. Пример: чтение по индексу, поиск по ключу.

O(log n)
Логарифмическое

Очень быстро. Каждый шаг делит задачу пополам. Пример: бинарный поиск.

O(n)
Линейное

Нормально. Один проход по данным. Пример: простой цикл, линейный поиск.

O(n²)
Квадратичное

Медленно. Цикл в цикле. Пример: пузырьковая сортировка. Избегайте на больших данных.

📌 Порядок от лучшего к худшему: O(1) → O(log n) → O(n) → O(n log n) → O(n²) → O(2ⁿ). В этом разделе мы работаем с первыми четырьмя. O(n log n) — это быстрая сортировка и сортировка слиянием (уроки 11.4 и 11.5).

✅ Итоги урока

  • Алгоритм — это чёткий набор шагов для решения задачи с входными данными и результатом
  • Выбор алгоритма критически влияет на производительность реальных приложений
  • Big O описывает, как растёт время работы при увеличении данных — независимо от железа
  • O(1) — константное: время не зависит от данных (доступ по индексу, ключу)
  • O(log n) — логарифмическое: делим задачу пополам (бинарный поиск)
  • O(n) — линейное: один проход по всем данным (простой цикл)
  • O(n²) — квадратичное: цикл в цикле, на больших данных — катастрофа
  • Один миллион элементов: O(log n) = 20 шагов, O(n²) = 32 года

В следующем уроке рассмотрим основные алгоритмы сортировки: когда использовать встроенный .sort(), а когда важно понимать внутреннее устройство. 🔢

Алгоритмы и Big O

7 вопросов

Определение сложности алгоритма

Определите сложность Big O для каждого фрагмента кода: 1) arr[0] + arr[arr.length - 1] 2) for (let i = 0; i < n; i++) { sum += arr[i]; } 3) for (let i = 0; i < n; i++) { for (let j = 0; j < n; j++) { console.log(i, j); } } 4) while (n > 1) { n = Math.floor(n / 2); steps++; } Напишите ответ в формате: O(1), O(n), O(n²), O(log n).

Примеры разных сложностей

Напишите три функции, демонстрирующие разные сложности: 1) getFirst(arr) — O(1), возвращает первый элемент массива. 2) findMax(arr) — O(n), находит максимальное число в массиве. 3) containsDuplicate(arr) — O(n²), проверяет есть ли дубликаты (используйте вложенный цикл).