Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

AlgStr / Библиотека / ЛЕКЦИИ / Реализация структур данных

.DOC
Скачиваний:
34
Добавлен:
23.03.2015
Размер:
179.2 Кб
Скачать

Функция абстракции

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

Эта взаимосвязь может быть определена функцией, которая называется функцией абстракции. Эта функция отображает объ­екты представления в абстрактные объекты:

А: repА

Здесь А обозначает набор абстрактных объектов. Для каж­дого объекта представления r, А(r) является абстрактным объ­ектом аA, который представляет r.

Рис. 1. Пример функции абстракции.

Например, функция абстракции для реализации intset ото­бражает массив array[int] в набор intset. На рис.1 показаны отображения некоторых точек функции абстракции. Заметим, что А может отображать различные элементы в один, т.е. различным объектам представления может соответствовать один и тот же абстрактный элемент из A. Например, [1: 1, 2] и [1: 2, 1] представ­ляют один набор intset {l, 2}. В том, что функция абстракции часто отображает различные элементы в один, нет ничего удиви­тельного, так как процесс абстракции отбрасывает не относя­щуюся к делу информацию. В нашем примере такая информация — это расположение элементов в массиве.

Функция абстракции – это важнейшая информация о реали­зации. Она определяет конкретное представление, т.е. то, каким образом объекты представления реализуют абстрактные объ­екты. Эта функция должна быть обязательно представлена в комментариях реализации. Описывая функцию реализации, мы иногда сталкиваемся с проблемой, связанной с тем, что, если спецификация типа неформальная, область изменения функции абстракции не может быть точно определена. Сейчас мы будем преодолевать эту проблему с помощью неформального описания «типичного» абстрактного объекта.

Для определения функции абстракции прежде всего опреде­ляем типичный элемент абстрактного типа. Это дает нам воз­можность говорить об абстрактных объектах. Затем можем оп­ределить функцию абстракции в терминах этого типичного объ­екта. Например, для наборов intset мы можем дать следующее описание:

% Типичный набор intset есть {х1, ... хn}

Здесь для описания наборов intset используем понятие мате­матического набора, точно так же, как делали это в специфи­кации intset. Затем пишем

% Функция абстракции есть

% А(r) = {r[i] | low(r) i high(r)},

где {x | p(x)} есть набор всех таких х, что р(х) имеет значение true.

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

% Типичный полином есть: c0 + c1x + c2x2+ ...

% Функция абстракции для коэффициента с1 есть

% ci = г [i], если low(r)  i  high (r)

% = 0 в противном случае

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

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

% Типичный стек – это последовательность [е1,...,еn], где

% еn – элемент со старшим индексом

% Функция абстракции есть

% А(г) = [r[low (r)], ..., r [high (r)]]

Если мы решаем уменьшать младший индекс и по этому ин­дексу записывать элемент, то

% А(r) = [r[high(r)], ..., r[low(r)]]

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

Инвариант представления

В ориентированных на работу с типами языках (как, например, в языке CLU) контроль типов гарантирует, что если абстрактный объект типа передается как аргумент операции типа, то этот аб­страктный объект представляется объектом типа представления. Во многих случаях, однако, не все объекты представления яв­ляются законными представлениями абстрактных объектов. На­пример, для кластера intset каждый массив независимо от значений является элементом R; например, массивы [1: 1, 7,6], [1: 1, 6, 6] и [1: 6, 1, 7]. Однако в реализации intset мы ре­шили, что каждый элемент набора записывается в массив только один раз. Следовательно, законные представления наборов int­set в этом кластере не содержат дублирующих друг друга элемен­тов; [1: 1, 6, 6], например — незаконное представление.

Условие, которому удовлетворяют все законные объекты, называется инвариантом представления. Инвариант представле­ния I есть предикат

I: гер  bool,

который принимает значение true для законных объектов пред­ставления. Например, для кластера intset мы можем дать следую­щий инвариант представления:

% Инвариант представления есть

% Для всех целых i, j, таких, что low(r)  i < j  high(r)

% r[i] ~ = r[j]

