Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
55
Добавлен:
01.05.2014
Размер:
2.53 Mб
Скачать
n + [n/2] + [n/4] + … + [n/2][log n]

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

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

Амортизационные оценки работы двоичного счетчика. Рассмот-

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

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

Анализ работы двоичного счетчика методом группировки. Приме-

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

Чтобы получить более точную оценку, учтем, что не каждый раз значения всех k битов меняются. В самом деле, младший бит A[0] меняется при каждом исполнении операции Increment. Следующий по старшинству бит A[1] меняется только через раз. При счете от нуля до n этот бит меняется [n/2] раз. Бит A[2] меняется только каждый четвертый раз, и так далее. Заметим, что если 0 ≤ i ≤ log2 n, то в процессе счета от 0 до n разряд A[i] меняется [n/2i] раз, а если i > [log2 n], то он вообще не меняется. Следовательно, общее количество операций зануления и записи 1 равно

< n(1 + 1/2 + 1/4 + …) = 2n.

Тем самым увеличение двоичного счетчика от 0 до n требует не более O(n) операций, причем константа не зависит от k и равна 2. Учетную стоимость операции Increment можно считать равной O(n)/n = O(1), константа не зависит от k.

Анализ работы двоичного счетчика методом предоплаты. Приме-

181

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

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

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

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

Пусть D0 – содержимое счетчика в начальный момент, Di – содержимое счетчика после выполнения i-й операции, φ(Di) – число единиц в записи Di , ti – число единиц, превращенных в нули при i-й операции.

Очевидно, φ(Di) ≤ φ(Di1) ti + 1.

Пусть далее ci – реальная стоимость i-й операции Increment, Ci – ее учетная стоимость.

Очевидно ci ti + 1. Тогда

Ci = ci + φ(Di ) −φ(Di1 ) ti +1+ φ(Di ) −φ(Di1 )

ti +1+ φ(Di1 ) ti +1−φ(Di1 ) = 2.

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

φ(D0 ) = 0

и

φ(Di ) ≥ φ(D0 )

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

182

n

n

ci Ci 2n,

i=1

i=1

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

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

 

n

 

n

 

Ci = ci + φ(Dn ) −φ(D0 ),

 

i=1

i=1

n

n

 

 

ci = Ci

−φ(Dn ) + φ(D0 ) 2n + φ(D0 ) 2n + k,

i=1

i=1

 

 

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

183

Глава 1. СПИСКИ

1.1. Общие сведения о списках

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

Кортеж – это конечная последовательность, возможно с повторениями, элементов некоторого множества E. Элементами кортежа могут быть числа, символы некоторого алфавита, точки плоскости и т. д. В более сложных случаях элементами кортежа, в свою очередь, могут быть также кортежи. Элементы, не являющиеся кортежами, называются атомами. Кортеж характеризуется своей длиной. Удобно рассматривать кортежи, не содержащие ни одного элемента. Такие кортежи называются пустыми. Длина пустого кортежа считается равной 0.

Элемент кортежа характеризуется своим номером в последовательности (кортежным номером) и своим содержанием, то есть элементом множества E. Если длина кортежа равна n, n > 0, то кортеж S удобно рассматривать как отображение s множества N = {1, 2, ..., n} в множество E. Таким образом, s(i) – это i-й элемент кортежа S.

Термин «список» используется как обобщающее название различных структур данных, используемых для представления кортежей в памяти компьютера. При представлении кортежа в памяти появляется еще одна характеристика элемента кортежа – его позиция в памяти. В некоторых случаях номер элемента в кортеже и его позиция в памяти связаны друг с другом арифметическими соотношениями таким образом, что по номеру легко вычисляется позиция и, наоборот, по позиции вычисляется номер. В других случаях связь между номерами и позициями задается «таблично» или осуществляется с помощью алгоритмических процедур. Множество позиций обозначим через P. Иногда удобно считать, что в множестве P имеется специальный элемент nil, указывающий на несуществующую область памяти. Таким образом, при рассмотрении того или иного списка имеем дело с тремя множествами E, N, P и с отображениями на этих множествах.

184

Типичными при работе со списками являются следующие операции:

Нахождение позиции элемента в памяти по его номеру в кортеже.

Нахождение позиции элемента, следующего в кортеже за элементом из заданной позиции.

Нахождение позиции элемента, предшествующего в кортеже элементу из заданной позиции.

Удаление элемента, находящегося в заданной позиции.

Вставка в кортеж нового элемента перед элементом, расположенным в заданной позиции.

Определение длины кортежа.

При описании этих и других операций со списками будем использовать следующие отображения и константы, заданные на множествах E,

N, P:

Info: P E, где Info (pos) элемент списка, находящийся в позиции pos памяти.

Next: P P, где Next (pos) позиция элемента, следующего за элементом из позиции pos.

Precede: P P, где Precede (pos) позиция элемента, находящегося перед элементом из позиции pos.

Number: P N, где Number (pos) кортежный номер элемента, находящегося в позиции pos.

Position: N P, где Position (k) позиция элемента, имеющего кортежный номер k.

Length длина списка.

First позиция первого элемента списка.

Last позиция последнего элемента списка.

