430 likes | 613 Views
Логическое программирование. Факультет Прикладной математики и физики Кафедра Вычислительной математики и программирования Московский авиационный институт (государственный технический университет). Лекция 6. Рекурсивные структуры данных. Списки и деревья. Последовательности.
E N D
Логическое программирование Факультет Прикладной математики и физики Кафедра Вычислительной математики и программирования Московский авиационный институт (государственный технический университет)
Лекция 6 Рекурсивные структуры данных. Списки и деревья.
Последовательности • Программирование замечательно способностью обрабатывать последовательности объектов • В императивном программировании – массив • Как быть в логическом программировании? • Нет переменных с возможностью модификации?
Определение • Списком называется: • пустой список Ø • структурный терм вида Λ(t,l), где t — произвольный терм, l — список, Λ — имя структурного терма, используемого для построения списка. В списке такого вида t называется головой, а l — хвостом списка.
Списки в Прологе • Этот принцип используется для представления списков в языках лог.программирования • Списочный терм обычно называют . (точка), а пустой список – []. L = .(1,.(2,.(3,[]))). L = [1,2,3]. ?- .(H,T) = .(1,.(2,.(3,[]))). H=1; T=[2,3]. ?- .(H,T) = [1,2,3]. H=1; T=[2,3]. ?- [H|T] = [1,2,3]. H=1; T=[2,3]. ?- [A,B|T] = [1,2].
Представление списков деревьями • [[1,[2]],3,[4]] • Что будет головой и хвостом?
Рекурсивная обработка рекурсивной структуры • Структура данных рекурсивна, потому что она определяется через саму себя • Соответственно, для такой структуры хорошо подходит рекурсивная обработка с pattern matching
Пример • Определение суммы элементов списка • sum([1,2,3],X). sum([X],X). sum([X|T],S) :- sum(T,S1), S is S1+X. :- func sum(list(int))=int. :- mode sum(in) = out is det. sum([])=0. sum([X|T]) = X+sum(T). sum([X],X). sum([X|T],S) :- sum(T,S1), S = S1+X.
Определение длины списка length(List,N) :- func length(list(T)) = int. :- mode length(in) = out is det. length([]) = 0. length([_|T]) = length(T)+1. length([],0). length([_|T],N) :- length(T,N1), N is N1+1. • В описании типа использован полиморфизм! • Предикат length не может использоваться для генерации списка переменных заданной длины • Упражнение: реализуйте предикат генерации списка заданной длины.
Принадлежность элемента списку member(X,List) :- pred member(T,list(T)). :- mode member(in,in) is semidet. :- mode member(out,in) is nondet. member(X,[X|_]). member(X,[_|T]) :- member(X,T). member(X,[X|_]). member(X,[_|T]) :- member(X,T). • member используется не только для проверки, но и для генерации последовательности значений, для «вставки» значения на место в списке-шаблоне • member(X,[1,2,3]), write(X), fail. • L = [_,_,_], member(1,L), member(2,L), member(3,L).
member для генерации значений member(X,[X|_]). member(X,[_|T]) :- member(X,T).
Удаление элемента из списка remove(X,List,Result) :- pred remove(T,list(T),list(T)). :- mode remove(in,in,out) is nondet. :- mode remove(out,in,out) is nondet. :- mode remove(in,out,in) is multi. remove(X,[X|T],T). remove(X,[H|T],[H|R]) :- remove(X,T,R). remove(X,[X|T],T). remove(X,[H|T],[H|R]):- remove(X,T,R). • Удаляется лишь одно вхождение элемента • Может использоваться для генерации и проверки вхождения, например member(X,L) :- remove(X,L,_). • Упражнение: постройте предикат remove_all удаления всех вхождений элемента из списка • Упражнение: постройте предикат remove, который в случае, если элемента нет в списке, возвращает исходный список.
remove для удаления ?-remove(X,[a,b,b,a],L). ?
Конкатенация списков append(A,B,C) :- pred append(list(T),list(T),list(T)). :- mode append(in,in,out) is det. :- mode append(out,out,in) is multi. append([],L,L). append([X|T],L,[X|R]) :- append(T,L,R). append([],L,L). append([X|T],L,[X|R]):- append(T,L,R).
Append для конкатенации append([],L,L). append([X|T],L,[X|R]):- append(T,L,R).
Append для генерации пар append([],L,L). append([X|T],L,[X|R]):- append(T,L,R).
Другие возможные варианты использования append • Для выделения первых/последних n элементов (rem_firstn/rem_lastn) • Для отделения/присоединения последнего элемента списка (last) • Для определения «соседних»элементов списка (next(A,B,L)) • Для выделения подсписка (sublist)
Варианты использования append-решения rem_first(N,L,R) :- append(X,L,R), length(X,N). last(X,L) :- append(_,[X],L). next(A,B,L) :- append(_,[A,B|_],L). sublist(R,L) :- append(_,T,L), append(R,_,T).
Построение перестановок permute(L,R) :- pred permute(list(T),list(T)). :- mode permute(in,in) is semidet. :- mode permute(in,out) is nondet. :- mode permute(out,in) is nondet. permute([],[]). permute(L,[X|T]) :- remove(X,L,R), permute(R,T). permute([],[]). permute(L,[X|T]) :- remove(X,L,R), permute(R,T). • Может использоваться для проверки перестановочности или генерации всех перестановок • При использовании для генерации первая переменная должна быть конкретизирована, иначе – процедурное зацикливание (проверьте!) • fact(N,F) :- gen_list(1,N,L), find_all(L1,permute(L,L1),R), length(R,F).
Функции высших порядков • Функции sum, mult, max и т.д. являются частным случаем более общей логики применения некоторой функции ко всем элементам списка. • Это нельзя формализовать в логике первого порядка, но возможно, если допустить возможность передачи предиката в качестве параметра. • call позволяет вызвать предикат, переданный в качестве параметра
Свертка списка: fold fold(List,Pred, Initial, Result) :- func fold(list(T),func(T,T)=T is det,T)=T is det. :- mode fold(in,in,in,out) is det. fold([],F,I) = I. fold([X|T],F,I) = F(fold(T,F,I),X). fold([],P,I,I). fold([X|T],P,I,R) :- fold(T,P,I,R1), call(P,R1,X,R). plus(X,Y,Z) :- Z is X+Y. mult(X,Y,Z) :- Z is X*Y. max(X,Y,Z) :- X>Y,Z=X. max(X,Y,Z) :- X=<Y,Z=Y. sum(L) = fold(L,(+),0). mult(L) = fold(L, lambda((X::in,Y::in)=Z::out is det, Z is X*Y),1)). sum(L,R) :- fold(L,plus,0,R). mult(L,R) :- fold(L,mult,1,R). max(L,R) :- fold(L,max,-1000,R).
Отображение списка и фильтрация: map и filter map(List, Pred, Result) filter(List, Pred, Result) :- func map(list(T),func(T)=T is det) =list(T) is det. :- mode map(in,in)=out is det. map([],_) = []. map([X|T],F) = [F(X)|map(T,F)]. map([],_,[]). map([X|T],P,[Y|R]) :- call(P,X,Y), map(T,P,R). filter([],_,[]). filter([X|T],P,[X|R]) :- call(P,X), filter(T,P,R). filter([X|T],P,R) :- \+call(P,X), filter(T,P,R).
Для разбора на семинаре • Сортировки списков • Вставка, выборка, пузырьковая, Хоара • Генерация списков • Одинаковых элементов • 1..N • Всех списков заданной длины с элементами из заданного множества • Выбор n-го элемента
Хвостовая рекурсия • В императивных языках многие операции со списками не требуют дополнительной памяти (длина списка, ...) • Во многих случаях (особенно при обработке списков) рекурсивные алгоритмы могут быть сведены к итерационным • Такая рекурсия называется хвостовой • Линейная • Рекурсивный вызов – в конце тела функции • т.е. вызов совершается сразу после выполнения тела функции • Не выделяется промежуточная память
Нехвостовая рекурсия length([],0). length([_|T],N) :- length(T,N1), N is N1+1.
Сведение length к хвостовой рекурсии За счет введения дополнительного аргумента (аккумулятора) удается проводить все вычисления (+1) до рекурсивного вызова, и рекурсия сводится в хвостовой. length(L,N) :-length(L,0,N). length([X|T],S,N) :- S1 is S+1, length(T,S1,N). length([],N,N).
Реверсирование списка reverse([],[]). reverse([X|T],L) :- reverse(T,R), append(R,[X],L).
Приводим к хвостовой рекурсии reverse(L,R) :- reverse(L,[],R). reverse([],R,R). reverse([X|T],L,R):- reverse(T,[X|L],R).
Разностные списки • Декларативный список reverse(L,L1,L2) – список L2 получен из L1 добавлением в конец реверса списка L • Пара <L2,L1> называется разностным представлением списка, или разностным списком • Разностные списки удобно записывать в операторной нотации L2/L1 • Разностные списки часто бывают недоконкретизированными reverse(L,R) :- rev(L,R/[]). rev([],X/X). rev([X|T],R/S) :- rev(T,R/[X|S]).
Разностный append • Рассмотрим разностные списки X/T и T/Y. • Очевидно, что dappend(X/T,T/Y,X/Y). ?-dappend([1,2|X]/[X],[3,4|Y]/Y,R/[]). • И это работает! • Append сводится только к унификации!
Порядковое представление списков • В математике последовательность логично представлять функцией int -> T • Аналогичное представление в логике может задаваться перечислением: list(l1,1,a). list(l1,2,b). list(l1,3,c). L1 = [elem(1,a),elem(2,b),elem(3,c)]. • Некоторые операции оказываются эффективнее в порядковом представлении • Особенно полезно для представления матриц, в т.ч. разреженных
Представление матриц • Удобно для разреженных матриц, при этом надо отдельно хранить размерность • Когда использовать то или иное представление? • Пример: транспонирование матрицы
Деревья • Деревья являются для логического программирования «родной» структурой данных • Однако не деревья общего вида, поскольку арность функторов, описывающих дерево, должна быть фиксированной 1 2 3 4 5 6 7
Сведение дерева общего вида к двоичному • Любое дерево общего вида может быть преобразовано к двоичному 1 1 2 2 3 4 5 3 5 6 7 6 4 7
+ 6 * 3 3 7 1 2 1 4 Двоичные деревья • Двоичным деревом называется: • пустое дерево Ø; • структурный терм вида T(t,l,r), где t — произвольный терм (узел дерева), l, r — двоичные деревья, называемые соответственно левым и правым поддеревьями.
+ 6 * 3 3 7 1 2 1 4 Представление двоичных деревьев T = +(*(1,2),3). T = t(6,t(3,t(1,nil,nil), t(4,nil,nil)), t(7,nil,nil). Значение узла задается одним из аргументов функутора Значение узла задается функтором (небольшое число узловых значений)
Применение двоичных деревьев • Деревья поиска • Способ представления упорядоченных данных в памяти с эффективными алгоритмами добавления/удаления элемента и поиском • В неявном виде используются при определении структур данных типа ассоциативных таблиц, при индексировании и т.д. • Деревья выражений • Представление арифметических выражений с бинарными операторами
6 3 7 1 4 Дерево поиска • Все вершины левого поддереваменьше х • Все вершины правого поддерева больше х • Дерево поиска типа T, на котором определен полный порядок < - это двоичное дерево, для каждой вершины x которого
Вставка в дерево поиска :- type tree(T) --> nil;tree(T,tree(T),tree(T)). :- func add(int,tree(int)) = tree(int). :- mode add(in,in) = out is det. add(X,nil) = tree(X,nil,nil). add(X,tree(Y,L,R)) = tree(Y,L1,R1):- X>Y --> L1=L, R1=add(X,R); X<Y --> L1=add(X,L), R1=R; L1=L, R1=R.
Сортировка списка :- func to_tree(list(int)) = tree(int). :- mode to_tree(in) = out is det. to_tree(L) = T :- to_tree(L,nil,T).:- pred to_tree(list(int), tree(int), tree(int)). :- mode to_tree(in, in, out) is det. to_tree([],R,R). to_tree([X|L],T,R) :-to_tree(L,add(X,T),R). :- func from_tree(tree(int)) = list(int). :- mode from_tree(in) = out is det. from_tree(nil) = []. from_tree(tree(X,L,R)) = Z :- append(from_tree(L),[X|from_tree(R)],Z). :- func tsort(list(int)) = list(int). :- mode tsort(in) = out is det. tsort(L) = from_tree(to_tree(L)).
Мораль • Рекурсивные структуры данных – часто используемый удобный механизм хранения данных в логическом программировании • Списки – гармоничная часть языка • Прозрачный синтаксис (специальный синтаксис конструкторов) • Во многих реализациях – встроенные библиотечные функции • Деревья (в особенности двоичные) для языков логического программирования являются базовой структурой • Применения деревьев: • Деревья выражений • Деревья поиска