В информатике амортизированный анализ — это метод анализа сложности заданного алгоритма или того , сколько ресурсов, особенно времени или памяти, требуется для его выполнения . Мотивация амортизированного анализа заключается в том, что рассмотрение наихудшего времени выполнения может быть слишком пессимистичным. Вместо этого амортизированный анализ усредняет время выполнения операций в последовательности по этой последовательности. [1] : 306 В качестве заключения: «Амортизированный анализ — это полезный инструмент, который дополняет другие методы, такие как анализ наихудшего и среднего случая ». [2] : 14
Для заданной операции алгоритма определенные ситуации (например, параметризация входных данных или содержимое структуры данных) могут подразумевать значительные затраты ресурсов, тогда как другие ситуации могут быть не такими затратными. Амортизированный анализ рассматривает как затратные, так и менее затратные операции вместе на протяжении всей последовательности операций. Это может включать учет различных типов входных данных, длины входных данных и других факторов, которые влияют на его производительность. [2]
Амортизированный анализ изначально возник из метода, называемого агрегатным анализом, который теперь относится к амортизированному анализу. Впервые этот метод был официально представлен Робертом Тарьяном в его статье 1985 года «Амортизированная вычислительная сложность » [1] , в которой рассматривалась необходимость в более полезной форме анализа, чем обычные вероятностные методы. Амортизация изначально использовалась для очень специфических типов алгоритмов, особенно тех, которые включают двоичные деревья и операции объединения . Однако теперь она повсеместна и вступает в игру при анализе многих других алгоритмов. [2]
Амортизированный анализ требует знания того, какие серии операций возможны. Чаще всего это касается структур данных , которые имеют состояние , сохраняющееся между операциями. Основная идея заключается в том, что наихудшая операция может изменить состояние таким образом, что наихудший случай не сможет произойти снова в течение длительного времени, таким образом «амортизируя» его стоимость.
Обычно существует три метода проведения амортизированного анализа: метод совокупного анализа, метод бухгалтерского учета и метод потенциала . Все они дают правильные ответы; выбор того, какой из них использовать, зависит от того, какой из них наиболее удобен для конкретной ситуации. [3]
Рассмотрим динамический массив , который увеличивается в размере по мере добавления в него новых элементов, например, ArrayList
в Java или std::vector
C++. Если бы мы начали с динамического массива размером 4, мы могли бы вставить в него 4 элемента, и каждая операция заняла бы постоянное время . Однако вставка пятого элемента в этот массив заняла бы больше времени, поскольку массиву пришлось бы создать новый массив вдвое большего текущего размера (8), скопировать старые элементы в новый массив, а затем добавить новый элемент. Следующие три операции вставки также заняли бы постоянное время, а затем последующее добавление потребовало бы еще одного медленного удвоения размера массива.
В общем случае для произвольного числа передач в массив любого начального размера время для шагов, которые удваивают массив, добавляется в геометрической прогрессии к , в то время как постоянное время для каждого оставшегося нажатия также добавляется к . Таким образом, среднее время на операцию нажатия составляет . Это рассуждение можно формализовать и обобщить для более сложных структур данных с использованием амортизированного анализа. [3]
Показана реализация очереди на Python3 , структуры данных FIFO :
class Queue : # Инициализирует очередь двумя пустыми списками def __init__ ( self ): self . input = [] # Сохраняет элементы, которые помещены в очередь self . output = [] # Сохраняет элементы, которые удалены из очереди def enqueue ( self , element ): self . input . append ( element ) # Добавить элемент в список ввода def dequeue ( self ): if not self.output : # Если выходной список пуст # Перенести все элементы из входного списка в выходной список , изменив порядок на обратный while self.input : # Пока входной список не пуст self.output.append ( self.input.pop ( )) # Извлечь последний элемент из входного списка и добавить его в выходной список return self.output.pop () # Извлечь и вернуть последний элемент из выходного списка
Операция постановки в очередь просто помещает элемент во входной массив; эта операция не зависит от длины входных или выходных данных и, следовательно, выполняется за постоянное время.
Однако операция dequeue более сложна. Если в выходном массиве уже есть некоторые элементы, то dequeue выполняется за постоянное время; в противном случае dequeue занимает время, чтобы добавить все элементы в выходной массив из входного массива, где n — текущая длина входного массива. После копирования n элементов из входного массива мы можем выполнить n операций dequeue, каждая из которых занимает постоянное время, прежде чем выходной массив снова станет пустым. Таким образом, мы можем выполнить последовательность из n операций dequeue всего за время, что означает, что амортизированное время каждой операции dequeue равно . [4]
В качестве альтернативы мы можем взимать плату за копирование любого элемента из входного массива в выходной массив с более ранней операции постановки в очередь для этого элемента. Эта схема взимания платы удваивает амортизированное время для постановки в очередь, но уменьшает амортизированное время для снятия с очереди до .