Лекция - структуры данных языа Пролог

advertisement
1 Списки
1.1 Понятие списка
В Прологе список — это объект, который содержит конечное число
других объектов. Списки можно грубо сравнить с массивами в других
языках, но, в отличие от массивов, для списков нет необходимости
заранее объявлять их размер.
Конечно, есть другие способы объединить несколько объектов в один.
Если число объектов заранее известно, то вы можете сделать их
аргументами одной составной структуры данных. Если число объектов не
определено, то можно использовать рекурсивную составную структуру
данных, такую как дерево. Но работать со списками обычно легче.
Список, содержащий числа 1, 2 и 3, записывается так:
[1, 2, 3]
Каждая составляющая списка называется элементом. Чтобы
оформить списочную структуру данных, надо отделить элементы списка
запятыми и заключить их в квадратные скобки.
1.2 Объявление списков
Чтобы объявить домен для списка целых, надо использовать
декларацию домена, такую как:
domains
Integerlist = integer*
Символ (*) означает "список чего-либо"; таким образом, integer*
означает "список целых".
Элементы списка могут быть любыми, включая другие списки.
Однако все его элементы должны принадлежать одному домену.
Декларация домена для элементов должна быть следующего вида:
domains
elementslist = elements*
elements = ....
Здесь elements имеют единый тип (например: integer, real или symbol)
или являются набором отличных друг от друга элементов, отмеченных
разными функторами.
1.3 Головы и хвосты
Список является рекурсивным составным объектом. Он состоит из
двух частей - головы, которая является первым элементом, и хвоста,
который является списком, включающим все последующие элементы.
Хвост списка — всегда список, голове списка — всегда элемент.
Например:
голова [а, b, с] есть а
хвост [а, b, с] есть [b, с]
Что происходит, когда вы доходите до одноэлементного списка?
Ответ таков:
голова [с] есть с
хвост [с] есть []
Если выбирать первый элемент списка достаточное количество раз,
вы обязательно дойдете до пустого списка [ ]. Пустой список нельзя
разделить на голову и хвост.
1.4 Работа со списками
В Прологе есть способ явно отделить голову от хвоста. Вместо
разделения элементов запятыми, это можно сделать вертикальной чертой
"|". Например:
[а, b, с] эквивалентно [а | [b, с]] и. продолжая процесс,
[а | [b, с] ] эквивалентно [а | [b] [с] ] ], что эквивалентно [а | [b | [с | [ ] ]
]]
Можно использовать оба вида разделителей в одном и том же списке
при условии, что вертикальная черта есть последний разделитель. При
желании можно набрать [a, b, с, d] как [а, b | [с, d]].
В таблице 2.1 приведено несколько примеров на присвоение в
списках.
Таблица 2.1 Присвоение в списках
Список 1
Список 2
Присвоение переменным
[X, У, Z]
[7]
[1, 2, 3,
4]
[эгберт, ест,
мороженое]
[X | У]
Х=эгберг, У=ест,
Z=мороженое
Х=7, У=[]
[X, У |
Х=1, У=2,
Z]
Z=[3,4]
1.4.1 Печать списков
Если нужно напечатать элементы списка, это делается рекурсивно:
domains
list=integer*
predicates
write_a_list(list)
clauses
write_a_list([]). % если список пустой – ничего не делать
write_a_list([H|T]) :- % присвоить H – голова, T – хвост, затем…
write(H), nl,
2
write_a_list(T).
goal
write_a_list([1,2,3]).
1.4.2 Подсчет элементов списка
Рассмотрим, как можно определить число элементов в списке. Что
такое длина списка? Вот простое логическое определение:
Длина [] — 0.
Длина любого другого списка — 1 плюс длина его хвоста.
Можно ли применить это? В Прологе — да. Для этого нужны два
предложения:
domains
list=integer*
% или любой другой тип
predicates
length_of(list,integer)
clauses
length_of([],0).
length_of([_,T],L):- length_of(T,TailLength), L = TailLength+1.
2 Деревья как типы данных
Структурой вводимого нами типа данных является дерево (рис2.1).
Каждая ветвь дерева сама является деревом, поэтому структура
рекурсивна.
Рис. 2.1 Часть фамильного дерева
Рекурсивные типы популяризировались Никлаусом Виртом,
изобретателем языка программирования Pascal. Он не применял в Pascal
рекурсивные типы, но определил, какими они должны быть. Но если бы в
Pascal они все-таки были, то можно было бы определить дерево
наподобие следующего:
tree = record /* Некорректно в Pascal! */
3
name: string[80];
left, right: tree
end.
На естественный язык этот фрагмент переводится так: «Дерево
состоит из имени (Name), которое есть строка (string), а также левого и
правого поддеревьев, которые тоже являются деревьями». Однако в Pascal
можно написать только следующим образом:
treeptr = ^tree;
tree = record
name: string[80];
left, right: treeptr
end.
Заметьте существенное различие: этот фрагмент имеет дело с
представлением дерева в памяти, а не с собственно структурой дерева.
Он обращается с деревом как с объектом, состоящим из узлов, каждый из
которых содержит некоторые данные и указатели на два других узла.
Пролог позволяет определить действительно рекурсивные типы, в
которых указатели создаются и обрабатываются автоматически.
Например, можно определить дерево следующим образом:
domains
treetype = tree(string, treetype, treetype)
Эта декларация говорит о том, что дерево записывается как функтор
tree, аргументами которого являются строка и два других дерева.
Но это не совсем удовлетворительная декларация, т. к. нет способа
закончить рекурсию. В действительности дерево не может
распространяться до бесконечности. Некоторые узлы не имеют связей с
последующими деревьями. В Pascal это можно выразить, присвоив
некоторым указателям специальное нулевое значение, но в Прологе нет
доступа к указателям. Решение состоит в том, чтобы определить два типа
деревьев – обычное и пустое. Это достигается тем, что дерево может
иметь один из двух функторов tree с тремя аргументами или empty без
аргументов.
domains
treetype = tree(string, treetype, treetype); empty
Заметьте, что названия tree (функтор, у которого три аргумента) и
empty (функтор без аргументов) создаются программистом, и ни одному
из них нет предопределенного в Прологе значения. С тем же успехом
можно использовать ххх и ууу. Вот как дерево, представленное на
рисунке 1, будет описано в Пролог-программе:
4
tree("Cathy",
tree("Michael",
tree("Charles", empty, empty),
tree("Hazel", empty, empty)),
tree ("Melody",
tree("Jim", empty, empty),
tree("Eleanor", empty, empty)))
Для удобства чтения программа имеет подразделы. Но в Прологе не
требуется такого подразделения, и деревья, при нормальной записи, не
требуется подразделять. Точно такая же структура будет составлена
другим способом:
tree("Cathy", tree("Michael", tree("Charles", empty, empty), tree("Hazel",
empty, empty)), tree("Melody", tree("Jim", empty, empty), tree("Eleanor",
empty, empty)))
Заметьте, что это не предложение Пролога, а лишь сложная структура
данных.
2.1 Обход дерева
Одной из наиболее часто осуществляемых операций с деревом
является исследование всех узлов и обработка их некоторым образом,
либо поиск некоторого значения, либо сбор всех значений. Эти процедуры
известны как обход дерева. Основной алгоритм для этого следующий:
1. Если дерево пусто, то ничего не делать.
2. Иначе, обработать текущее значение, затем перейти на левое
поддерево, затем перейти на правое поддерево.
Как и само дерево, алгоритм является рекурсивным: он обрабатывает
левое и правое поддеревья так же, как и исходное дерево. В Прологе он
выражается двумя предложениями: одно для пустого, а другое для
непустого дерева.
traverse (empty).
% ничего не делать
traverse (tree (X, Y, Z)) :do_something_with_X,
traverse(Y),
traverse(Z).
5
Рис. 2.2 - Обход дерева "сначала – вглубь"
Этот алгоритм известен как поиск "в глубину", т.к. он спускается по
каждой ветви вниз, насколько возможно, прежде чем вернуться вверх для
обхода другой ветви.
Рассмотрим программу, которая обходит дерево и печатает все
элементы, которые ей попадаются.
domains
treetype = tree(string, treetype, treetype); empty()
predicates
traverse(treetype)
clauses
traverse(empty).
traverse(tree(Name, Left, Right)) :write(Name,'\n'),
traverse(Left),
traverse(Right).
goal
traverse(tree("Cathy",
tree("Michael",
tree("Charles", empty, empty),
tree("Hazel", empty, empty)),
tree("Melody",
tree("Jim", empty, empty),
tree("Eleanor", empty, empty)))).
6
Download