1. Введение в алгоритмическую сложность: время и память
Введение в алгоритмическую сложность: время и память
Представьте, что вам нужно найти номер телефона конкретного человека в старом бумажном справочнике. Вы можете начать с первой страницы и просматривать каждое имя по порядку, пока не найдете нужное. А можете открыть книгу ровно посередине, посмотреть, с какой буквы начинаются фамилии, и сразу отбросить половину справочника, продолжая делить оставшуюся часть пополам. Обе стратегии приведут к результату, но первая займет часы, а вторая — считанные секунды.
В программировании эти стратегии называются алгоритмами. Алгоритм — это четкая последовательность действий для решения конкретной задачи. Однако написать работающий алгоритм — это лишь половина дела. Важно понять, насколько он эффективен. Для оценки этой эффективности используется вычислительная сложность (computational complexity).
Иллюзия быстрых секунд
Первая мысль, которая приходит в голову при оценке скорости программы: нужно просто запустить ее с секундомером. Если программа отработала за секунды, значит, она быстрая. Но этот подход в корне ошибочен.
Время выполнения в секундах зависит от множества внешних факторов: * Мощности процессора (старый ноутбук против современного сервера). * Количества фоновых программ, запущенных в операционной системе. * Языка программирования и особенностей компилятора.
Один и тот же алгоритм сортировки списка из элементов на суперкомпьютере выполнится за секунды, а на микроконтроллере умных часов — за секунд. Секунды не говорят ничего о самом алгоритме, они говорят лишь о железе.
> Истинная эффективность алгоритма измеряется не в секундах, а в количестве базовых операций, которые ему необходимо выполнить.
Временная сложность: считаем шаги
Временная сложность (time complexity) показывает, как увеличивается количество операций алгоритма при увеличении объема входных данных.
Рассмотрим простой пример поиска максимального числа в списке.
В этом коде компьютер выполняет базовые операции: присваивание значений и сравнение чисел. Если в списке numbers находится элементов, цикл выполнится раз. Если элементов , цикл выполнится миллион раз.
Количество операций растет прямо пропорционально количеству элементов. В теории алгоритмов для описания такого роста используется специальная математическая нотация — О-большое (Big O Notation). В данном случае сложность составит , где — размер входных данных.
Для точного математического описания времени работы можно использовать формулу:
где — общее время выполнения алгоритма, — количество элементов (размер данных), а — среднее время выполнения одной базовой операции процессором.
Иногда алгоритмы работают настолько эффективно, что их время выполнения вообще не зависит от объема данных. Например, если нам нужно просто узнать первое число в массиве, компьютеру потребуется всего одна операция, независимо от того, содержит массив элементов или . Такая сложность называется константной и обозначается как .
Пространственная сложность: цена памяти
Процессорное время — не единственный ресурс компьютера. Любая программа во время работы сохраняет данные в оперативную память (RAM).
Пространственная сложность (space complexity) определяет, сколько дополнительной оперативной памяти потребуется алгоритму для обработки входных данных.
Представьте, что вам нужно перевернуть массив чисел задом наперед.
| Характеристика | Временная сложность | Пространственная сложность | |---|---|---| | Что измеряет | Количество элементарных вычислительных шагов | Объем выделяемой оперативной памяти | | От чего зависит | От структуры циклов, рекурсий и условий | От создаваемых переменных и новых структур данных | | Главный ресурс | Процессорное время (CPU) | Оперативная память (RAM) |
Баланс между временем и памятью
В идеальном мире программисты создают алгоритмы, которые работают мгновенно и не потребляют память. В реальности часто приходится идти на компромисс, который называется time-memory trade-off.
Суть компромисса проста: вы можете ускорить работу программы, если пожертвуете частью оперативной памяти, и наоборот.
Яркий пример — вычисление чисел Фибоначчи. Если вычислять -е число Фибоначчи с помощью простой рекурсии (когда функция вызывает саму себя), алгоритм не потребует дополнительной памяти, но выполнит миллиарды операций. Процессор будет загружен на процентов в течение нескольких минут.
Если же создать массив из элементов и сохранять в него каждое вычисленное число (метод мемоизации), алгоритм выполнит всего операций и выдаст ответ за долю миллисекунды. Мы потратили немного оперативной памяти на массив, но сэкономили колоссальное количество времени.
Три сценария работы алгоритма
Сложность алгоритма может меняться в зависимости от того, в каком порядке расположены входные данные. Поэтому в анализе принято выделять три сценария:
Лучший случай (best case*): Идеальные условия. Например, вы ищете число в массиве, и оно оказывается самым первым элементом. Алгоритм завершает работу за одну операцию. На практике этот случай рассматривают редко, так как он не дает гарантий стабильности. Худший случай (worst case*): Самые неблагоприятные условия. Вы ищете число , а его вообще нет в массиве. Программе придется проверить каждый элемент до самого конца. Именно на худший случай ориентируются инженеры при проектировании надежных систем. Средний случай (average case*): Ожидаемое поведение алгоритма на типичных, случайных данных. Для поиска числа в массиве средний случай предполагает, что искомый элемент находится где-то в середине.
Понимание этих сценариев помогает избежать катастрофических сбоев. Если ваш сервис по продаже билетов отлично работает в среднем случае, но зависает на минут в худшем, пользователи уйдут к конкурентам при первом же столкновении с проблемой.
Итоги
* Вычислительная сложность позволяет оценивать алгоритмы математически, абстрагируясь от мощности конкретного компьютера и языка программирования. * Временная сложность измеряется в количестве базовых операций, а не в секундах. * Пространственная сложность показывает аппетиты алгоритма к оперативной памяти. При разработке часто приходится выбирать между скоростью работы и экономией памяти (time-memory trade-off*). * Оценка алгоритма почти всегда проводится по худшему случаю, чтобы гарантировать стабильную работу системы при любых входных данных.