Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Лекции флп.doc
Скачиваний:
270
Добавлен:
09.02.2015
Размер:
3.68 Mб
Скачать

Лекция №10 Неполные структуры данных

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

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

Разностные списки

Рассмотрим последовательность 1,2,3. Она может быть представлена как разность двух списков – списков [1,2,3,4,5] и [4,5], или списков [1,2,3,8] и [8] или списков [1,2,3] и [ ]. Каждый из этих случаев представляет собой пример разности между двумя неполными списками [1,2,3 | Xs] и Xs.

Для обозначения разности двух списков будем использовать структуру As|Bs, которая названа разностным списком. В этой структуре As голова разностного списка, a bsего хвост. Для рассмотренного примера [1,2,3 | Xs] \ Xs есть наиболее общий разностный список, представляющий последовательность 1,2,3 (здесь [1,2,3 | Xs] – голова, а Xs – хвост разностного списка).

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

Обычные и разностные списки тесно связаны. И те и другие используются для представления последовательностей элементов. Любой список L может быть тривиально представлен как разностный список L\[ ]. Пустой список представляется посредством любого разностного списка, голова и хвост которого одинаковы, наиболее общей формой при этом будет разностный список As\As,

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

Рассмотрим списки на рис. 15,1. Разностный список Xs\Zsэто результат присоединения разностного списка Ys \ Zs к разностному списку Х\Ys. Это может быть выражено одним фактом. Программа 15.1 определяет предикат append_dl(As,Bs, Cs), который истинен, если разностный список Cs является результатом присоединения разностного списка Bs к разностному списку As. Суффикс _dl используется для обозначения варианта предиката (соединения), в котором аргументами являются разностные списки.

Необходимым и достаточным условием возможности соединения двух разностных списков As\Bs и Xs\Ys с помощью программы 15.1 является унифицируемость Bs и Xs. В этом случае соединенные разностные списки совместимы. Если хвост разностного списка не конкретизирован, то он совместим с любым разностным списком. Более того, в таком случае программа 15.1 будет соединять списки за константное время. Например, вопросу append_dl([a,b,c|Xs]\Xs,[1,2]\[ ],Ys)? будет соответствовать результат (Xs = [1,2], Ys = [a,b,c,l,2]\[ ]).

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

Рис. 15.1 Соединение разностных списков

append_dl ( As. Bs. Сs) 

разностный список Cs – результат присоединения разностного списка Bs к разностному списку As; As и Bs – совместные разностные списки

append_dl(Xs\Ys,Ys\Zs,Xs\Zs).

Программа 15.1. Соединение разностных списков.

flatten(Xs, Ys)

Ys раскрытый список, составленный из элементов, содержащихся в Xs.

flatten (Xs.Ys)  flatten_dl(X, Ys\[ ]). flatten_dl([X!Xs].Ys\Zs)

flatten_dl(X,Ys\Ysl}.flatten_dl(Xs, Ys1\Zs). flatten_dl(X,[X | Xs]\Xs)

constant(X), X [ ]. flatten_dl([ ],Xs\Xs).

Программа 15 2. Программа раскрытия списка списков с использованием разностных списков.

Хорошим примером программы, которую можно усовершенствовать путем использования разностных списков, является программа 9.1 а, предназначенная для раскрытия списков. В ней использованы двойная рекурсия для раздельной линеаризации головы и хвоста списков и последующее соединение полученных списков друг с другом. Приспособим эту программу для вычисления отношения flatten _dl(Ls, Xs), где Хs – разностный список, представляющий элементы, которые располагаются в списке списков Ls в правильном порядке. Ниже представлен результат прямой трансляции программы 9.1а в эквивалентную программу с разностными списками.

flatten_dl(X | Xs],Ys \ Zs)

flatten_dl(X, As \ Bs),flatten_dl(Xs,Cs\Ds),

append_dl(As\Bs,Cs\,Ds,Ys\Zs).

flatten_dl(X,[X | Xs]\Xs)

constant(X), X  [ ]

flatten_dl([ ],Xs \ Xs).

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

flatten_dl([X | Xs].As\Ds)  flatten_dl(X, As, Bs), flatten_dl(Xs, Bs\Ds).

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

flatten(Xs,Ys) flatten._.dl(Xs,Ys\[ ]).

Наконец, выполнив необходимые переименования переменных, получим программу 15.2.

