Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ФиЛП_материалы / Материалы / Prolog / ПособиеПролог.doc
Скачиваний:
49
Добавлен:
01.06.2015
Размер:
449.02 Кб
Скачать

5.1. Процедуры обработки списков

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

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

В правосторонней рекурсии промежуточные результаты используются на спуске для формирования ответа; при достижении терминальной ситуации он уже готов и нужно только передать его вызывающей функции верхнего уровня. Приведенные в разд. 2 рекурсивные процедуры «предок» и «нод» являются правосторонними.

Процедуры обработки списков основаны на представлении списка в виде [H|T]. В качестве терминального условия обычно используется предикат – имя процедуры с аргументом в виде пустого списка. Так как аргументом рекурсивного вызова является хвост текущего списка, то при конечном исходном списке этот аргумент рано или поздно оказывается пустым списком; очередное сопоставление вызова и терминального предиката становится успешным (подцель, которая является предикатом вызова, становится доказанной) и рекурсивные вызовы прекращаются.

В качестве примера левосторонней рекурсии приведем процедуру вывода на консоль элементов списка. (Встроенный предикат write (Список) выводит список так, как он определен в программе, т.е. с квадратными скобками и, например, с апострофами, если элементы списка – символы).

write_list([]). % Терминальный предикат

write_list([X|T]):- write_list(T), write(X), write(“ “).

При каждом рекурсивном вызове в памяти сохраняется очередной элемент списка X, начиная с первого. В конце концов первая подцель правила становится доказанной: список T – пустой и сопоставление write_list(T) и терминального предиката оказывается успешным. После этого предикат write(X) выводит элементы списка по очереди, начиная с последнего запомненного, т.е. исходный список выводится в инверсном порядке.

В правостороннем варианте рекурсии рекурсивный вызов является последней подцелью правила:

write_list([]).

write_list([X|T]:-write(X), write(“ “),write_list(T).

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

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

Как правосторонняя, так и левосторонняя рекурсия называется простой: в теле процедуры существует только один рекурсивный вызов. В обобщенной рекурсивной схеме вызовов может быть два и более. В частности, рекурсия в программе «Ханойские башни» является обобщенной. Здесь этот тип рекурсии подробно не обсуждается.

Рассмотрим несколько различных процедур обработки списков.

1. Проверка принадлежности элемента Х списку L осуществляется процедурой, построенной по следующим правилам:

а) элемент Х принадлежит списку, если Х является головой списка (терминальное условие);

б) элемент Х принадлежит списку, если Х содержится в списке Т (рекурсивный вызов).

Процедура выглядит следующим образом:

member (X, [ X | T ] ). % терминальный предикат

member (X, [ H | T ]) : - member (X, T).

DOMAINS

list=integer*

name=integer

PREDICATES

member (name,list)

CLAUSES

member (X, [ X | T ] ):-write(X,”Да”).

member (X, [] ):-write(X,”Нет”).

/*Предикаты write необязательны, но без них нет соответствующих сообщений*/

member (X, [ H | T ]) :- member (X, T).

GOAL

member (2,[4,7,2,5]).

Результат

Да

2. Задача присоединения списка L2 к списку L1 выполняется процедурой append(L1, L2, L3), в которой L3 – результирующий список.

Правила построения процедуры:

a) если первый аргумент пустой список, то L3 совпадает с L2;

б) если первый аргумент не пуст, то он выглядит как [X | L1]; результат присоединения – список [X | L3], где исходный L3 получен после копирования L2 в L3.

Процедура выглядит так:

append([ ] , L, L). % терминальный предикат

append ([X|L1], L2, [X|L3 ]):- append (L1, L2, L3).

(Копирование L2 в L3 выполняется терминальным предикатом; после этого на подъеме рекурсии к полученному L3 последовательно добавляются запомненные элементы L1).

Например,

append ([1,2,3], [6,7,8], L).

L= [1,2,3,4,5,6]

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

append (L1, L2, [1, 2, 3]).

В результате получим

L1=[ ] L2=[1, 2,3]

(Традиционный Пролог дает 4 варианта решения:

L1=[ ] L2=[1, 2,3]

L1=[1] L2=[2, 3]

L1=[1, 2] L2=[3]

L1=[a, b, c] L2=[ ]

Для получения только одного необходимо использовать отсечение в терминальном предикате: append( [ ] , L, L):-!.

Обозначим конкретизированный параметр символом ‘i’, а свободный символом ‘o’; тогда способ задания аргументов для первого случая применения append можно записать как (i, i, o), для второго (o, o, i). Возможностью изменять конкретизацию аргументов обладают многие предикаты Пролога. В Visual Prolog 5.1 варианты конкретизации параметров называют flow patterns (шаблон потока) и приводят при объявлении процедур в качестве комментария. В Visual Prolog 7 объявление flow patterns является обязательным.

Эту же процедуру можно использовать для вставки элемента в голову, в хвост или в произвольное место списка. Например, цель для вставки в список [a, b, c, d] элемента ‘f’ после ‘b’ имеет следующий вид.

X='b', L3=[ 'a', 'b', 'c', 'd'], append (L1, [X|L2], L3),

% L1= [a], L3= [c,d]

append (L1, [X], L4), % L4=[a, b]

Y= 'f', append ( L4, [Y], L5), % L5=[a, b, f]

append ( L5, L2, L6). % L6=[a, b, f, c, d]

3. После выполнения процедуры delete ( X, L, L1) список L1 совпадает с L, в котором нет X.

Правила удаления:

а) если Х является головой списка, то результат удаления – хвост списка;

