Структуры данных и модели вычислений

Вводная

Разбить на страницы
Показывать лекцию целиком

При разработке алгоритма для компьютерного решения той или иной задачи необходимая информация формализуется в виде набора элементов различных типов. В каждой системе программирования предусмотрено использование некоторых примитивных типов данных и средств, с помощью которых можно группировать их в более сложные структуры. Будем считать примитивными следующие типы: булевский, целый, вещественный и символьный. Многие системы программирования позволяют группировать данные примитивных типов в массивы однотипных элементов, записи из фиксированного числа элементов разных типов и в некоторые другие структуры. Вообще, под типом данных понимают произвольное множество, называемое множеством значений, и набор операций над значениями. Так, значениями целого типа являются целые числа из некоторого диапазона, зависящего от системы программирования, а операциями — обычные арифметические операции.

При алгоритмизации задач, решение которых опирается на использование математических знаний и требует математических доказательств, разработка алгоритма часто проводится также в математических или формализованных прикладных терминах. При этом в достаточно большой степени происходит отвлечение от технических возможностей исполнителя алгоритмов (человека или технического устройства). Так, если при описании алгоритма используется понятие множества, то достаточно уметь выполнять некоторый набор операций с множествами и отвечать на некоторые вопросы относительно множеств. Перечень таких операций может быть следующим:

  • ввести в рассмотрение пустое множество;
  • включить элемент в множество;
  • исключить элемент из множества;
  • проверить, пусто ли рассматриваемое множество;
  • узнать, сколько элементов в рассматриваемом множестве.
  • Эти операции можно считать элементарными и до известной поры не думать о способе их реализации исполнителем. В таких случаях говорят, что мы имеем дело с абстрактным типом данных.

    Широко используемыми абстрактными типами данных наряду с множествами являются мультимножества, взвешенные множества, кортежи, отображения, приоритетные очереди, графы и так далее. Когда дело доходит до разработки программ в конкретной системе программирования, приходится принимать решения о представлении таких данных в памяти и о реализации операций с ними доступными средствами.

    В настоящее время большинство алгоритмов проектируется для использования в устройствах, обладающих адресуемой памятью. Каждый элемент информации, размещенный в такой памяти, занимает определенную позицию. По известной позиции элемента в такой памяти доступ к нему осуществляется за некоторую условную единицу времени, зависящую только от типа получаемой информации, фактически — от физического размера ячейки памяти или от количества таких ячеек, предназначенных для ее хранения, но не от ее конкретного содержания. Более того, позиции элементов сами могут быть элементами информации, с которыми могут производиться некоторые операции, что позволяет использовать так называемую косвенную адресацию. Наличие косвенной адресации позволяет поручить программной системе или разрабатываемой прикладной программе поиск свободных участков памяти для размещения новых элементов информации и запоминание их адресов с последующим использованием для доступа к информации.

    Информация, размещенная в адресуемой памяти, приобретает новые свойства. Элемент информации характеризуется не только своим содержанием, но и адресом, то есть местом расположения в памяти. Два элемента данных, соседних в содержательном смысле, не обязательно будут располагаться в соседних ячейках памяти. Они могут оказаться в "непредвиденных" местах. Проектируя программную реализацию алгоритма, необходимо проектировать и способ расположения в памяти обрабатываемой информации.

    Существуют типы данных, которые естественным образом вкладываются в адресуемую структуру технической памяти, причем легко выполняются все операции, предусмотренные для такого типа данных. Примером может служить вектор фиксированной размерности, задаваемый упорядоченным набором своих компонент. Наиболее естественным является его хранение в виде массива, при котором соседние компоненты располагаются в ячейках с идущими подряд номерами. Этот способ позволяет легко выполнять покомпонентные операции (сложение векторов, вычисление скалярного произведения и другие). Однако если размерность вектора в процессе работы изменяется, например путем удаления компонент или вставки новых, то представление в виде массива оказывается неудобным, так как операции удаления и вставки при условии сохранения порядка следования элементов требуют перезаписи, возможно, достаточно большого числа компонент, что может неблагоприятно сказаться на эффективности алгоритма. Чтобы избежать такой ситуации, одновременно с алгоритмом проектируют структуру представления данных, позволяющую реализовать выполнение всех необходимых операций в приемлемое время.

    В практике программирования накоплен большой опыт структурирования информации. К счастью, способы структурирования, изобретенные при решении одной задачи, часто находят применение и во многих других. Например, для представления кортежей и множеств в памяти компьютера могут использоваться такие структуры данных, как линейные и циклические списки.

    Во многих задачах исходные данные представляют собой так называемые взвешенные множества. Взвешенным называется множество, каждому элементу которого поставлено в соответствие в качестве веса некоторое число. Часто используемыми операциями с такими множествами являются поиск элемента с минимальным весом, вставка нового элемента со своим весом, удаление элемента и некоторые другие. Для быстрого выполнения таких операций разработаны так называемые кучеобразные структуры данных.

    Классы функций, используемые для оценки сложности алгоритмов

    Все функции, используемые ниже для оценки сложности алгоритмов, считаются асимптотически неотрицательными функциями натурального аргумента, то есть неотрицательными, начиная с некоторого значения аргумента $$n$$.

    Для асимптотических оценок сверху используется класс функций$${ O (g(n))=\{f(n):\exists c>0, \exists n_{0} \forall n>n_{0} [0\le f(n)\le c\cdot g(n)]\}. }$$

    Для асимптотических оценок снизу используется класс функций$${ \Omega (g(n))=\{ f(n):\exists c>0,\exists n_{0} \forall n>n_{0} [0\le c\cdot g(n)\le f(n)]\}. }$$

    Для асимптотически точных оценок используется класс функций$${ \Theta (g(n))=\{ f(n):\exists c_{1},c_{2} >0,\exists n_{0} \forall n>n_{0} [0\le c_{1} g(n)\le f(n)\le c_{2} g(n)]\}. }$$

    Очевидно, что справедливы следующие соотношения$${ \begin{gathered} \Theta (g(n))= O (g(n))\cap \Omega (g(n)),\\ f(n)\in O (g(n))\Leftrightarrow g(n)\in \Omega (f(n)). \end{gathered} }$$

    Далее, предполагается знание стандартных функций, используемых при оценках сложности, таких как полиномы, экспоненты, суперэкспоненты, логарифмы, суперлогарифмы, факториалы, числа Фибоначчи. Предполагается умение сравнивать их по скоростям роста.

    Амортизационный анализ

    Наряду с получением верхних и нижних оценок и оценок в среднем, часто используются так называемые амортизационные оценки.

    Амортизационный анализ применяется при оценке времени выполнения корректной последовательности, состоящей из $$n$$ однотипных или разнотипных операций с некоторой структурой данных. Если верхнюю оценку времени выполнения одной операции умножить на $$n$$, получим верхнюю оценку выполнения всех $$n$$ операций. Часто такая оценка бывает сильно завышенной. Иногда длительное время выполнения очередной операции влечет за собой малое время выполнения следующих операций. Более того, такая ситуация может создаваться искусственно, то есть при выполнении очередной операции мы можем готовить почву для более эффективного выполнения следующей. Поэтому возникает задача изучения асимптотического поведения гарантированной оценки для среднего времени выполнения одной операции.

    При амортизационном анализе определяется некоторая так называемая учетная (амортизационная) стоимость одной операции, которая может быть как больше, так и меньше реальной стоимости конкретной операции. Но при этом для любой корректной последовательности операций фактическая суммарная длительность всех операций не должна превосходить суммы их учетных стоимостей. Зная учетную стоимость одной операции, верхнюю оценку времени выполнения последовательности из $$n$$ операций можно получить, умножив ее на $$n$$.

    Ниже рассмотрим три часто используемых метода амортизационного анализа: метод группировки, метод предоплаты и метод потенциалов.

    Метод группировки. Предположим, что мы оценили сверху время выполнения последовательности из $$n$$ операций, установив, что она не превосходит $$T(n)$$, тогда величину $$T(n)/n$$ объявим учетной стоимостью любой операции из рассматриваемой последовательности, независимо от ее длительности.

    Метод предоплаты. В этом методе операции разных типов получают разные учетные стоимости, причем эти стоимости могут быть как больше, так и меньше фактических. Если учетная стоимость превосходит фактическую, то разность между ними рассматривается как резерв на оплату в будущем тех операций, у которых учетная стоимость ниже реальной. Учетные стоимости должны выбираться так, чтобы в любой момент времени фактическая стоимость не превосходила суммы учетных стоимостей, то есть чтобы резерв оставался неотрицательным.

    Метод потенциалов. Этот метод является обобщением метода предоплаты. Здесь резерв определяется функцией состояния структуры данных в целом. Эта функция называется потенциалом.

    Общая схема метода такова. Пусть над структурой данных предстоит произвести $$n$$ операций, и пусть $$D_i$$ — состояние структуры данных после $$i$$ -й операции ( $$D_0$$ — исходное состояние). Потенциал представляет собой функцию $$\phi$$ из множества возможных состояний структуры данных в множество действительных чисел.

    Пусть $$c_i$$ — реальная стоимость $$i$$ -й операции. Учетной стоимостью $$i$$ -й операции объявим число $$C_i$$, определяемое формулой$$\eq*{ C_{i} =c_{i} +\phi (D_{i})-\phi (D_{i-1}) }$$ как сумма реальной стоимости операции плюс приращение потенциала в результате выполнения этой операции. Тогда суммарная учетная стоимость всех операций равна$$\eq*{ \suml_{i=1}^{n}C_{i} =\suml_{i=1}^{n}c_{i} +\phi (D_{n} )-\phi (D_{0}). }$$

    Если нам удалось придумать функцию $$\phi$$, для которой$$\eq*{ \phi (D_{n} )\ge \phi (D_{0}), }$$ то суммарная учетная стоимость даст верхнюю оценку для реальной стоимости последовательности из $$n$$ операций. Не ограничивая общности, можно считать, что$$\eq*{ \phi (D_{0} )=0. }$$ Говоря неформально, если разность потенциалов$$\eq*{ \phi (D_{i} )-\phi (D_{i-1}) }$$ положительна, то учетная стоимость $$i$$ -й операции включает в себя резерв (предоплату за будущие операции); если же эта разность отрицательна, то учетная стоимость $$i$$ -й операции меньше реальной и разница покрывается за счет накопленного к этому моменту потенциала.

    Учетные стоимости и оценки реальной стоимости, рассчитанные с помощью метода потенциалов, зависят от выбора потенциальной функции, а сам этот выбор является делом творческим.

    Ниже эти три метода будут проиллюстрированы на примере анализа работы двоичного счетчика с единственной операцией Increment (прибавление единицы).

    Амортизационный анализ работы двоичного счетчика

    Рассмотрим работу $$k$$ -разрядного двоичного сбрасываемого счетчика, реализованного как массив битов $$A[0 \ldots k-1]$$, хранящего двоичную запись числа $$x$$. Будем считать, что $$A[0]$$ — младший разряд. Пусть первоначально $$x = 0$$. Единственной операцией в нашем примере будет операция Increment, увеличивающая $$x$$ на 1 по модулю $$2^k$$.

    Увеличение счетчика на единицу происходит следующим образом: все начальные единичные биты в массиве $$A$$, если они есть, становятся нулями, а следующий непосредственно за ними нулевой бит, если он есть, устанавливается в единицу. Стоимость операции Increment линейно зависит от общего количества битов, подвергшихся изменению. Каждое такое изменение будем считать элементарной операцией.

    Анализ работы двоичного счетчика методом группировки. Применим метод группировки для анализа сложности $$n$$ -кратного выполнения операции Increment. Поскольку в худшем случае, когда массив $$A$$ состоит из одних единиц, меняются все $$k$$ битов, то $$n$$ -кратное выполнение операции Increment может быть оценено как $$O(nk)$$ элементарных операций. Но эта оценка слишком груба.

    Чтобы получить более точную оценку, учтем, что не каждый раз значения всех $$k$$ битов меняются. В самом деле, младший бит $$A[0]$$ меняется при каждом исполнении операции Increment. Следующий по старшинству бит $$A[1]$$ меняется только через раз. При счете от нуля до $$n$$ этот бит меняется $$[n/2]$$ раз. Бит $$A[2]$$ меняется только каждый четвертый раз, и так далее. Заметим, что если $$0 \le i \le \log_2 n$$, то в процессе счета от $$0$$ до $$n$$ разряд $$A[i]$$ меняется $$[n/2^i]$$ раз, а если $$i > [\log_2 n]$$, то он вообще не меняется. Следовательно, общее количество операций зануления и записи 1 равно$$\eq*{ n + [n/2] + [n/4] + \ldots + [n/2]^{[\log n]} < n(1 + 1/2 + 1/4 + \ldots) = 2n. }$$ Тем самым, увеличение двоичного счетчика от $$0$$ до $$n$$ требует не более $$O(n)$$ операций, причем константа не зависит от $$k$$ и равна $$2$$. Учетную стоимость операции Increment можно считать равной $$O(n)/n = O(1)$$.

    Анализ работы двоичного счетчика методом предоплаты. Применим метод предоплаты для анализа сложности $$n$$ -кратного выполнения операции Increment. Будем считать, что реальная стоимость изменения бита составляет $$1$$ рубль. Установим такие учетные стоимости: $$2$$ рубля за запись единицы, $$0$$ за очистку. При каждой установке бита в единицу одним из двух рублей учетной стоимости будем расплачиваться за реальные затраты на эту установку, а второй рубль, остающийся в резерве, будем "прикреплять" к рассматриваемому биту. Поскольку первоначально все биты были нулевыми, в каждый момент к каждому ненулевому биту будет прикреплен резервный рубль. Стало быть, за очистку любого бита дополнительно платить нам не придется: мы расплатимся за нее рублем, прикрепленным к этому биту в момент его установки.

    Теперь легко определить учетную стоимость операции Increment. Поскольку каждая такая операция требует не более одной установки бита, ее учетную стоимость можно считать равной $$2$$ рублям. Следовательно, фактическая стоимость $$n$$ последовательных операций Increment, начинающихся с нуля, есть $$O(n)$$, поскольку она не превосходит суммы учетных стоимостей $$2n$$.

    Анализ работы двоичного счетчика методом потенциалов. Проанализируем теперь трудоемкость $$n$$ -кратного выполнения операции Increment с помощью метода потенциалов.

    Пусть $$D_0$$ — содержимое счетчика в начальный момент, $$D_i$$ — содержимое счетчика после выполнения $$i$$ -й операции, $$\phi(D_i)$$ — число единиц в записи $$D_i$$, $$t_i$$ — число единиц, превращенных в нули при $$i$$ -й операции. Очевидно, что$$\eq*{ \phi(D_i) \le \phi(D_{i-1}) - t_i +1. }$$

    Пусть далее $$c_i$$ — реальная стоимость $$i$$ -й операции Increment, $$C_i$$ — ее учетная стоимость. Очевидно, что $$c_i \le t_i + 1$$. Тогда$$C_{i} =c_{i} +\phi (D_{i})-\phi (D_{i-1} )\le t_{i} +1+\phi (D_{i} )-\phi (D_{i-1} )\le \\ \le t_{i} +1+\phi (D_{i-1})-t_{i} +1-\phi (D_{i-1})=2.$$

    Если счет начинается с нуля, то

    $$\eq*{ \phi (D_{0})=0 }$$

    и

    $$\eq*{ \phi (D_{i} )\ge \phi (D_{0}) }$$

    для всех $$i$$. Поскольку сумма учетных стоимостей оценивает сверху сумму реальных стоимостей, имеем

    $$\eq*{ \suml_{i=1}^{n}c_{i} \le \suml_{i=1}^{n}C_{i} \le 2n, }$$

    то есть получаем, что суммарная стоимость $$n$$ операций есть $$O(n)$$ с константой (двойкой), не зависящей от $$k$$.

    Метод потенциалов позволяет разобраться и со случаем, когда счет начинается не с нуля. В этом случае имеем

    $$\eq*{ \begin{gathered} \suml_{i=1}^{n}C_{i}=\suml_{i=1}^{n}c_{i} +\phi (D_{n} )-\phi (D_{0}),\\ \suml_{i=1}^{n}c_{i} =\suml_{i=1}^{n}C_{i} -\phi (D_{n} )+\phi (D_{0} )\le 2n+\phi (D_{0} )\le 2n+k, \end{gathered} }$$

    откуда при достаточно больших значениях $$n$$ ( $$n = \Om(k)$$ ) получаем, что реальная стоимость оценивается как $$O(n)$$, причем константа в $$O$$ -записи не зависит ни от $$k$$, ни от начального значения счетчика.

    Страницы:

    При разработке алгоритма для компьютерного решения той или иной задачи необходимая информация формализуется в виде набора элементов различных типов. В каждой системе программирования предусмотрено использование некоторых примитивных типов данных и средств, с помощью которых можно группировать их в более сложные структуры. Будем считать примитивными следующие типы: булевский, целый, вещественный и символьный. Многие системы программирования позволяют группировать данные примитивных типов в массивы однотипных элементов, записи из фиксированного числа элементов разных типов и в некоторые другие структуры. Вообще, под типом данных понимают произвольное множество, называемое множеством значений, и набор операций над значениями. Так, значениями целого типа являются целые числа из некоторого диапазона, зависящего от системы программирования, а операциями — обычные арифметические операции.

    При алгоритмизации задач, решение которых опирается на использование математических знаний и требует математических доказательств, разработка алгоритма часто проводится также в математических или формализованных прикладных терминах. При этом в достаточно большой степени происходит отвлечение от технических возможностей исполнителя алгоритмов (человека или технического устройства). Так, если при описании алгоритма используется понятие множества, то достаточно уметь выполнять некоторый набор операций с множествами и отвечать на некоторые вопросы относительно множеств. Перечень таких операций может быть следующим:

  • ввести в рассмотрение пустое множество;
  • включить элемент в множество;
  • исключить элемент из множества;
  • проверить, пусто ли рассматриваемое множество;
  • узнать, сколько элементов в рассматриваемом множестве.
  • Эти операции можно считать элементарными и до известной поры не думать о способе их реализации исполнителем. В таких случаях говорят, что мы имеем дело с абстрактным типом данных.

    Широко используемыми абстрактными типами данных наряду с множествами являются мультимножества, взвешенные множества, кортежи, отображения, приоритетные очереди, графы и так далее. Когда дело доходит до разработки программ в конкретной системе программирования, приходится принимать решения о представлении таких данных в памяти и о реализации операций с ними доступными средствами.

    В настоящее время большинство алгоритмов проектируется для использования в устройствах, обладающих адресуемой памятью. Каждый элемент информации, размещенный в такой памяти, занимает определенную позицию. По известной позиции элемента в такой памяти доступ к нему осуществляется за некоторую условную единицу времени, зависящую только от типа получаемой информации, фактически — от физического размера ячейки памяти или от количества таких ячеек, предназначенных для ее хранения, но не от ее конкретного содержания. Более того, позиции элементов сами могут быть элементами информации, с которыми могут производиться некоторые операции, что позволяет использовать так называемую косвенную адресацию. Наличие косвенной адресации позволяет поручить программной системе или разрабатываемой прикладной программе поиск свободных участков памяти для размещения новых элементов информации и запоминание их адресов с последующим использованием для доступа к информации.

    Информация, размещенная в адресуемой памяти, приобретает новые свойства. Элемент информации характеризуется не только своим содержанием, но и адресом, то есть местом расположения в памяти. Два элемента данных, соседних в содержательном смысле, не обязательно будут располагаться в соседних ячейках памяти. Они могут оказаться в "непредвиденных" местах. Проектируя программную реализацию алгоритма, необходимо проектировать и способ расположения в памяти обрабатываемой информации.

    Существуют типы данных, которые естественным образом вкладываются в адресуемую структуру технической памяти, причем легко выполняются все операции, предусмотренные для такого типа данных. Примером может служить вектор фиксированной размерности, задаваемый упорядоченным набором своих компонент. Наиболее естественным является его хранение в виде массива, при котором соседние компоненты располагаются в ячейках с идущими подряд номерами. Этот способ позволяет легко выполнять покомпонентные операции (сложение векторов, вычисление скалярного произведения и другие). Однако если размерность вектора в процессе работы изменяется, например путем удаления компонент или вставки новых, то представление в виде массива оказывается неудобным, так как операции удаления и вставки при условии сохранения порядка следования элементов требуют перезаписи, возможно, достаточно большого числа компонент, что может неблагоприятно сказаться на эффективности алгоритма. Чтобы избежать такой ситуации, одновременно с алгоритмом проектируют структуру представления данных, позволяющую реализовать выполнение всех необходимых операций в приемлемое время.

    В практике программирования накоплен большой опыт структурирования информации. К счастью, способы структурирования, изобретенные при решении одной задачи, часто находят применение и во многих других. Например, для представления кортежей и множеств в памяти компьютера могут использоваться такие структуры данных, как линейные и циклические списки.

    Во многих задачах исходные данные представляют собой так называемые взвешенные множества. Взвешенным называется множество, каждому элементу которого поставлено в соответствие в качестве веса некоторое число. Часто используемыми операциями с такими множествами являются поиск элемента с минимальным весом, вставка нового элемента со своим весом, удаление элемента и некоторые другие. Для быстрого выполнения таких операций разработаны так называемые кучеобразные структуры данных.

    Классы функций, используемые для оценки сложности алгоритмов

    Все функции, используемые ниже для оценки сложности алгоритмов, считаются асимптотически неотрицательными функциями натурального аргумента, то есть неотрицательными, начиная с некоторого значения аргумента $$n$$.

    Для асимптотических оценок сверху используется класс функций$${ O (g(n))=\{f(n):\exists c>0, \exists n_{0} \forall n>n_{0} [0\le f(n)\le c\cdot g(n)]\}. }$$

    Для асимптотических оценок снизу используется класс функций$${ \Omega (g(n))=\{ f(n):\exists c>0,\exists n_{0} \forall n>n_{0} [0\le c\cdot g(n)\le f(n)]\}. }$$

    Для асимптотически точных оценок используется класс функций$${ \Theta (g(n))=\{ f(n):\exists c_{1},c_{2} >0,\exists n_{0} \forall n>n_{0} [0\le c_{1} g(n)\le f(n)\le c_{2} g(n)]\}. }$$

    Очевидно, что справедливы следующие соотношения$${ \begin{gathered} \Theta (g(n))= O (g(n))\cap \Omega (g(n)),\\ f(n)\in O (g(n))\Leftrightarrow g(n)\in \Omega (f(n)). \end{gathered} }$$

    Далее, предполагается знание стандартных функций, используемых при оценках сложности, таких как полиномы, экспоненты, суперэкспоненты, логарифмы, суперлогарифмы, факториалы, числа Фибоначчи. Предполагается умение сравнивать их по скоростям роста.

    Амортизационный анализ

    Наряду с получением верхних и нижних оценок и оценок в среднем, часто используются так называемые амортизационные оценки.

    Амортизационный анализ применяется при оценке времени выполнения корректной последовательности, состоящей из $$n$$ однотипных или разнотипных операций с некоторой структурой данных. Если верхнюю оценку времени выполнения одной операции умножить на $$n$$, получим верхнюю оценку выполнения всех $$n$$ операций. Часто такая оценка бывает сильно завышенной. Иногда длительное время выполнения очередной операции влечет за собой малое время выполнения следующих операций. Более того, такая ситуация может создаваться искусственно, то есть при выполнении очередной операции мы можем готовить почву для более эффективного выполнения следующей. Поэтому возникает задача изучения асимптотического поведения гарантированной оценки для среднего времени выполнения одной операции.

    При амортизационном анализе определяется некоторая так называемая учетная (амортизационная) стоимость одной операции, которая может быть как больше, так и меньше реальной стоимости конкретной операции. Но при этом для любой корректной последовательности операций фактическая суммарная длительность всех операций не должна превосходить суммы их учетных стоимостей. Зная учетную стоимость одной операции, верхнюю оценку времени выполнения последовательности из $$n$$ операций можно получить, умножив ее на $$n$$.

    Ниже рассмотрим три часто используемых метода амортизационного анализа: метод группировки, метод предоплаты и метод потенциалов.

    Метод группировки. Предположим, что мы оценили сверху время выполнения последовательности из $$n$$ операций, установив, что она не превосходит $$T(n)$$, тогда величину $$T(n)/n$$ объявим учетной стоимостью любой операции из рассматриваемой последовательности, независимо от ее длительности.

    Метод предоплаты. В этом методе операции разных типов получают разные учетные стоимости, причем эти стоимости могут быть как больше, так и меньше фактических. Если учетная стоимость превосходит фактическую, то разность между ними рассматривается как резерв на оплату в будущем тех операций, у которых учетная стоимость ниже реальной. Учетные стоимости должны выбираться так, чтобы в любой момент времени фактическая стоимость не превосходила суммы учетных стоимостей, то есть чтобы резерв оставался неотрицательным.

    Метод потенциалов. Этот метод является обобщением метода предоплаты. Здесь резерв определяется функцией состояния структуры данных в целом. Эта функция называется потенциалом.

    Общая схема метода такова. Пусть над структурой данных предстоит произвести $$n$$ операций, и пусть $$D_i$$ — состояние структуры данных после $$i$$ -й операции ( $$D_0$$ — исходное состояние). Потенциал представляет собой функцию $$\phi$$ из множества возможных состояний структуры данных в множество действительных чисел.

    Пусть $$c_i$$ — реальная стоимость $$i$$ -й операции. Учетной стоимостью $$i$$ -й операции объявим число $$C_i$$, определяемое формулой$$\eq*{ C_{i} =c_{i} +\phi (D_{i})-\phi (D_{i-1}) }$$ как сумма реальной стоимости операции плюс приращение потенциала в результате выполнения этой операции. Тогда суммарная учетная стоимость всех операций равна$$\eq*{ \suml_{i=1}^{n}C_{i} =\suml_{i=1}^{n}c_{i} +\phi (D_{n} )-\phi (D_{0}). }$$

    Если нам удалось придумать функцию $$\phi$$, для которой$$\eq*{ \phi (D_{n} )\ge \phi (D_{0}), }$$ то суммарная учетная стоимость даст верхнюю оценку для реальной стоимости последовательности из $$n$$ операций. Не ограничивая общности, можно считать, что$$\eq*{ \phi (D_{0} )=0. }$$ Говоря неформально, если разность потенциалов$$\eq*{ \phi (D_{i} )-\phi (D_{i-1}) }$$ положительна, то учетная стоимость $$i$$ -й операции включает в себя резерв (предоплату за будущие операции); если же эта разность отрицательна, то учетная стоимость $$i$$ -й операции меньше реальной и разница покрывается за счет накопленного к этому моменту потенциала.

    Учетные стоимости и оценки реальной стоимости, рассчитанные с помощью метода потенциалов, зависят от выбора потенциальной функции, а сам этот выбор является делом творческим.

    Ниже эти три метода будут проиллюстрированы на примере анализа работы двоичного счетчика с единственной операцией Increment (прибавление единицы).

    Амортизационный анализ работы двоичного счетчика

    Рассмотрим работу $$k$$ -разрядного двоичного сбрасываемого счетчика, реализованного как массив битов $$A[0 \ldots k-1]$$, хранящего двоичную запись числа $$x$$. Будем считать, что $$A[0]$$ — младший разряд. Пусть первоначально $$x = 0$$. Единственной операцией в нашем примере будет операция Increment, увеличивающая $$x$$ на 1 по модулю $$2^k$$.

    Увеличение счетчика на единицу происходит следующим образом: все начальные единичные биты в массиве $$A$$, если они есть, становятся нулями, а следующий непосредственно за ними нулевой бит, если он есть, устанавливается в единицу. Стоимость операции Increment линейно зависит от общего количества битов, подвергшихся изменению. Каждое такое изменение будем считать элементарной операцией.

    Анализ работы двоичного счетчика методом группировки. Применим метод группировки для анализа сложности $$n$$ -кратного выполнения операции Increment. Поскольку в худшем случае, когда массив $$A$$ состоит из одних единиц, меняются все $$k$$ битов, то $$n$$ -кратное выполнение операции Increment может быть оценено как $$O(nk)$$ элементарных операций. Но эта оценка слишком груба.

    Чтобы получить более точную оценку, учтем, что не каждый раз значения всех $$k$$ битов меняются. В самом деле, младший бит $$A[0]$$ меняется при каждом исполнении операции Increment. Следующий по старшинству бит $$A[1]$$ меняется только через раз. При счете от нуля до $$n$$ этот бит меняется $$[n/2]$$ раз. Бит $$A[2]$$ меняется только каждый четвертый раз, и так далее. Заметим, что если $$0 \le i \le \log_2 n$$, то в процессе счета от $$0$$ до $$n$$ разряд $$A[i]$$ меняется $$[n/2^i]$$ раз, а если $$i > [\log_2 n]$$, то он вообще не меняется. Следовательно, общее количество операций зануления и записи 1 равно$$\eq*{ n + [n/2] + [n/4] + \ldots + [n/2]^{[\log n]} < n(1 + 1/2 + 1/4 + \ldots) = 2n. }$$ Тем самым, увеличение двоичного счетчика от $$0$$ до $$n$$ требует не более $$O(n)$$ операций, причем константа не зависит от $$k$$ и равна $$2$$. Учетную стоимость операции Increment можно считать равной $$O(n)/n = O(1)$$.

    Анализ работы двоичного счетчика методом предоплаты. Применим метод предоплаты для анализа сложности $$n$$ -кратного выполнения операции Increment. Будем считать, что реальная стоимость изменения бита составляет $$1$$ рубль. Установим такие учетные стоимости: $$2$$ рубля за запись единицы, $$0$$ за очистку. При каждой установке бита в единицу одним из двух рублей учетной стоимости будем расплачиваться за реальные затраты на эту установку, а второй рубль, остающийся в резерве, будем "прикреплять" к рассматриваемому биту. Поскольку первоначально все биты были нулевыми, в каждый момент к каждому ненулевому биту будет прикреплен резервный рубль. Стало быть, за очистку любого бита дополнительно платить нам не придется: мы расплатимся за нее рублем, прикрепленным к этому биту в момент его установки.

    Теперь легко определить учетную стоимость операции Increment. Поскольку каждая такая операция требует не более одной установки бита, ее учетную стоимость можно считать равной $$2$$ рублям. Следовательно, фактическая стоимость $$n$$ последовательных операций Increment, начинающихся с нуля, есть $$O(n)$$, поскольку она не превосходит суммы учетных стоимостей $$2n$$.

    Анализ работы двоичного счетчика методом потенциалов. Проанализируем теперь трудоемкость $$n$$ -кратного выполнения операции Increment с помощью метода потенциалов.

    Пусть $$D_0$$ — содержимое счетчика в начальный момент, $$D_i$$ — содержимое счетчика после выполнения $$i$$ -й операции, $$\phi(D_i)$$ — число единиц в записи $$D_i$$, $$t_i$$ — число единиц, превращенных в нули при $$i$$ -й операции. Очевидно, что$$\eq*{ \phi(D_i) \le \phi(D_{i-1}) - t_i +1. }$$

    Пусть далее $$c_i$$ — реальная стоимость $$i$$ -й операции Increment, $$C_i$$ — ее учетная стоимость. Очевидно, что $$c_i \le t_i + 1$$. Тогда$$C_{i} =c_{i} +\phi (D_{i})-\phi (D_{i-1} )\le t_{i} +1+\phi (D_{i} )-\phi (D_{i-1} )\le \\ \le t_{i} +1+\phi (D_{i-1})-t_{i} +1-\phi (D_{i-1})=2.$$

    Если счет начинается с нуля, то

    $$\eq*{ \phi (D_{0})=0 }$$

    и

    $$\eq*{ \phi (D_{i} )\ge \phi (D_{0}) }$$

    для всех $$i$$. Поскольку сумма учетных стоимостей оценивает сверху сумму реальных стоимостей, имеем

    $$\eq*{ \suml_{i=1}^{n}c_{i} \le \suml_{i=1}^{n}C_{i} \le 2n, }$$

    то есть получаем, что суммарная стоимость $$n$$ операций есть $$O(n)$$ с константой (двойкой), не зависящей от $$k$$.

    Метод потенциалов позволяет разобраться и со случаем, когда счет начинается не с нуля. В этом случае имеем

    $$\eq*{ \begin{gathered} \suml_{i=1}^{n}C_{i}=\suml_{i=1}^{n}c_{i} +\phi (D_{n} )-\phi (D_{0}),\\ \suml_{i=1}^{n}c_{i} =\suml_{i=1}^{n}C_{i} -\phi (D_{n} )+\phi (D_{0} )\le 2n+\phi (D_{0} )\le 2n+k, \end{gathered} }$$

    откуда при достаточно больших значениях $$n$$ ( $$n = \Om(k)$$ ) получаем, что реальная стоимость оценивается как $$O(n)$$, причем константа в $$O$$ -записи не зависит ни от $$k$$, ни от начального значения счетчика.

    Вернуться к учебному плану