При декларативном прочтении программа 15.2 выглядит простой. Благодаря тому, что раскрытие исходного списка приводит к разностному, а не обычному списку, явное обращение к предикату append оказывается необязательным. Полученная программа более эффективна, так как размер ее дерева доказательства линейно, а не квадратично зависит от числа элементов в списке списков.

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

Рассмотрим, как действует эта программа. На рис. 15.2 представлена трассировка выполнения программы, вызванного вопросом: flatten([[a],[b,[c]]],Xs)?

flatten([[a].[b,[c]]],Xs)

flatten_dl([[a].[b,[c]]],Xs\[ ]

flatten _dl([a],Xs\Xs1)

flatten _dl(a,Xs\Xsl) Xs=[a|Xs2]

constant(a)

a[ ]

flatten_dl([ ].Xs2\Xsl) Xs2 = Xs1

flatten_dl([[b,[c]]],Xsl\[ ])

flatten_dl([b,[c]],Xsl\Xs3)

flatten_d1(b,Xs1\Xs4) Xsl =[b|Xs4]

constant(b)

b[ ]

flatten_dl([[c]],Xs4\Xs3)

flatten_dl([c].Xs4\ Xs5)

flatten _dl(c,Xs4\ Xs6) Xs4 = [c | Xs6]

constant(c)

c[ ]

flatten_dl([ ],Xs6\Xs5) Xs6 = Xs5

flatten_dl([ ].Xs5\Xs3)` Xs5 = Xs3

flatten _dl([ ],Xs3\[ ]) Xs3 = [ ]

Выход: Xs = [a,b,c]

Рис. 15.2. Трассировка вычислений с использованием разностных списков.

Трассировка показывает, что результат Xs формируется по принципу «сверху вниз» (в терминологии разд. 7.5), Хвост разностного списка действует подобно указателю конца неполной структуры данных. Указатель устанавливается посредством унификации. Благодаря использованию таких «указателей» не требуется в отличие от программы 9.1а строить промежуточные структуры.

Противоречие между ясностью декларативного и трудностью процедурного понимания программы обусловлено мощью логической переменной. Можно описывать логические отношения неявно и предоставлять их интерпретацию Пролог-системе. Здесь соединение разностных списков выражается неявно, и его появление в программе кажется таинственным. Программы, написанные с использованием разностных списков, иногда структурно подобны программам, в которых используются накопители. В упражнении 9.1(1) предлагалось написать с использованием двойной рекурсии программу для отношения flatten, в которой вместо обращения к предикату append используются накопители. Решением будет следующая программа:

flatten(Xs, Ys)  flatten(Xs,[ ],Ys).

flatten([X | Xs],Zs, Ys)

flatten(Xs,Zs,Ysl),flatten(X, Ysl, Ys). flatten (X, Xs,[X | Xs])

constant(X), X  [ ]. flatten([ ],Xs, Xs).

Она весьма схожа с программой 15.2, за исключением двух отличий. Первое отличие – синтаксическое. Разностный список представляется двумя аргументами, однако они расположены в обратном порядке: хвост предшествует голове списка. Второе отличие заключается в порядке целей в рекурсивном предложении flatten. В результате раскрытый список строится по принципу «снизу вверх», т.е. начиная с хвоста.

Рассмотрим еще один пример сходства разностных списков и накопителей. Программа 15.3 является результатом преобразования «наивной» версии предиката reverse, а именно программы 3.16а. В новом варианте обычные списки заменены разностными списками, а операция append вообще не применяется.

reverse(Xs, Ys) 

список Ys – есть обращение списка Xs.

reverse (Xs,Ys)  reverse _dl(Xs,Ys \ [ ]).

reverse_.dl([X | Xs].Ys \ Zs)

reverse_dl(Xs,Ys \ [X | Zs]). reverse_dl([ ],Xs \ Xs).

Программа 15.4Обращение с использованием разностных списков.

quicksort ( List, SortedList) 

sortedlisi упорядоченная перестановка элементов списка List.

quicksort(Xs,Ys)  quicksort_dl(Xs,Ys \ l:]).

quicksort_dl([X | Xs],Ys \ Zs)

partition (Xs, X, Littles, Bigs),

quicksort_dl(Littles,Ys \ |:X | Ysl]),

quicksort_dl(Bigs,Ysl \ Zs).

quicksort_dl([ ],Xs \ Xs).

partition(Xs,X,Ls,Bs)  См. программу 3.22.

Программа 15.4. Программа быстрой сортировки с использованием разностных списков

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

Программа 3.22 (quicksort) – логическая программа, реализующая алгоритм быстрой сортировки, является примером программы с двойной рекурсией, в которой конечный результат – упорядоченный список, получается соединением двух упорядоченных подсписков. С помощью применения разностных списков можно получить более эффективную программу сортировки. Как показано в программе 15.4, все операции append, используемые при объединении частичных результатов, могут быть выполнены неявно.

В программу 15.4. аналогично тому, как это было сделано в программе 15.2 для отношения flatten, первым включено предложение с заголовком quicksort и целью quicksort_dl. Рекурсивное предложение в этой программе определяет алгоритм быстрой сортировки, реализованный с использованием разностных списков, в котором конечный результат достигается неявным соединением частичных результатов. Последнее предложение quicksort_dl определяет, что результат сортировки пустого списка есть пустой разностный список. Отметим, что при выполнении унификации цели quicksort_dl(Littles,Ys\[X\Ys1]) обеспечивается размещение «разделяющего» элемента Х после наименьшего элемента списка Ys и перед наибольшим элементом из Ys1.

Программа 15.4 получена преобразованием программы 3.22 таким же образом, что и программа 15.2 из программы 9.1а. Списки заменяются разностными списками, а цель append_dl раскрывается соответствующим образом. Предложение quicksort с целью quicksort_dl выражает отношение между списком, подлежащим сортировке, и разностным списком, используемым в процессе вычислений.

Замечательным примером преимущества использования разностных списков является решение упрощенной версии задачи Дейкстры о голландском флаге. Задача формулируется следующим образом: «Дан список элементов, окрашенных в красный, белый и голубой цвета. Упорядочить список так, чтобы все красные элементы расположились вначале, за красными должны следовать белые элементы, за которыми в свою очередь должны быть расположены голубые элементы. При переупорядочении должен быть сохранен исходный относительный порядок элементов одного и того же цвета». Например, список [red(l),white(2),blue(3),red(4).white(5)] после переупорядочения должен принять вид [red(l),red(4),white(2),white(5),blue(3)].

В простом и разумном решении этой задачи, представленном программой 15.5 элементы исходного списка сначала распределяются по трем различным спискам, которые затем соединяются друг с другом в нужной последовательности. Базовое отношение в программе – dutch (Xs,Ys), где Xs – исходный список окрашенных элементов, a Ys – переупорядоченный список с требуемым расположением элементов по цветам.

dutch (Xs,RedWhitesBliies) 

RedsWhitesBlues список элементов списка Xs, упорядоченных по цветам: красные. белые, голубые.

dutch(Xs,RedsWhitesBlues) 

distribute{Xs, Reds, Whites, Blues),

append(Whites, Blues, WhitesBlues).

append(Reds,WhitesBlues,RedsWhitesBlues).

distribute (Xs, Reds, Wiites, Blues)

Reds, Whites и Blues – списки красных, белых и голубых элементов списка Xs соответственно.

distribute([red(X) | Xs].[red(X) | Reds],Whites, Blues) 

dislribute(Xs. Reds. Whites, Blues),

dislribute([white(X) | Xs],Reds,[white(X) | Whites], Blues) 

distribute(Xs. Reds, Whites, Blues).

distribute([blue(X) | Xs].Reds,Whitcs,[biue(X)|Blues])

distribute(Xs, Reds, Whites, Biues).

distributed:([ ].[ ],[ ].[ ]).

append (Xs,Ys,Zs)  См. программу 3.15.

Программа 15.5. Решение задачи о голландском флаге.

Ядро программы – процедура distribute, которая строит три одноцветных списка. Списки строятся по принципу «сверху вниз». Если использовать предикат distribute_dls для построения трех различных разностных списков вместо трех обычных, то два обращения к процедуре append могут быть исключены из программы. Соответствующая модификация представлена в виде программы 15.6.

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

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

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

dutch ( Хs,RedsWhitesBlues)

RedsWhitesBlues список элементов списка Xs, упорядоченных по цвету: красные, белые, голубые.

dutch(Xs,RedsWilesBlues)

distribute_dls(Xs,RedsWhitesB1ues\WhitesBlues,WhitesBlues\Blues,Blues\[ ]).

distribute_dls (Xs, Red,Wiiles,Blues)

Reds, Whites и Blues: разностные списки красных, белых и голубых элементов в списке Xs соответственно.

distribute_dls([red (X) | Xs], [red (X)| Reds] \ Reds1, Whites, Blues) 

distribute, dls(Xs, Reds\Reds1, Whites, Blues).

distribute_dls([while(X) | Xs], Reds, [white(X) | Whites] \Whitesl, Blues) 

distribute_dls(Xs, Reds, Whites \ Whites1, Blues).

distribute_dls([blue(X) | Xs], Reds, Whiles, [blue(X)| Blues]\Blues!) 

distribute_dls(Xs,Reds,Whites,Blues\Blues1).

distribute_dls([ ],Reds\Reds,Whiles\Whites,Blues\Blues).

Программа 15.6. Решение задачи о голландском флаге с использованием разностных списков.

Разностные структуры

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

Рассмотрим задачу нормализации арифметических выражений, например сумм. На рис. 15.3 представлены две суммы: (a+b)+(c+d) и (а+(b+(с+d))). Согласно стандартному синтаксису Пролога, скобки в терме а + b расставляются следующим образом: ((а+ b)+ с). Опишем процедуру преобразования суммы в нормализованную сумму, в которой сгруппированы правые скобки. Например, выражение, представленное на рис, 15.3 слева, должно быть преобразовано в нормализованное выражение, показанное справа. Такая процедура полезна для упрощения алгебраических выражений, облегчения написания программ проверки эквивалентности выражений.

Введем понятие разностной суммы как некоторого варианта разностного списка. Разностная сумма представлена структурой вида Е1 + + Е2, где Е1 и E2 – неполные нормализованные суммы. Предполагается, что + + является обозначением бинарного инфиксного оператора. Условимся также использовать 0 для обозначения пустой суммы.

Рис 15.3. Ненормализованная и нормализованная суммы

Программа 15.7 является программой для нормализации сумм. Реляционной схемой программы является отношение normalize (Exp,NormExp}, где NormExp выражение, эквивалентное выражению Ехр, в котором сгруппированы правые скобки при сохранении порядка констант, определенного в выражении Ехр.

normalize (Sum, NormalizedSiim)

NormalizedSum – результат нормализации выражения Sum.

normalize(Exp,Norm)  normalize._ds(Exp,Norm+ +0).

normalize_ds(A + В, Norm + + Space) 

normalize_ds(A, Norm+ +NormB),

normalize_ds(B, NormB + + Space).

normalize_ds(A,(A + Space) + +Space) 

constant (A).

Программа 15.7. Нормализация выражений типа «сумма».

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

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

Эти программы имеют простой декларативный смысл. Операционное их толкование состоит в представлении процесса построения структуры путем ее наращивания, когда явно указывается «место» для последующих результатов. Здесь видна полная аналогия с разностными списками.

Справочники

Отличный от рассмотренных способ использования неполных структур данных позволяет реализовывать справочники. Рассмотрим задачу создания, использования и обновления набора значений, индексированных ключами. Существуют две основные операции, выполняемые со словарем: поиск значения, записанного под определенным ключом, и ввод новых ключей и связанных с ними значений. Эти операции должны выполняться с обеспечением непротиворечивости, так, например, один и тот же ключ не должен встречаться дважды с двумя разными значениями. Оказывается, можно выполнять обе операции – поиск значений по ключу и ввод новых ключей и значений – с использованием одной простой процедуры, в которой применяются неполные структуры данных.

Рассмотрим линейную последовательность пар ключ-значение. Преимущества использования для их представления неполной структуры данных очевидны. Программа 15,8 определяет отношение lookup(Key,Diсt,Value), которое истинно, если запись с ключом Key в справочнике Dict имеет значение Value. Словарь представляется как неполный список пар вида (Key, Value).

lookup (Key, Dictionary, Value)

Dictionary содержит значение Value, индексированное ключом Key.

Dictionary представляется списком пар (Key, Value).

lookup(Key, [(Key, Value) | Dictionary], Value).

lookup(Key,[Keyl, Value!) | Dictionary], Value) 

Key  Key1,lookup(Key, Dictionary, Value),

Программа 15.8. Поиск в словаре, представленном списком кортежей.

Рассмотрим пример справочника, используемого для хранения номеров телефонов, фамилии владельцев которых играют роль ключей. Предположим, что справочник Diсt первоначально представлен списком [(арнольд, 8881), (барри, 4513), (кэти, 5950)|Xs]. Тогда на вопрос lookup(арнольд, Dict,N)?, используемый для поиска номера телефона человека по имени Арнольд, будет получен ответ N = 8881. Вопрос lookup (барри,Dict,4513), используемый для проверки номера телефона Барри, имеет утвердительный ответ.

Ввод новых ключей и значений рассмотрим на примере вопроса lookup (Дэвид, Dict, 1199)?. Синтаксически этот вопрос кажется проверкой номера телефона Дэвида. Но он приводит к иному результату. Этот вопрос успешно решается: справочник Dict будет теперь представлен списком [(арнольд,8881), (барри,4513), (кэти,5950), (дэвид, 1199) |Хs1]. Таким образом, с помощью процедуры lookup вводится новое значение.

Что произойдет, если проверить номер телефона Кэти с помощью вопроса lookup(кэти,Dict,5951)?, в котором номер задан неправильно? Вторая запись в справочник номера телефона Кэти сделана не будет: вследствие проверки Key Keyl решение вопроса будет безуспешным.

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

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

Бинарные деревья, описанные в разд. 3.4, преобразуются в четырехместную структуру dict(Key, Value, Left, Right), где Left и Right левый и правый подсправочники соответственно, a Key и Valueкак и прежде, ключ и значение. Функтор did напоминает, что речь идет о справочнике.

Поиск по дереву-справочнику имеет очень элегантное определение, напоминающее по духу определение процедуры поиска в программе 15.8. Он выполняется рекурсивно по двоичным деревьям, а не по спискам и основан на унификации для конкретизации переменных в структурах справочника. В программе 15.9 определена процедура lookup (Key. Dictionary, Value), которая, как и прежде, обеспечивает поиск значения по заданному ключу и вводит новые значения.

На каждом шаге поиска ключ сравнивается с ключом, записанным в текущем узле. Если ключ поиска меньше ключа в текущем узле, то рекурсивно продолжается поиск по левой ветви, если больше, то выбирается правая ветвь. Если ключ не числовой, то предикаты < и > необходимо обобщить. В программе 15.9 в отличие от программы 15.8 следует использовать отсечение. Это обусловлено «нелогическим» характером действий операторов сравнения, которые будут приводить к ошибкам, если ключи не конкретизированы.

lookup (Key,Dictionary, Value)

Dictionary содержит значение Value, индексированное ключом Key.

Dictionary представляется упорядоченным, бинарным деревом.

lookup(Key,dict,(Kev,X, Left, Right), Value) 

!, X = Value.

lookup(Key,dict(Key1,X,Left,Right), Value) 

Key < Keyl,lookup(Key, Left, Value).

lookup(Key,dict(Keyl,X, Left, Right), Value) 

Key > Keyl, lookup(Key, Right, Value).

Программа 15.9. Поиск в словаре, составленном в виде бинарного дерева.

Если имеется ряд пар ключей и значений, то справочник их определяет не единственным образом. Форма справочника зависит от порядка, в котором поступали вопросы.

Справочник может использоваться и для размораживания терма, который был заморожен с помощью программы 13.2 для предиката numbervars. С этой целью может использоваться программа 15.10. Каждая размороженная переменная вводится в справочник так, чтобы значения были присвоены соответствующим общим переменным.

freeze(A,B)

Заморажнвание терма А в терме В.

freeze (А, В) 

сору(А.В), numbervars(B,0,N).

Melt_new( А,В)

Освобождение (размораживание) терма А в терме В.

melt_new(A,B) 

melt (А, В, Dictionary),!.

melt ('$VAR'(N),X, Dictionary) 

lookup(N, Dictionary, X).

mell(X,Y, Dictionary) 

conslant(X).

melt(X,Y, Dictionary) 

compound(X),

functor(X,F,N),

funclor(Y,F,N),

melt(N,X,Y,Dictionary).

melt(N,X,Y, Dictionary) 

N >0.

arg(N,X,ArgX),

melt(ArgX,ArgY, Dictionary),

arg(N,Y,ArgY),

N1:= N- 1,

melt(N1,Х,У, Dictionary).

melt(0,X,Y, Dictionary).

copy(A.B)  assert('$foo'(A)),retract('$foo'(B)).

numbervars(Term,Nl,N2)  См. программу 13.2.

lookup(Key. Dictionary, Value)  См. программу 15.9.

Программа 15.10. Размораживание терма.

Очереди

Интересным применением разностных списков является организация очередей. Очередь – структура данных для хранения списков, используемая по принципу: «первым записан – первым считан». Голова разностного списка представляет начало очереди, хвост – ее конец, а объекты разностного списка – это элементы очереди. Очередь пуста, если пуст разностный список, т.е. голова и хвост тождественны.

Управление очередью отличается от управления описанным выше справочником. Рассмотрим отношение queue(S), предназначенное для обработки потока команд, представленных списком S. Существуют две основные операции с очередью – включение элемента в очередь и исключение элемента из очереди, представляемые структурами enqueue(X) и dequeue(X) соответственно, где Х –интересующий нас элемент.

В программе 15.11 эти операции реализуются абстрактно. Предикат queue (S) обращается к предикату queue (S,Q), где Q первоначально пустая очередь. Предикат queue/2 является интерпретатором для потока команд включения и исключения. Он воспринимает каждую команду и соответственно изменяет состояние очереди. При включении элемента используется незаполненность хвоста очереди; производится конкретизация его новым элементом и соответствующее обновление хвоста очереди. Понятно, что отношения enqueue и dequeue могут быть и неразвернутыми, что дает более выразительную и эффективную, однако более трудную для чтения программу.

queue(S)

S – последовательность операций включения элементов в очередь и исключения элементов из очереди, представленных списком термов enqueue (Х) и dequeue(X).

queue(S)  queue(S,Q\Q).

queue([enqueue(X)|Xs],Q) 

enqueue(X,Q,Q1),queue(Xs,Q1).

queue([dequeue(X)|Xs],Q) 

dequeue(X,Q,Ql),queue(Xs,Ql).

queuc([ ],Q).

enqueue(X.Qh\[X|Qt],Qh\Qt).

dequeue[X,[X | Qh]\Qt,Qh\Qt).

Программа 15.11. Выполнение операций над очередью.

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

queue([ ].Q)  empty(Q).

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

Продемонстрируем использование очередей для раскрытия списка в программе 15.12. Хотя этот пример до некоторой степени надуманный, он ясно показывает, как могут быть использованы очереди.

flatten (Xs,Ys)

Ysраскрытый список, содержащий элементы из списка Xs.

flatten(Xs,Ys)  flatten_q(Xs,Qs\Qs,Ys).

flatten_q([X Xs],Ps\[Xs| Qs],Ys) 

flatten_q(X.Ps\Qs.Ys),flatten_q(X,[Q| Ps]\Qs,[X! Ys]) 

constant(X).X  [ ],flatten_q(Q,Ps\Qs,Ys).

flatten._q([ ].[Q | Ps] \Qs,Ys) 

flatten q(Q.Ps\Qs,Ys).

flatten„q([ ].[ ]\[ ],[ ]).

Программа 15.12. Программа раскрытия списка с использованием очереди.

Основное отношение в программе flatten_q(Ls,Q,Xs), где Lsсписок списков, подлежащий раскрытию, Q – очередь списков, ожидающих раскрытия, и Xs – список элементов в Ls. Первое предложение flatten/3 посредством отношения flatten/2 инициализирует пустую очередь. Базовой операцией являются включение в очередь хвоста списка и рекурсивное раскрытие головы списка:

flatten. q([X|Xs].Q,Ys) enqueue(Xs,Q,Ql),flatten_q(X,Ql,Ys).

Явно pacкрывая отношение enqueue получим

flatten_q([X | Xs],Qh\ [Xs | Qt].Ans)

flatten_q(X.Qh\Qt,Ys).

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

flatten_q(X,[Q|Qh] \Qt,[X | Ys])  constant(X).X  [ ],flatten_q(Q,Qh \,Qt,Ys).

Когда при раскрытии встречается пустой список, либо верхний элемент исключается из очереди

flatten_q([ ].[Q | Qh],Qt,Ys)  flatten_q(Q,Qh\Qt,Ys),

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

Рассмотрим операционный смысл программы 15.11. В процессе предполагаемого использования очереди сообщения enqueue (X) передаются с определенными X, а deqiieue(X) – c неопределенными X. Пока в очередь включено больше элементов, чем из нее исключено, очередь ведет себя нормально; разность между головой очереди и ее хвостом дает элементы, содержащиеся в очереди. Однако, если число полученных команд исключения из очереди превышает число команд включения элементов в очередь, происходит интересное явление – содержимое очереди становится «отрицательным». Голова опережает хвост очереди, что приводит к появлению в очереди отрицательной последовательности неопределенных элементов.

Число таких элементов определяется числом избыточных операций исключения из очереди.

Интересно понаблюдать, как такое поведение согласуется с ассоциативностью присоединения разностных списков. Если очередь Qs\[X1,X2,X3\ Qs], содержащая три неопределенных отрицательных элемента, присоединяется к очереди [а,b,c,d,e\Xs]\Xs, состоящей из пяти элементов, то результатом присоединения будет очередь [d,e\Xs]\Xs с двумя элементами, где отрицательные элементы Х1,Х2,ХЗ инфицируются с а,b,с.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]