б) если Х находится в хвосте, то он рекурсивно удаляется оттуда.

delete (X, [ X | T ], T ). % (i, i, o), (i, o, i)

delete ( X, [ Y | T ], [ Y | T1 ] ) : - delete ( X, T, T1).

Если в списке находится несколько вхождений X, то в традиционном Прологе процедура удалит их все перебором с возвратом. Например,

delete (a, [a, b, a, a], L).

L=[ b, a, a]

L=[ a, b, a]

L=[ a, b, a]

В соответствии со вторым вариантом задания параметров процедуру delete можно использовать для добавления элементов к списку. Например, если надо вставить элемент 2 в список

[1, 3, 5], то следует задать вопрос, каким должен быть список L, чтобы после удаления из него элемента 2 получился бы этот список.

delete(2, L, [1, 3, 5])

4. Правила выполнения процедуры определения количества элементов Z в списке L – length (L, Z) следующие:

a) если список пустой, то его длина равна 0 – length ( [], 0 );

б) если список не пуст, то его длина равна длине хвоста плюс единица:

length ( [ X | L1 ], Z ) :- length (L1, Y), Z =Y +1.

(Здесь типичная левосторонняя рекурсия; при достижении терминального условия предикаты length ( [], 0 ) и length ( [], Y ) успешно сопоставляются и переменная Y конкретизируется 0; после этого на обратном ходе рекурсии значение Z последовательно увеличивается на 1.)

5. Процедура инверсии списка выглядит следующим образом

reverse ( [ ],[ ]).

reverse( [H | T], R) :-

reverse (T, Rt),

append(Rt, [H], R).

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

min_list ([ H | [ ] ], H). % терминальный предикат

min_list ([ H | T ], Min):- min_list(T, M1), H<M1, Min=H.

min_list ([ H | T ], Min):- min_list(T, M1), H>=M1, Min=M1.

Второй аргумент процедуры – значение минимального элемента. Рекурсивные вызовы заканчиваются в тот момент, когда в текущем списке остается один элемент (последний). При сопоставлении вызова и терминального предиката переменная M1 конкретизируется этим элементом и далее на подъеме рекурсии происходит определение минимального среди запомненных при спуске элементов списка.

Максимальный элемент определяется с помощью хвостовой рекурсивной процедуры max_list (L,N,Max).

max_list ([], Max, Max). % терминальный предикат

max_list ([H|T], N, Max):- H>N, max_list(T,H,Max).

max_list ([H|T], N, Max):- H<=N, max_list(T,N,Max).

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

L=[1,5,3,-6,8,-4], L=[H|T], max_list(T, H, Max).

Объявление доменов и предикатов для обеих процедур выглядит следующим образом:

DOMAINS

list=integer*

PREDICATES

max_list(list, integer, integer)

min_list(list, integer)

В заключение приведем процедуру сортировки элементов списка по методу пузырька. Введем вспомогательное отношение

больше(X, Y) : - X > Y.

Алгоритм сортировки выглядит следующим образом: найти в списке List два смежных элемента таких, что предикат «больше» является истиной и поменять их местами, получив новый список List1; отсортировать список List1. Если в List нет ни одной пары элементов, таких, что предикат «больше» – истина, то считать, что исходный список уже отсортирован и отсортированный список есть SortList.

DOMAINS

list = integer*

PREDICATES

nondeterm пузырек(list, list)

nondeterm переставить(list, list)

больше(integer, integer)

CLAUSES

больше(X, Y):- X > Y.

пузырек(List, SortList):-

переставить(List, List1),пузырек(List1, SortList).

пузырек(SortList, SortList).

переставить([ X, Y | T] , [ Y, X | T] ):-больше(X, Y).

переставить ( [ Z | T] , [ Z | T1 ]):- переставить ( T, T1).

GOAL

пузырек ([1,3,8,4,6], L).

(В традиционном Прологе перед предикатом пузырек в первом правиле надо поставить отсечение для устранения повторных его вызовов.)

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

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

PREDICATES

буква(symbol, char)

CLAUSES

буква (“Согласная”, ‘b’).

буква (“Согласная”, ‘c’).

буква (“Гласная”, ‘a’).

буква (“Гласная”, ‘о’).

GOAL

findall(X, буква(“Согласная”, X), L), write(L).

L=[‘b’, ‘c’]

Предположим, что информация о сотрудниках некоторой организации задана в виде фактов вида «сотрудник(фамилия, адрес, возраст)». Для определения среднего возраста сотрудников можно использовать сопоставление с откатом и с fail для перебора всех фактов, а можно получить с помощью findall соответствующий список, а затем обработать его рекурсивной процедурой.

DOMAINS

адрес, фамилия=string

возраст=integer

list = возраст*

PREDICATES

nondeterm сотрудник(фамилия, адрес, возраст)

подсчет(list, integer, integer)

CLAUSES

сотрудник("Сидоров","ул.Пушкинская 5",50).

сотрудник("Иванов","пр.Платова 10",25).

сотрудник("Семенов","ул.Пупкина 5",50).

сотрудник("Васильев","пр.Петрова 10",37).

сотрудник("Сидоров","пер.Печатников 7",28).

подсчет([ ], N, Ср_возр):- S =Ср_возр/N,

write("Средний возраст=",S).

подсчет([H|T], N, Ср_возр):-

N1=N+1, S1=Ср_возр+H,подсчет(T, N1, S1).

GOAL

findall(Возраст, сотрудник(_, _, Возраст), L),

подсчет(L, 0, 0).

Соседние файлы в папке Prolog