Заметим, что для массива [1: 1, 6, 6] I принимает значение false, а для массивов [1: 1, 7, 6] и [1: 6, 1, 7] – значение true. Заметим также, что мы не включаем в I условия, которые прини­мают значение true для всех массивов (например, size(r) = high(r) - low(r) + 1), потому что это гарантируется про­цедурами работы с массивами и соответственно предполагается в реализации intset.

В качестве второго примера инварианта представления рас­смотрим альтернативное представление наборов intset, состоя­щее из булева массива размерности 100 и массива целых:

гер = record [els: array [bool], other_els: array [int], size: int]

Идея заключается в том, что для целых от 1 до 100 мы отме­чаем их принадлежность набору, записывая значение true в r.els [i]. Не принадлежащие этому диапазону целые записываются в other_els точно так же, как в нашей предыдущей реализации intset. Так как невыгодно вычислять размер набора intset, про­сматривая все элементы массива els, мы записываем этот размер в представление в явном виде. Это представление хорошо, если почти все члены набора принадлежат диапазону 1 – 100. (В про­тивном случае пространство, требующееся для массива els, будет использоваться неэффективно.) Таким образом, мы имеем .

% Функция абстракции есть

% А (г) = {r.other.els [i] | low (r.other_els) i  high (r.other_els)}

% 

%{j | 1  j  100 & r.els[j]}

Здесь  обозначает математическое объединение двух набо­ров. Итак,

% Инвариант представления есть

% size (r.els) = 100 & low (r.els) = 1

% & все элементы r.other не принадлежат диапазону 1- 100

% & в r.other нет дубликатов элементов

% & r.size = size (r.other_els) +

% (число элем. со знач. true в r.els)

Заметим, что поле size в этом представлении излишне: оно со­держит информацию, которая может быть получена из остальной части представления путем вычислений. Но, поскольку такая из­лишняя информация все же содержится в представлении, взаимо­связь этой информации с остальной частью представления должна быть объяснена в инварианте представления (например, послед­няя строка нашего инварианта представления).

Иногда удобно использовать в инварианте представления или в функции абстракции вспомогательную функцию. Например, последняя строка инварианта представления может быть перепи­сана следующим образом:

% r.size = size (r.other_els) + cnt (r.els, low(r.els))

% где cnt (a, i) = if i > high(a) then 0

% else if r[i] then 1 + cnt (a, i + 1)

% else cnt (a, i + 1)

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

% Инвариант представления есть

% low (r)  0

% & if empty(r) then high (r) = 0

% else r [low (r) ] ~ = 0 & r [high (r) ] ~ = 0

Вспомним, что реализация операции degree предполагает, что верхняя граница массива равна степени полинома. Теперь мы видим это требование в инварианте представления.

Иногда все объекты представления законны. Тогда мы просто имеем

% Инвариант представления есть

% true

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

Инвариант представления – это «инвариант» потому, что он всегда сохраняется для представлений абстрактных объектов, т.е. он сохраняется, если объект используется вне его реализа­ции. Инвариант представления не должен, однако, обязательно сохраняться всегда: он может быть нарушен при выполнении од­ной из операций типа. Например, операция poly$add может по­лучить массив с нулевым старшим и младшим элементом, но такие элементы удаляются из массива перед возвратом из операции add. Инвариант представления должен сохраняться при возврате из операций.

Между функцией абстракции и инвариантом представления имеется определенная взаимосвязь. Функций абстракции имеет смысл только для законных представлений, так как только они представляют абстрактные объекты. Следовательно, нет необ­ходимости определять ее для незаконных представлений. Напри­мер, как мы упоминали раньше, функция абстракции для поли­номов имеет некую область определения – она определена только в случае, если массив имеет неотрицательную нижнюю границу. Этому требованию удовлетворяют все законные представления полиномов, что отражено в инварианте представления.

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

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

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

insert = proc(s: cvt, x: int)

rep$addh (s, x)

end insert

Эта реализация может создать объект с дублирующимися элементами. Если мы знаем, что инвариант представления за­прещает такие объекты, то эта реализация очевидно некорректна.

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

