Что такое алгоритм и сложность? Big O для людей
Алгоритм — это не страшная математика из учебника. Это просто чёткий набор шагов для решения задачи. В этом уроке мы разберёмся, почему алгоритмы важны и как оценивать их эффективность с помощью нотации Big O — без формул и слёз.
🗺️ Что такое алгоритм
Слово «алгоритм» звучит серьёзно и научно. На самом деле вы используете алгоритмы каждый день, просто не называете их так.
Рецепт приготовления пасты — это алгоритм. Инструкция по сборке мебели из 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 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 вопросов