МИНИСТЕРСТВО ОБРАЗОВАНИЯ РЕСПУБЛИКИ БЕЛАРУСЬ МИНИСТЕРСТВО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ Учреждение образования Белорусско – Российский университет Кафедра «АСУ» Лабораторная работа №5 на тему «Рекурсивные структуры данных. Деревья » Могилев 2009 Оглавление 1 Цель лабораторной работы ..................................................................................................... 3 1.1 Содержание отчета ........................................................................................................... 3 2 Общие сведения ....................................................................................................................... 3 2.1 Деревья как типы данных ................................................................................................ 3 2.2 Обход дерева ..................................................................................................................... 5 2.2.1 Листинг программы ch06e09.pro .............................................................................. 6 2.3 Создание дерева ................................................................................................................ 6 2.3.1 Листинг программы ch06e10.pro .............................................................................. 7 3 Бинарные поисковые деревья ................................................................................................. 8 3.1 Сортировка на основе дерева ................................................................................................................................................. 10 3.1.1 Листинг программы ch06e11.pro ............................................................................... 10 4 Упражнения ............................................................................................................................ 11 4.1 Упражнение ..................................................................................................................... 11 4.2 Листинг программы ch06e08.pro 4.3 Упражнение на поиск с возвратом ....................................................................... 12 5 Задания к лабораторной работе ............................................................................................ 13 5.1 Контрольные вопросы .................................................................................................... 13 5.2 Практические задания .................................................................................................... 13 1 Цель лабораторной работы Изучить рекурсивные типы данных. 1.1 Содержание отчета - Тема и цель работы. - Листинг программы. Общие сведения Рекурсивными могут быть не только предложения, но и структуры данных. Пролог является единственным широко используемым языком программирования, который позволяет просто определить типы рекурсивных данных. Тип данных является рекурсивным, если он допускает структуры, содержащие такие же структуры, как и они сами. Наиболее важным рекурсивным типом данных является список, хотя он и не выглядит непосредственно рекурсивной конструкцией. Структурой рекурсивного типа данных является дерево (рис. 13.1). Важно, что каждая ветвь дерева сама является деревом, поэтому структура рекурсивна. 2 2.1 Деревья как типы данных Visual Prolog позволяет определить, используя деревья как типы данных, рекурсивные типы, в которых указатели создаются и обрабатываются автоматически. Например, можно определить дерево следующим образом: domains treetype = tree(string, treetype, treetype) Эта декларация говорит о том, что дерево записывается как функтор tree, аргументами которого являются строка и два других дерева. Но это не совсем удовлетворительная декларация, т. к. нет способа закончить рекурсию. В действительности дерево не может распространяться до бесконечности. Некоторые узлы не имеют связей с последующими деревьями. В Pascal это можно выразить, присвоив некоторым указателям специальное нулевое значение, но в Прологе нет доступа к указателям. Решение состоит в том, чтобы определить два типа деревьев — обычное и пустое. Это достигается тем, что дерево может иметь один из двух функторов tree с тремя аргументами или empty без аргументов. domains treetype = tree (string, treetype, treetype); empty Заметьте, что названия tree (функтор, у которого три аргумента) и empty (функтор без аргументов) создаются программистом, и ни одному из них нет предопределенного в Прологе значения. С тем же успехом можно использовать ххх и ууу. Вот как дерево, представленное на рис. 13.1, будет описано в Пролог-программе: 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.2 Обход дерева Одной из наиболее часто осуществляемых операций с деревом является исследование всех узлов и обработка их некоторым образом, либо поиск некоторого значения, либо сбор всех значений. Эти процедуры известны как обход дерева. Основной алгоритм для этого следующий: 1 . Если дерево пусто, то ничего не делать. 2. Иначе, обработать текущее значение, затем перейти на левое поддерево, затем перейти на правое поддерево. Как и само дерево, алгоритм является рекурсивным: он обрабатывает левое и правое поддеревья так же, как и исходное дерево. В Прологе он выражается двумя предложениями: одно для пустого, а другое для непустого дерева. traverse(empty). % ничего не делать traverse(tree(X, Y, Z)) :do_something_with_X, traverse (Y) , traverse(Z). Этот алгоритм известен как поиск "сначала — вглубь", т. к. он спускается по каждой ветви вниз, насколько возможно, прежде чем вернуться вверх для обхода другой ветви (рис. 13.2). Чтобы посмотреть алгоритм в действии, изучите программу ch06e09.pro (листинг 13.9), которая обходит дерево и печатает все элементы, которые ей попадаются. Дерево, показанное на рис. 13.1 и 13.2, программа ch06e09.pro распечатает следующим образом: Cathy Michael Charles Hazel Melody Jim Eleanor Конечно, вы можете легко приспособить программу для выполнения каких-то других операций над элементами, а не для их печати. 2.2.1 Листинг программы ch06e09.pro /* Обход дерева "сначала—вглубь" и печать каждого элемента, который попадается на пути */ 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)))). Поиск "сначала — вглубь" очень похож на способ, которым Пролог осуществляет поиск в базе знаний. Он организует предложения в дерево и проходит каждое поддерево, пока предложения не закончатся. Вы могли бы описать дерево посредством набора правил Пролога, таких как: father_of ("Cathy", Michael") . mother_of("Cathy", "Melody"). father_pf ("Michael", "Charles") . mother_of ("Michael", "Hazel") . … Это было бы предпочтительнее, если бы дерево предназначалось только для выражения родственных связей между людьми. Но такой вид описания делает невозможным обработку всего дерева как единой, сложной структуры данных. Как вы увидите, сложные структуры данных бывают весьма полезными, т. к. они упрощают сложные вычислительные задачи. 2.3 Создание дерева Один из способов создания дерева — это вложенная структура из функторов и аргументов, как в предыдущем примере ch06e09.pro. Однако в общем случае Пролог создает дерево путем вычисления. На каждом шаге пустое поддерево заменяется непустым в процессе унификации (сопоставления по аргументам).Создание дерева из одного узла путем присвоения обычных данных тривиально: create_tree(N, tree(N, empty, empty)). Здесь говорится: "Если N — данное, то tree (N, empty, empty) — это дерево из одного узла, содержащее его". Построение структуры дерева так же просто. Следующей процедуре нужны три дерева в качестве аргументов. Она вставляет первое дерево в качестве левого поддерева во второе дерево, и результат этого присваивает третьему дереву: insert_left(X, tree(А, _, В), tree(А, X, В ) ). Заметьте, что в этом предложении нет тела, т. е. нет четких шагов при его выполнении. Все, что должен сделать компьютер, — это соединить аргументы друг с другом в правильном порядке, — и работа закончена. Предположим, что вы хотите вставить tree ("Michael", empty, empty) в качестве левого поддерева для tree ("Cathy", empty, empty). Чтобы это сделать, надо выполнить целевое утверждение: insert_left(tree("Michael", empty, empty), tree("Cathy", empty, empty),T) Тогда T примет значение: tree("Cathy", tree("Michael", empty, empty), empty). Это дает способ построения дерева шаг за шагом. Этот же способ показан в программе ch06e10.рго (листинг 13.10). В действительности элементы, которые надо добавить к дереву, могут приходить с внешнего входа. 2.3.1 Листинг программы ch06e10.pro */Простые процедуры построения дерева create_tree(А, В) помещает А в поле данных одноузлового дерева В, insert_left(А, В, С) вставляет А как левое поддерево В и присваивает результат С, insert_right(А, В, С) вставляет А как правое поддерево В и присваивает результат С */ domains treetype = tree(string,treetype,treetype); empty() predicates create_tree(string,treetype) insert_left(treetype,treetype,treetype) insert_right(treetype, treetype, treetype) run clauses create_tree(A,tree(A,empty,empty)). insert_left (X, tree (A,_,B) ,tree (A, X, B) ) . insert_right (X, tree (A, B,_) ,tree(A,B,X) ) . run:create_tree("Charles",Ch), create_tree("Hazel",H), create_tree("Michael",Mi), create_tree("Jim",J), create_tree("Eleanor",E) , create_tree("Melody",Me), create_tree("Cathy",Ca), insert_left (Ch, Mi, Mi2), insert_right(H, Mi2, Mi3), insert_left(J, Me, Me2), insert_right(E, Me2,Me3), insert_left(Mi3, Ca, Ca2), insert_right(Me3, Ca2, Ca3), write(Ca3, '\n'). goal run. Заметьте, что в Прологе нет возможности изменить значение переменной после того, как присвоение произошло. Поэтому в программе ch06e10.рго используется так много имен переменных; каждый раз, когда нужно получить новое значение, требуется новая переменная. Но обычно большое число имен переменных не нужно, т. к. в общем случае повторяющиеся процедуры получают новые переменные, вызывая себя рекурсивно, и каждый вызов имеет определенный набор переменных. Обратите внимание, что утилита Test Goal имеет ограничение на количество переменных (<12), используемых в целевом утверждении (раздел goal), поэтому следует использовать предикат "оболочку" run. 3 Бинарные поисковые деревья Итак, мы использовали дерево для представления отношений между элементами. Это, конечно, не самый лучший вид использования деревьев, т. к. предложения Пролога могут выполнить такую же работу. Но деревья можно использовать и иначе. В деревьях имеется возможность хранить данные так, что их можно быстро отыскать. Дерево, построенное для такой цели, называется поисковым деревом. С точки зрения пользователя, сама структура дерева не несет информации, дерево — это боке быстрая альтернатива списку или массиву. Вспомним, что при обходе обычного дерева вы рассматриваете текущий узел, а затем оба его поддерева. Чтобы найти определенное значение, вы должны рассмотреть каждый узел всего дерева. !Замечание Время, затрачиваемое на поиск в обычном дереве из N элементов, в среднем пропорционально N.! Бинарное поисковое дерево строится таким образом, что, глядя на любой узел, можно предсказать, в каком из его узлов находится заданное значение. Это делается заданием отношения порядка между значениями, таким как алфавитный или пронумерованный порядок. Значения в левом поддереве предшествуют значению в текущем узле, а в правом — следуют после значения в текущем узле (рис. 13.3). Заметьте, что те же имена, установленные в ином порядке, дадут другое дерево, и хотя в дереве десять имен, любое из них можно найти максимум за пять шагов. Как только вы посмотрите во время поиска на узел бинарного поискового дерева, вы можете исключить из рассмотрения половину оставшихся узлов и провести поиск очень быстро. Если теперь размер дерева увеличить вдвое, то для поиска потребуется только один дополнительный шаг. !Замечание Время, требуемое для поиска значения на бинарном поисковом дереве, в среднем пропорционально log2N (а в действительности пропорционально logN по любому основанию).! Чтобы построить дерево, нужно начать с пустого дерева и добавлять к нему значения одно за другим. Процедура добавления значения такая же, как при поиске: необходимо просто найти место, где это значение должно находиться, и вставить его туда. Этот алгоритм заключается в следующем: 1 . Если текущий узел есть пустое дерево, то вставить в него значение. 2. Иначе, сравнить значение, которое необходимо вставить, со значением в текущем узле. Вставить значение в левое или правое поддерево, в зависимости от результата сравнения. В Прологе этому алгоритму нужно три предложения — по одному на каждый случай. Первое предложение таково: insert(NewItem, empty, tree(NewItem, empty, empty):- !. На естественный язык эта запись переводится так: "Результатом вставки Newltem (нового значения) в empty (пустое дерево) будет дерево tree(Newltem, empty, empty)". Восклицательный знак (! — отсечение) означает, что если это предложение можно успешно применить, то другие предложения проверять не надо. Второе и третье предложения осуществляют вставку в непустые деревья: insert(NewItem, tree(Element, Left, Right), tree(Element, NewLeft, Right) :Newltem < Element,!, insert(Newltem, Left, NewLeft). insert(NewItem, tree(Element, Left, Right), tree(Element, Left, NewRight):insert(NewItem, Right, NewRight). Если NewItem < Element, то вы вставляете его в левое поддерево, а если иначе, то вставляете его в правое поддерево. Заметьте, как много работы мы выполняем проверкой соответствия аргументов в голове правила. 3.1 Сортировка на основе дерева После того, как дерево построено, можно легко переставить все его элементы в алфавитном порядке. Алгоритм для этого — вновь вариант поиска "сначала — вглубь": 1.Если дерево пустое, то ничего не делать. 2. Иначе, переставить все элементы левого поддерева, потом текущий элемент, за тем все элементы правого поддерева. Или на Прологе: retrieve_all(empty). % Ничего не делать retrieve_all(tree(Item, Left, Right)) :retrieve_all(Left), do_something_to(Item), retrieve_all(Right). Сортировку следующих друг за другом значений можно выполнить, вставляя их в дерево, и затем переставляя их по порядку. Для N значений это займет время, пропорциональное N log N, т. к. вставка и перестановка занимают время, пропорциональное log N, причем то и другое должно быть выполнено N раз. Это самый быстрый сортирующий алгоритм, известный на сегодня. Программа ch06ell.pro (листинг 13.11) применяет описанный способ для расстановки по алфавиту вводимых символьных значений. В этом примере используются некоторые стандартные предикаты Visual Prolog, которые мы не встречали ранее. Эти предикаты будут подробно обсуждаться в последующем. 3.1.1 Листинг программы ch06e11.pro domains chartree = tree(char, chartree, chartree); end predicates nondeterm do(chartree) action(char, chartree, chartree) create_tree(chartree,chartree) insert(char,chartree, chartree) write_tree(chartree) nondeterm repeat clauses do(Tree):-repeat,nl, write("*****"),nl, write("Enter 1 to update tree\n"), write("Enter 2 to show tree\n"), write("Enter 7 to exit\n"), write("*****************************************************"), nl, write("Enter number — " ) , readchar(X),nl, action(X, Tree, NewTree), do(NewTree). action('1',Tree,NewTree):write("Enter characters or # to end: " ) , create_Tree(Tree, NewTree). action('2' ,Tree,Tree):write_Tree(Tree), write("\nPress a key to continue"), readchar(_),nl. action('7', _, end):exit. create_Tree(Tree, NewTree):readchar(C), C<>'#', !, write(C, " " ) , insert(C, Tree, TempTree), create_Tree(TempTree, NewTree). create_Tree(Tree, Tree). insert(New,end,tree(New,end,end)):!. insert(New,tree(Element,Left,Right),tree(Element,NewLeft,Right)):- New<Element, !, insert(New,Left,NewLeft). insert(New,tree(Element,Left,Right),tree(Element,Left,NewRight)):- insert(New,Right,NewRight). write_Tree(end). write_Tree(tree(Item,Left,Right)):write_Tree(Left), write(Item, " "), write_Tree(Right). repeat. repeat:-repeat. goal write("*************** Character tree sort ***************+***"),nl, do(end). 4 Задания к лабораторной работе 4.1 Контрольные вопросы 1. Какие структуры могут быть рекурсивными? 2. Основной алгоритм обхода дерева? 3. Для какой цели строится поисковое дерево? 4. Чему равно время, затрачиваемое на поиск в обычном дереве и бинарном? 5. Алгоритм добавления элемента в бинарное дерево. 4.2 Практические задания. 1. На основе дерева описанного в листинге ch06e10.pro выполнить один из вариантов заданий: Варианты заданий: Справочник адресов Каталог автомобилей Прайс-лист Спортивные соревнования Кинотеатры Персональные компьютеры Программное обеспечение 8. Домашняя библиотека 1. 2. 3. 4. 5. 6. 7. 2. На основе алгоритма использованного в программе в листинге ch06e09.pro выведите все элементы в созданном вами дереве. 3. Загрузите и запустите программу ch06e11.pro. Выполните сортировку вводимой символьной последовательности. В отчете приведите предикат обработки вводимых символов и поясните алгоритм его работы. Измените символ используемый для окончания ввода символов.