% Инвариант представления есть

% Для всех i, j, таких, что low (г) i < j high (г)

% r[i] < r[j]

Инвариант представления говорит тому, кто изучает реализацию, почему операции реализованы именно так, как они реализованы.

Сохранение инварианта представления

Для того чтобы продемонстрировать корректность реализа­ции типа, мы должны, в частности, показать, что инвариант пред­ставления сохраняется для всех законных объектов представ­ления. Мы можем сделать это следующим образом. Прежде всего мы покажем, что инвариант сохраняется для объектов, возвра­щаемых операциями (как, например, операцией poly$create), которые возвращают объект типа, но не имеют аргументов типа. Для всех других операций мы можем предположить, что при их вызове инвариант сохраняется для всех аргументов, являющихся объектами типа; тогда мы должны показать, что он сохраняется при возврате как для аргументов типа, так и для возвращаемых объектов типа.

Например, реализация набора intset имеет инва­риант

% Для всех целых i, j, таких, что low (г) i < j high (г)

%r[i] ~ = r[j]

Операция intset$create отвечает этому инварианту потому, что заново созданный массив пуст. Операция member также сохраняет этот инвариант, так как мы знаем, что инвариант сох­раняется для аргумента s и что эта операция не модифицирует s. Операция insert также сохраняет инвариант, так как

  1. инвариант сохраняется для ее аргумента s при обращении;

  2. при вызове операции member из операции insert инвариант сохраняется потому, что он сохраняется в операции member; и

  3. операция insert добавляет х к s, только если выражение member (s, х). имеет значение true; следовательно, так как на­бор s удовлетворяет инварианту при вызове, он также удовлетво­ряет инварианту после добавления к нему х.

В качестве второго примера рассмотрим реализацию поли­номов poly. Вспомним, что инва­риант есть

% low (r)  0

% & if empty (s) then high (r) = 0

% else r[low (r)l ~ = 0 & r[high (r)] ~ = 0

Операция poly$create сохраняет инвариант потому, что она создает нулевой полином. Операция mul сохраняет инвариант потому, что

  1. при вызове инвариант сохраняется для р и q;

  2. случай, когда р или q — нулевой полином, распознается и создается соответствующее представление; и

  3. в противном случае ни р, ни дне содержат нулей ни в стар­шем, ни в младшем члене; следовательно, ни младший элемент воз­вращаемого массива (bottom (р) * bottom (q)), ни старший эле­мент (top (р) * top (q)) не может быть нулем.

Здесь мы предполагаем, что абстрактные объекты нельзя мо­дифицировать вне их реализации. Если это возможно, то мы не можем предполагать, что инвариант сохраняется при вызове операции, так как какие-то модули могут его изменить.

Изменяемые представления

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

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

rep = record [num, denom: int]

Функция абстракции есть

% типичное рациональное число есть n/d

% Функция абстракции есть

% А (r) == r.num / r.denom

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

% Инвариант представления есть

% r.denom > 0

Давайте также решим находить сокращенную форму при про­верке двух рациональных чисел на равенство. Это мы можем сделать, используя процедуру gcd.

gcd = proc (n, d: int) returns (int)

requires n и d должны быть положительными

effects Возвращает макс. общий делитель n и d. ,

Реализация операции equal представлена на рис.2. Од­нажды найденные, сокращенные формы записываются в представ­ление, так как это ускоряет работу при последующих обраще­ниях к операции equal.

rep = record [num, denom: int] equal = proc (rl,. r2: cvt) returns (bool)

if rl.num = 0 then return (r2.num = 0)

elseif r2.num = 0 then return (false)

end

reduce (rl)

reduce (r2)

return (rl.num = r2.num cand rl.denom = r2.denom)

end equal

% reduce — внутренняя программа, приводящая ее аргумент к сокращенной

% форме

reduce proc (r: гер)

g: int := gcd (int$abs (r.num), r.denom)

r.num := r.num/g

r.denom := r.denom/g

end reduce

Рис. 2. Благоприятный побочный эффект.

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