Сложность алгоритмов и операций на примере Python |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Нейронные сети начинающим Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2020-11-05 12:01 На примере языка Python и его структур данных мы разберемся с классами сложности различных операций и научимся комбинировать их, чтобы вычислить сложность целой функции. Такой тип анализа называется статическим, так как не требует запуска программы, – в противовес динамическому, или эмпирическому, анализу, при котором проводятся измерения параметров работающего кода. Классы сложности операций в Python Самое базовое понятие статического анализа – O(1). Этот класс сложности имеют операции, которые выполняются за константное время, например, создание переменной или сложение небольших чисел. Время выполнения большинства операций зависит от количества элементов, с которыми приходится работать, например, от размера списка. Например, класс сложности O(N) описывает линейную зависимость. Если размер списка увеличится в два раза, то операция также будет выполняться в два раза дольше. Параметр Рассмотрим основные операции некоторых структур данных в Python. Списки (lists)
Кортежи (tuples) поддерживают все операции, которые не изменяют структуру данных – и они имеют такие же классы сложности, как у списков. Множества (sets)
Ряд операций со множествами имеет сложность O(1), в отличие от аналогичных операций со списками и кортежами. Более быстрая реализация обусловлена тем, что множествам не требуется хранить информацию о порядке элементов. Неизменяемые множества (frozen sets) поддерживают все операции, которые не изменяют структуру данных – и они имеют такие же классы сложности, как у обычных множеств. Словари (dict и defaultdict)
Как видно, большинство операций со словарями имеют сложность O(1). Тип defaultdict поддерживает все эти операции с теми же классами сложности. Таким образом, вызов конструктора в том случае, если значения не найдены в defaultdict, имеет сложность O(1). Тонкости анализа Обратите внимание, что операция Если При этом Точно так же *** При сравнении двух списков на равенство, класс сложности должен быть O(N), как указано в таблице выше. Однако в реальности это значение нужно умножить на O==(...), где O==(...) это класс сложности для операции сравнения ( Эта проблема возникает при любом сравнении, однако мы в дальнейших расчетах будем предполагать, что эта операция имеет сложность O(1) – например, для чисел и строк малой/фиксированной длины. Составные классы сложности Разобравшись со сложностью отдельных операций мы переходим к их комбинированию. Закон сложения для O-нотации При сложении двух классов сложности складываются функции этих классов. В конечном счете, Таким образом, так как Это правило позволяет вычислить класс сложности для последовательности операций. Например, сначала мы выполняем выражение со сложностью O(f(n)), а следом за ним – O(g(n)). Тогда выполнение обоих выражений (одно за другим) будет иметь сложность O(f(n)) + O(g(n)), то есть O(f(n) + g(n)). Пример Вызов функции Сложность этого действия будет равна: Если мы вызовем функцию то получим: Константа Условия Отдельно разберем исполнение условий (if). Сначала выполняется само условие, а затем один из блоков (if или else). Предположим, что вычисление условия Тогда сложность всего кода будет равна:
Подставим реальные значения:
и вычислим сложность кода: Если бы операция Фактически, общий класс сложности для if-условий можно записать еще проще: Для первого примера в этом случае получим: В O-нотации мы всегда отбрасываем менее значимые элементы – по сути это аналогично работе функции Закон умножения для O-нотации Если мы повторяем O(N) раз некоторый процесс с классом сложности O(f(N)), то результирующий класс сложности будет равен: Предположим, некоторая функция Сложность этого кода будет равна: Это правило позволяет вычислять класс сложности для выполнения некоторого повторяющегося несколько раз выражения. Необходимо умножить класс сложности количества повторений на класс сложности самого выражения. Статический анализ на практике Возьмем три разные функции, которые решают одну и ту же задачу – определяют, состоит ли список из уникальных значений (не имеет дубликатов). Для каждой функции вычислим класс сложности. Для всех трех примеров размер списка обозначим как Алгоритм 1 Список уникален, если каждое его значение не встречается ни в одном последующем индексе. Для значения Определим сложность для каждой строки метода:
Таким образом, класс сложности целой функции Если размер списка увеличится вдвое, то выполнение функции займет в 4 раза больше времени. *** Возможно, вы хотели написать так: ведь выражение Кроме того, в худшем случае вычисляемый срез списка – это *** Вместо срезов можно использовать вложенный цикл. Сложность функции при этом не изменится. Класс сложности целой функции тот же: Алгоритм 2 Список уникален, если после его сортировки рядом не находятся одинаковые значения. Сначала мы копируем список, чтобы не изменять исходную структуру сортировкой – функции обычно не должны влиять на входящие параметры. Сложность по строчкам:
Общий класс сложности функции Сложность этой реализации меньше, чем Наибольшую сложность имеет операция сортировки – она занимает больше всего времени. При удвоении размера списка именно сортировка займет больше половины добавившегося времени выполнения. Класс сложности O(N log N) хуже, чем O(N), но уже значительно лучше, чем O(N2). Фактически, в код метода можно внести одно упрощение: Функция Это изменение ускорит выполнение кода, но не повлияет на его сложность, так как O(N + N log N) = O(N log N). Ускорение – это всегда хорошо, но намного важнее найти алгоритм с минимальной сложностью. Нужно отметить, что Алгоритм 3 Список уникален, если при превращении в множество (set) его размер не изменяется. Рассчитать класс сложности для всей функции очень просто: Таким образом, третья реализация оказалась самой эффективной из всех с линейным временем выполнения. При увеличении размера списка в два раза, время выполнения функции Тело функции можно записать в одну строчку: Сложность при этом не изменится. В отличие от *** Одну проблему можно решить разными способами, из которых одни могут быть эффективнее других. Статический анализ (без запуска кода) позволяет оценить сложность выполнения алгоритмов и функций. Это имеет большое значение для работы с большими наборами данных – чем больше размер входных данных, тем больше выигрыш. В то же время для небольших объемов данных классы сложности неэффективны. Чтобы найти лучший алгоритм в этом случае, необходимо учитывать константы и термины низшего порядка. Также хорошо работает эмпирический, или динамический, анализ. Кроме того, при анализе важно учитывать дополнительные ограничения реализации (например, типы данных). Приоритетные очереди Рассмотрим еще один важный пример зависимости вычислительной сложности от реализации алгоритма – приоритетные очереди. Этот тип данных поддерживает две операции:
Разные реализации приоритетных очередей имеют разные классы сложности этих операций:
Реализация 1 Новое значение добавляется в конец списка (list) или в начала связного списка (linked list) – O(1). Чтобы найти приоритетное значение для удаления осуществляется поиск по списку – O(N). Легко добавить, но трудно удалить. Реализация 2 Для нового значения определяется правильное место – для этого перебираются элементы списка (или связного списка) – O(N). Зато самое приоритетное значение всегда находится в конце списка (или в начале связного списка) – O(1). Легко удалить, но трудно добавить. Реализация 3 Используется двоичная куча, которая позволяет реализовать обе операции со "средней" сложностью O(log N), что для больших значений ближе к O(1), чем к O(N). Теперь проведем анализ этих реализаций на реальных задачах. Сортировка Чтобы отсортировать N значений с помощью приоритетной очереди, нужно сначала добавить все значения в очередь, а затем удалить их все. Реализация 1 Реализация 2 Реализация 3 Примечание: N * O(...) – это то же самое, что и O(N) * O(...). Первая и вторая реализации выполняют одну операцию быстро, а другую медленно. В итоге общий класс сложности определяет самая медленная операция. Третья реализация оказывается быстрее всех, так как ее среднее время O(N log N) ближе к O(1), чем к O(N). 10 максимальных значений Теперь найдем с помощью приоритетной очереди 10 максимальных значений. Для этого в очередь помещаются Реализация 1 Реализация 2 Реализация 3 Теперь первая реализация оказывается самой эффективной, так как операция добавления элемента у нее очень дешевая по времени. *** Мораль этого примера заключается в том, что иногда не существует "самой лучшей реализации". В зависимости от задачи оптимальными могут быть разные варианты. А чтобы найти лучшее решение, важно понимать, что такое вычислительная сложность и как ее определить :) Источники Источник: proglib.io Комментарии: |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||