Существует множество вопросов о том, как анализировать время выполнения алгоритмов (см., Например, анализ времени выполнения и анализ алгоритма ). Многие из них похожи, например, те, которые запрашивают анализ затрат на вложенные циклы или алгоритмы «разделяй и властвуй», но большинство ответов, похоже, сделаны специально.
С другой стороны, ответы на другой общий вопрос объясняют общую картину (в частности, касающуюся асимптотического анализа) некоторыми примерами, но не о том, как испачкать руки.
Существует ли структурированный общий метод анализа стоимости алгоритмов? Стоимость может быть временем выполнения (сложность времени) или какой-либо другой мерой стоимости, такой как количество выполненных сравнений, сложность пространства или что-то еще.
Предполагается, что это станет справочным вопросом, на который можно указать новичкам; следовательно, его более широкий, чем обычно, объем. Пожалуйста, позаботьтесь о том, чтобы дать общие, дидактически представленные ответы, которые иллюстрируются хотя бы одним примером, но тем не менее охватывают многие ситуации. Спасибо!
Ответы:
Перевод кода в математику
Учитывая (более или менее) формальную операционную семантику, вы можете буквально перевести (псевдо) код алгоритма в математическое выражение, которое дает вам результат, при условии, что вы можете манипулировать выражением в полезной форме. Это хорошо работает для показателей аддитивной стоимости, таких как число сравнений, перестановок, операторов, обращений к памяти, циклов, в которых нуждается некоторая абстрактная машина, и так далее.
Пример: сравнения в Bubblesort
Рассмотрим этот алгоритм, который сортирует данный массив
A
:Допустим, мы хотим выполнить обычный анализ алгоритма сортировки, то есть посчитать количество сравнений элементов (строка 5). Сразу отметим, что эта величина не зависит от содержимого массиваn
A
, только от его длины . Таким образом, мы можем буквально перевести (вложенные) циклы в (вложенные) суммы; переменная цикла становится переменной суммирования, и диапазон переносится. Мы получили:for
где - стоимость каждого исполнения строки 5 (которую мы считаем).1
Пример: свопы в Bubblesort
Я обозначу через подпрограмма , которая состоит из линий до и по расходы на выполнение этой подпрограммы (один раз). C i , jPi,j Ci,j
i
j
Теперь предположим, что мы хотим считать свопы , то есть как часто выполняется . Это «базовый блок», то есть подпрограмма, которая всегда выполняется атомарно и имеет некоторую постоянную стоимость (здесь, ). Заключение таких блоков является одним из полезных упрощений, которое мы часто применяем, не задумываясь и не говоря об этом. 1P6,8 1
С переводом, аналогичным приведенному выше, мы приходим к следующей формуле:
( i , j ) P 5 , 9A(i,j) обозначает состояние массива перед -й итерацией .(i,j) P5,9
Обратите внимание, что я использую вместо качестве параметра; мы скоро увидим почему. Я не добавляю и качестве параметров поскольку затраты здесь не зависят от них (то есть в модели однородных затрат ); в общем, они просто могут.A n i j C5,9
Очевидно, что затраты на зависят от содержания (значения и , в частности), поэтому мы должны это учитывать. Теперь перед нами стоит задача: как нам «развернуть» ? Ну, мы можем сделать зависимость от содержимого явной:P5,9 A C5,9 A
A[j]
A[j+1]
Для любого входного массива эти затраты четко определены, но мы хотим более общее утверждение; нам нужно сделать более сильные предположения. Давайте рассмотрим три типичных случая.
Худший случай
Просто взглянув на сумму и заметив, что , мы можем найти тривиальную верхнюю оценку стоимости:C5,9(A(i,j))∈{0,1}
Но может ли это произойти , то есть есть ли для этой верхней границы? Как оказалось, да: если мы вводим отсортированный в обратном порядке массив попарно различных элементов, каждая итерация должна выполнять свопинг. Таким образом, мы вывели точное число наихудших вариантов свопов Bubblesort.A
Лучший случай
И наоборот, существует тривиальная нижняя граница:
Это также может произойти: в массиве, который уже отсортирован, Bubblesort не выполняет ни одного обмена.
Средний случай
В худшем и лучшем случае откроется довольно большой разрыв. Но каково типичное количество свопов? Чтобы ответить на этот вопрос, нам нужно определить, что означает «типичный». Теоретически, у нас нет причин предпочитать один вход другому, поэтому мы обычно предполагаем равномерное распределение по всем возможным входам, то есть каждый вход одинаково вероятен. Мы ограничимся массивами с попарно различными элементами и, таким образом, примем модель случайной перестановки .
Затем мы можем переписать наши расходы следующим образом²:
Теперь нам нужно выйти за рамки простых манипуляций с суммами. Рассматривая алгоритм, мы отмечаем, что каждый своп удаляет ровно одну инверсию в (мы всегда обмениваем только соседей3). То есть, число свопов , выполненных на именно число инверсий из . Таким образом, мы можем заменить внутренние две суммы и получитьA A inv(A) A
К счастью для нас, среднее число инверсий было определено как
что является нашим конечным результатом. Обратите внимание, что это ровно половина стоимости наихудшего случая.
i = n-1
внешнего цикла, который ничего не делает, не выполняется.Общий метод
Мы видели в примере, что мы должны перевести управляющую структуру в математику; Я представлю типичный ансамбль правил перевода. Мы также видели, что стоимость любой данной подпрограммы может зависеть от текущего состояния , то есть (приблизительно) текущих значений переменных. Поскольку алгоритм (обычно) изменяет состояние, общий метод немного громоздок для записи. Если вы начинаете чувствовать смущение, я предлагаю вам вернуться к примеру или придумать свой.
Мы обозначаем текущее состояние (представьте его как набор переменных). Когда мы выполняем программу, начинающуюся в состоянии , мы в состоянии (при условии, что завершается).ψ ψ ψ/P
P
P
Индивидуальные заявления
Учитывая только одно утверждениеCS(ψ)
S;
, вы назначаете его стоит . Обычно это будет постоянная функция.Выражения
Если у вас есть выражение
E
формыE1 ∘ E2
(скажем, арифметическое выражение, где∘
может быть сложение или умножение, вы рекурсивно складываете расходы:Обратите внимание, что
так что вам, возможно, придется проявить гибкость с этим правилом.
Последовательность
Учитывая программу
P
как последовательность программQ;R
, вы добавляете затраты кConditionals
Учитывая программу
P
видаif A then Q else R end
, расходы зависят от состояния:В целом, оценка
A
может очень хорошо изменить состояние, отсюда и обновление стоимости отдельных филиалов.Для петель
Учитывая программу
P
формыfor x = [x1, ..., xk] do Q end
, назначьте расходыгде - это состояние до обработки значения , т. е. после итерации с установленным значением , ..., .ψi
Q
xi
x
x1
xi-1
Обратите внимание на дополнительные константы для обслуживания цикла; переменная цикла должна быть создана ( ) и присвоена ее значениям ( ). Это актуально сcinit_for cstep_for
xi
может быть дорогостоящим иfor
цикл с пустым телом (например, после упрощения в лучшем случае с определенной стоимостью) не имеет нулевой стоимости, если он выполняет итерации.В то время как-Loops
Учитывая программу
P
формыwhile A do Q end
, назначьте расходыПри проверке алгоритма это повторение часто можно представить в виде суммы, аналогичной сумме для циклов for.
Пример: рассмотрим этот короткий алгоритм:
Применяя правило, мы получаем
с некоторыми постоянными затратами для отдельных утверждений. Мы неявно предполагаем, что они не зависят от состояния (значения и ); это может или не может быть правдой в «реальности»: подумайте о переполнении!c…
i
x
Теперь мы должны решить эту повторяемость для . Отметим, что ни количество итераций, ни стоимость тела цикла не зависят от значения , поэтому мы можем его отбросить. Мы остались с этим повторением:C1,4
i
Это решает с элементарными средствами для
повторное введение полного состояния символически; если , то .ψ={…,x:=5,…} ψ(x)=5
Процедурные вызовы
Учитывая программу
P
видаM(x)
для некоторого параметра (ов),x
гдеM
есть процедура с (именованным) параметромp
, назначьте затратыЕще раз обратите внимание на дополнительную константу (которая на самом деле может зависеть от !). Вызовы процедур дороги из-за того, как они реализованы на реальных машинах, и иногда даже доминируют во время выполнения (например, наивно оценивая повторяемость числа Фибоначчи).ccall ψ
Я затушевываю некоторые семантические проблемы, которые у вас могут возникнуть с состоянием здесь. Вы захотите различать глобальное состояние и такие локальные для вызовов процедур. Давайте просто предположим, что мы передаем только глобальное состояние и
M
получаем новое локальное состояние, инициализируемое установкой значенияp
tox
. Кроме того, этоx
может быть выражение, которое мы (обычно) предполагаем оценить перед его передачей.Пример: рассмотрим процедуру
Согласно правилу (ам), мы получаем:
Обратите внимание, что мы игнорируем глобальное состояние, поскольку
fac
явно не имеет доступа к любому. Это конкретное повторение легко решитьМы рассмотрели языковые возможности, с которыми вы столкнетесь, в типичном псевдокоде. Остерегайтесь скрытых затрат при анализе псевдокода высокого уровня; если сомневаешься, развернись. Запись может показаться громоздкой и, безусловно, является вопросом вкуса; перечисленные понятия нельзя игнорировать. Однако, имея некоторый опыт, вы сможете сразу увидеть, какие части штата имеют значение для какого показателя стоимости, например, «размер проблемы» или «количество вершин». Остальное можно отбросить - это значительно упрощает вещи!
Если вы сейчас думаете , что это слишком сложно, посоветуйте: она есть ! Получение точных затрат на алгоритмы в любой модели, которая настолько близка к реальным машинам, что позволяет делать прогнозы времени выполнения (даже относительные), является трудной задачей. И это даже не учитывая кеширование и другие неприятные эффекты на реальных машинах.
Следовательно, алгоритм анализа часто упрощается до такой степени, что его можно математически отследить. Например, если вам не нужны точные затраты, вы можете преувеличить или недооценить в любой точке (для верхних или нижних границ): уменьшить набор констант, избавиться от условных выражений, упростить суммы и т. Д.
Примечание об асимптотической стоимости
То, что вы обычно найдете в литературе и на веб-сайтах, это «анализ Big-Oh». Подходящим термином является асимптотический анализ, который означает, что вместо получения точных затрат, как мы делали в примерах, вы даете затраты только до постоянного фактора и в пределе (грубо говоря, «для больших »).n
Это (часто) справедливо, поскольку абстрактные операторы в действительности имеют некоторые (как правило, неизвестные) затраты, в зависимости от машины, операционной системы и других факторов, а в коротких периодах выполнения может преобладать операционная система, которая в первую очередь настраивает процесс, и еще много чего. Так или иначе, вы получаете некоторое возмущение.
Вот как асимптотический анализ относится к этому подходу.
Определите доминирующие операции (которые вызывают затраты), то есть операции, которые происходят чаще всего (с учетом постоянных факторов). В примере с Bubblesort одним из возможных вариантов является сравнение в строке 5.
В качестве альтернативы, ограничьте все константы для элементарных операций их максимумом (сверху) соответственно. их минимум (снизу) и выполнить обычный анализ.
Убедитесь, что вы понимаете значение символов Ландау . Помните, что такие границы существуют для всех трех случаев ; использование не подразумевает анализ наихудшего случая.O
дальнейшее чтение
Есть много других проблем и приемов в анализе алгоритмов. Вот некоторые рекомендуемые чтения.
Как описать алгоритмы, доказать и проанализировать их?
Как мы можем предположить, что основные операции над числами занимают постоянное время?
Что составляет одну единицу времени в анализе времени выполнения?
Есть много вопросов, помеченных алгоритмом анализа вокруг, которые используют методы, подобные этому.
источник
Количество выполненных заявлений
Есть еще один метод, отстаиваемый Дональдом Кнутом в его серии «Искусство программирования ». В отличие от перевода всего алгоритма в одну формулу , он работает независимо от семантики кода со стороны «объединения вещей» и позволяет переходить на более низкий уровень только при необходимости, начиная с точки зрения «орлиного глаза». Каждое утверждение может быть проанализировано независимо от остальных, что приводит к более четким вычислениям. Тем не менее, этот метод хорошо подходит для довольно подробного кода, а не для псевдокода высокого уровня.
Метод
Это довольно просто в принципе:
Подсчитать общую стоимость
Вы можете вставить оценки и / или символические величины в любой точке, ослабляя соотв. обобщая результат соответственно.
Помните, что шаг 3 может быть сколь угодно сложным. Обычно там, чтобы получить результаты, вам нужно работать с (асимптотическими) оценками, такими как « ».e77∈O(nlogn)
Пример: поиск в глубину
Рассмотрим следующий алгоритм обхода графа:
Мы предполагаем, что (неориентированный) граф задается списками смежности на узлах . Пусть будет количеством ребер.{0,…,n−1} m
Просто взглянув на алгоритм, мы увидим, что некоторые операторы выполняются так же часто, как и другие. Мы вводим некоторые заполнители , и для счетчиков выполнения :A B C ei
В частности, поскольку каждый рекурсивный вызов в строке 8 вызывает вызов в строке 3 (а один вызывается исходным вызовом из ). Кроме того, потому что условие должно проверяться один раз за итерацию, а затем еще раз, чтобы выйти из него.e8=e3−1 e6=e5+e7
foo
dfs
while
Понятно, что . Теперь во время доказательства корректности мы покажем, что он выполняется ровно один раз для каждого узла; то есть . Но затем мы перебираем каждый список смежности ровно один раз, и каждое ребро подразумевает всего две записи (по одной для каждого инцидентного узла); мы получаем итераций в общей сложности. Используя это, мы выводим следующую таблицу:A=1 B=n C=2m
foo
Это приводит нас к общей стоимости точно
Создавая подходящие значения для мы можем получить более конкретные затраты. Например, если мы хотим посчитать доступ к памяти (за слово), мы бы использовалиCi
и получить
дальнейшее чтение
Смотрите внизу моего другого ответа .
источник
Анализ алгоритмов, как и доказательство теорем, в значительной степени является искусством (например, существуют простые программы (например, проблема Коллатца ), которые мы не знаем, как анализировать). Мы можем преобразовать проблему сложности алгоритма в математическую проблему, на которую всесторонне ответил Рафаэль , но затем, чтобы выразить ограничение стоимости алгоритма в терминах известных функций, нам осталось:
источник