1. Введение в теорию алгоритмов и понятие вычислительной сложности
Введение в теорию алгоритмов и понятие вычислительной сложности
Представьте, что вам нужно найти слово в толковом словаре, состоящем из 100 000 страниц. Если вы будете просматривать каждую страницу по порядку с самого начала, это займет огромное количество времени. Однако интуитивно вы открываете словарь примерно посередине, смотрите на букву и отбрасываете половину книги, продолжая поиск в нужной части. В первом случае вы использовали крайне неэффективный метод, а во втором — применили оптимальный алгоритм поиска.
Алгоритм — это конечная и точная последовательность действий, направленная на решение конкретной задачи. В программировании алгоритмы являются фундаментом: от того, насколько грамотно они спроектированы, зависит жизнеспособность любого программного продукта.
Почему важна эффективность алгоритмов?
Современные процессоры способны выполнять миллиарды операций в секунду. Кажется, что вычислительной мощности достаточно для решения любых задач мгновенно. Но на практике производительность оборудования не может компенсировать неэффективность кода.
> Компьютеры становятся быстрее, но плохой алгоритм на быстром компьютере всегда проиграет хорошему алгоритму на медленном компьютере при достаточно большом объеме данных. > > Дональд Кнут, автор монографии «Искусство программирования»
Рассмотрим пример. У нас есть база данных интернет-магазина, содержащая 1 000 000 товаров. Нам нужно отсортировать их по цене.
Если мы используем простой, но медленный алгоритм (например, сортировку пузырьком), количество необходимых операций будет пропорционально квадрату количества элементов. Для 1 000 000 товаров потребуется около 1 000 000 000 000 (одного триллиона) операций сравнения. Даже если компьютер выполняет 100 миллионов операций в секунду, процесс займет почти 3 часа.
Если же применить эффективный алгоритм (например, быструю сортировку), количество операций составит около 20 000 000. Тот же самый компьютер справится с задачей за 0,2 секунды. Разница между 3 часами и долей секунды — это и есть результат работы теории алгоритмов.
Время и память: два ресурса вычислительной сложности
При оценке алгоритмов нас интересуют два главных ресурса, которые потребляет программа:
Важно понимать, что временная сложность измеряется не в секундах или минутах. Время выполнения в секундах зависит от тактовой частоты процессора, архитектуры системы, фоновых процессов и языка программирования. Вместо этого в теории алгоритмов подсчитывают количество элементарных операций (сложение, присваивание, сравнение), которые должен выполнить алгоритм.
В приведенном выше коде количество операций напрямую зависит от размера списка numbers. Если в списке 10 элементов, цикл выполнится 10 раз. Если 10 000 элементов — 10 000 раз. Зависимость является линейной.
Асимптотический анализ и нотация «О» большое
Для стандартизации оценки сложности математики и программисты используют асимптотический анализ. Он изучает поведение функции при стремлении аргумента к бесконечности. Нас интересует не точное количество операций, а тенденция роста.
Для записи асимптотической сложности используется нотация Big O («О» большое).
Строгое математическое определение выглядит так:
Где: * — точное время работы алгоритма (количество операций). * — символ нотации, обозначающий верхнюю границу сложности. * — математическая функция, описывающая скорость роста. * — размер входных данных (например, количество элементов в массиве).
При использовании нотации Big O применяются два главных правила:
* Отбрасывание констант. Если алгоритм выполняет операций, мы записываем это как . Константный множитель не влияет на характер роста функции при огромных значениях . * Отбрасывание младших членов. Если точное количество операций выражается формулой , мы оставляем только слагаемое с наибольшей степенью роста. В данном случае это . Сложность составит .
Пример с числами: пусть . Тогда , а . Вклад слагаемого в общий результат составляет всего 0,05%, поэтому им можно пренебречь.
Основные классы сложности
Все алгоритмы можно разделить на классы в зависимости от функции . Чем медленнее растет функция, тем эффективнее алгоритм.
| Класс сложности | Название | Характеристика | Пример алгоритма | Операций при | |---|---|---|---|---| | | Константная | Время выполнения не зависит от объема данных. | Доступ к элементу массива по индексу. | | | | Логарифмическая | Время растет очень медленно. При увеличении данных в 2 раза требуется всего 1 дополнительный шаг. | Бинарный поиск в отсортированном массиве. | | | | Линейная | Время растет прямо пропорционально объему данных. | Поиск максимума в не отсортированном массиве. | | | | Линейно-логарифмическая | Эффективные алгоритмы для работы с большими наборами данных. | Быстрая сортировка, сортировка слиянием. | | | | Квадратичная | Время растет в квадрате. Плохо подходит для больших данных. | Сортировка пузырьком, вложенные циклы. | | | | Экспоненциальная | Время удваивается при добавлении каждого нового элемента. | Полный перебор вариантов (задача о рюкзаке). | (невыполнимо) |
Алгоритмы с экспоненциальной сложностью практически неприменимы для больших данных. Если для 10 элементов алгоритм выполнит 1024 операции, то для 50 элементов количество операций превысит миллион миллиардов. На современном компьютере это займет годы вычислений.
Худший, средний и лучший случаи
Один и тот же алгоритм может работать с разной скоростью в зависимости от того, какие именно данные поступили на вход. Поэтому в теории алгоритмов принято выделять три сценария:
В подавляющем большинстве случаев программисты и математики ориентируются на худший случай. Это дает гарантию, что при любых обстоятельствах программа не будет работать медленнее заявленного времени. Оценка по худшему случаю позволяет планировать архитектуру высоконагруженных систем, где задержки недопустимы.
Итоги
* Вычислительная сложность позволяет оценивать эффективность алгоритмов независимо от мощности железа и языка программирования. * Два главных ресурса, которые мы экономим — это время (количество операций) и память (объем дополнительных данных). Нотация «О» большое (Big O*) описывает асимптотическое поведение алгоритма, отбрасывая константы и младшие члены. * Алгоритмы делятся на классы: от сверхбыстрых константных до практически невыполнимых экспоненциальных . * При проектировании систем всегда следует ориентироваться на оценку сложности в худшем случае.