Иногда для распознавания концевых элементов списка пользуются следующим соглашением: eсли pos является позицией последнего элемента списка, то полагают Next (pos) = pos, а если началом, то – Preced (pos) = pos. В случаях когда позицией элемента, следующего за последним или предшествующего первому, является несуществующая позиция nil, будем считать, что nil принадлежит множеству P. При изменении содержимого списка все введенные множества, отображения и константы могут изменяться.

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

185

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

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

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

При представлении кортежей, для которых планируется выполнение операций вставки/удаления элемента из произвольной позиции, используется возможность нахождения программным путем свободного пространства в памяти для размещения вставляемого элемента. При использовании языков программирования высокого уровня эти обязанности обычно берет на себя система программирования (оператор new – в языках PASCAL и C). При вставлении нового элемента в список место, куда он вставляется, указывается с помощью косвенной адресации. Это может быть адрес элемента, перед которым либо после которого, вставляется новый элемент,

186

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

Отметим еще, что при конструировании списков иногда удобно элементами списка считать не сами элементы множества E, а их позиции в памяти. В этом случае списки по терминологии Р. Тарьяна называются экзогенными (внешними), в противном случае – эндогенными (внутренними). Эндогенный способ используют в тех случаях, когда элементы множества E для своего представления требуют большого пространства и переписывание элемента из одного участка памяти в другой сопряжено с большими затратами времени.

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

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

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

Поля, относящиеся к конкретному списку L, будем записывать в форме

L.<имя_поля>.

187

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

Так, например, дескриптор списка L может иметь форму

[first, length].

При таком дескрипторе

L.first означает позицию первого элемента списка L,

L.length – его длину.

1.2. Списки с прямым доступом

Прямой доступ, как правило, реализуется при представлении списка массивом. Элементы кортежа размещаются в идущих подряд ячейках некоторого массива. Для локализации списка в массиве введем целочисленную переменную first для хранения номера позиции массива, в которой расположен его первый элемент, и целочисленную переменную length, означающую длину списка. Равенство length = 0 является признаком того, что массив содержит пустой список. Иногда для переменных, хранящих позицию элемента массива, удобно иметь какое-либо условное значение, выходящее за рамки индексации массива. Будем обозначать его beyond.

Рассмотрим подробнее реализацию списка с прямым доступом, дающую возможность добавлять элементы к списку с любого его конца. Воспользуемся циклической «нумерацией» элементов массива, при которой следующим за последним элементом массива считается его первый элемент, а предыдущим для первого – последний (речь идет об элементах массива, а не об элементах списка). Если элементы массива пронумеровать числами от 0 до n 1, то переход к следующему (предыдущему) элементу списка осуществляется с помощью прибавления (вычитания) единицы по модулю n, где n – длина массива.

Дескриптор такого списка будет иметь форму

S = [n, info, first, length].

Добавление элемента к началу списка осуществляется его записью в позицию newfirst = (first – 1) mod n (0 newfirst < n) с последующим присваиванием first := newfirst, а добавление в конец записью элемента в позицию (first + length) mod n с последующим выполнением оператора length := (length + 1). Заметим, что при таком способе начальный фрагмент кортежа может оказаться в конце массива, а конечный фрагмент – в

188

начале. Заметим также, что добавление нового элемента возможно только при условии length < n. Следует заметить также, что в системах программирования со статическим распределением памяти под массивы, которое происходит во время компиляции, длину массива следует выбирать достаточной для размещения списков, порождаемых разрабатываемым алгоритмом. Следует иметь в виду, что максимальная длина списка зависит не только от алгоритма, но и от входных данных.

Основные отображения для списка с прямым доступом, имеющего дескриптор S = [n, info, first, length], определяются следующим образом:

Info (pos) = S.info [pos],

Next (pos) = if (pos = S.first+S.length1) then pos

else if (S.length < S.n) then (pos + 1) mod S.n else beyond,

Preced (pos) = if (pos = S.first) then pos

else if (S.length < S.n) then (pos 1) mod S.n else beyond, Last = (S.first + S.length 1) mod S.n,

Number (pos) = if (S.first < pos) then (pos S.first + 1) else (S.pos S.first + S.n 1).

Приведем несколько процедур для работы со списками с прямым доступом.

Создать пустой список S

procedure SetEmpty (S);

begin S.first:=0; S.length:=0 end;

Добавить элемент e к концу списка S

procedure AddToEnd (e, S); begin

if S.length < S.n

then {S.info[(S.first + S.length) mod S.n] := e; S.length := S.length + 1} else 'массив переполнен'

end;

Добавить элемент e к началу списка S

procedure AddToBegin (e, S);

189

begin

if S.length < S.n

then {S.first := S.first1; S.info[S.first] := e; S.length := S.length + 1} else 'массив переполнен'

end;

Заменить элемент с кортежным номером k на элемент e

procedure Set (k, e, S); begin

S.info [S.first + k 1] := e end;

Удалить последний элемент списка S

procedure DelLast (S); begin

if S.length > 0 then S.length:= S.length 1 else 'список пуст'

end;

Удалить первый элемент списка S

procedure DelFirst (S); begin

if S.length > 0

then {S.first := (S.first + 1) mod S.n; S.length := S.length 1} else 'список пуст'

end;

1.3. Списки с последовательным доступом

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

190