Тема Корневые деревья Лекция 18. 11. 13 г. 1
Представление корневых деревьев Понятие корневое дерево объединяет в себе различные «нелинейные» структуры данных, для узлов которых определены не только направления перемещения к другим узлам «вправо» и «влево» , но и «вверх» , «вниз-влево» , «вниз-вправо» и др. Общим признаком «древовидных» структур является наличие единственного корня – узла, для которого указатель «вверх» всегда равен NULL. Узел корневого дерева, как и узел связанного списка, хранит «полезную» информацию и некоторое множество указателей, количество и назначение которых зависит от вида дерева. Лекция 18. 11. 13 г. 2
Бинарные деревья Бинарное дерево – это разновидность корневого дерева, каждый узел которого (кроме корня) имеет единственного предка и не более двух потомков. NULL 5 10 elem left right 20 p 1 3 elem 4 p elem left right typedef struct Tree 2 { double elem; struct Tree 2 *p, *left, *right; } Tree 2; N right p p p elem N N N Tree 2* init_T 2(double z) { Tree 2 *t = (Tree 2*)calloc(1, sizeof(Tree 2)); t->elem = z; t->p = t->left = t->right = NULL; return t; } Лекция 18. 11. 13 г. 3
Включение узла в бинарное дерево Рассмотрим две функции, позволяющие включить новый узел в бинарное дерево в качестве потомка заданного узла и последовательность их вызова для формирования дерева из приведенного выше примера. void Insert_left_T 2(Tree 2 *t, double z) { Tree 2* q = (Tree 2*)calloc(1, sizeof(Tree 2)); t->left = q; q->elem = z; q->p = t; q->left = q->right = NULL; } void Insert_right_T 2(Tree 2 *t, double z) { Tree 2* q = (Tree 2*)calloc(1, sizeof(Tree 2)); t->right = q; q->elem = z; q->p = t; q->left = q->right = NULL; } Tree 2 *t = init_T 2(5. 0); Insert_left_T 2(t, 10. 0); Insert_right_T 2(t, 20. 0); Insert_left_T 2(t->left, 1. 0); Insert_right_T 2(t->left, 3. 0); Insert_right_T 2(t->right, 4. 0); Лекция 18. 11. 13 г. 5 10 1 20 4 3 4
Обход бинарного дерева Рассмотрим задачу вывода на печать сформированного двоичного дерева. Эта задача является частным случаем задачи обхода дерева – посещения всех узлов дерева и выполнения некоторых действий в каждом узле. Из теории известно, что существуют три основных порядка возможного посещения узлов бинарного дерева: 1)Прямой обход (сверху вниз), при котором мы посещаем узел, а затем, последовательно – левое и правое поддеревья 2)Поперечный обход (слева направо), при котором мы посещаем левое поддерево, затем узел, а затем правое поддерево 3)Обратный обход (снизу вверх), при котором мы посещаем левое и правое поддеревья, а затем узел. 1 2 4 5 5 10 2 20 6 5 5 10 3 20 5 5 10 20 1 3 4 6 1 3 6 1 2 4 Прямой обход Поперечный обход Лекция 18. 11. 13 г. Обратный обход 5
Обход бинарного дерева Эти методы можно легко реализовать с помощью рекурсивной функции traverse, которая в каждом посещенном узле дерева вызывает функцию visit для выполнения некоторой «полезной работы» (например, для печати содержимого узла). В зависимости от места расположения вызова visit в тексте функции traverse реализуется прямой, поперечный или обратный обход дерева. void visit(Tree 2* h) { printf("%5. 1 f ", h->elem); } void traverse(Tree 2* h) { if (h == NULL) return; // visit(h); // прямой обход traverse(h->left); // visit(h); // поперечный обход traverse(h->right); // visit(h); // обратный обход } void Print_T 2(Tree 2 *t) { if (t == NULL) printf("Tree is empty"); traverse(t); printf("n--------n"); } Лекция 18. 11. 13 г. 6
Тестирование бинарного дерева #include
Рекурсивные алгоритмы бинарных деревьев Рассмотренный выше алгоритм обхода дерева демонстрирует необходимость рассмотрения и других рекурсивных алгоритмов для бинарных деревьев, что обусловлено самой рекурсивной структурой этих деревьев. Для решения многих задач можно непосредственно применять рекурсивные алгоритмы типа «разделяй и властвуй» , которые, по существу, обобщают алгоритмы обхода деревьев: ØОбработка дерева выполняется посредством обработки корневого узла и (рекурсивно) его поддеревьев; вычисление можно выполнять перед, между или после рекурсивных вызовов (или же использовать все три метода). Часто требуется определять различные структурные параметры дерева, имея только ссылку на дерево. На следующем слайде приведены рекурсивные функции для вычисления количества узлов и высоты заданного дерева. Эти функции не зависят от порядка обработки рекурсивных вызовов: они обрабатывают все узлы дерева и возвращают одинаковый результат, если, например, рекурсивные вызовы поменять местами. Лекция 18. 11. 13 г. 8
Вычисление количества узлов и высоты дерева int count(Tree 2 *h) { if (h == NULL) return 0; return count(h->left) + count(h->right) + 1; } int height(Tree 2 *h) { if (h == NULL) return -1; int u = height(h->left), v = height(h->right); if (u > v) return u+1; else return v+1; } 5 10 1 Лекция 18. 11. 13 г. 20 4 3 9
Анализ работы функции count 5 int count(Tree 2 *h) { if (h == NULL) return 0; return count(h->left) + count(h->right) + 1; } 10 20 1 4 3 count(5) 6 count(10)+count(20)+1 2 3 count(1)+count(3)+1 count(NULL)+count(4)+1 1 count(NULL)+count(NULL)+1 Лекция 18. 11. 13 г. 10
Вычисление количества листьев дерева Разработаем еще одну функцию – list_count, которая будет подсчитывать количество листьев дерева. Определение. Узел дерева, не имеющий потомков (дочерних узлов), называется листом или внешним узлом. Узел дерева, не являющийся корнем или листом, называется внутренним узлом. int list_count(Tree 2* h) { if (h == NULL) return 0; if ( (h->left==NULL) && (h->right==NULL) ) return 1; return list_count(h->left) + list_count(h->right); } 5 10 1 Лекция 18. 11. 13 г. 20 4 3 11
Анализ работы функции list_count int list_count(Tree 2* h) { if (h == NULL) return 0; if ( (h->left==NULL) && (h->right==NULL) ) return 1; return list_count(h->left) + list_count(h->right); } 5 10 1 20 4 3 l_count(5) 3 l_count(10)+l_count(20) 1 2 l_count(1)+l_count(3) 1 l_count(NULL)+l_count(4) 1 1 Лекция 18. 11. 13 г. 12
Корневые деревья с произвольным ветвлением (сильноветвящиеся деревья) До сих пор мы рассматривали только способы представления бинарных деревьев. В ряде задач используются сильноветвящиеся деревья, у которых любой узел может иметь произвольное количество дочерних узлов. Пример – файловая система: Лекция 18. 11. 13 г. 13
Сильноветвящиеся деревья Схему представления бинарных деревьев можно обобщить и для сильноветвящихся деревьев, в которых количество дочерних узлов не превышает некоторой константы K. При этом поля с указателями left и right заменяются полями child 1, child 2, … child. K. Если количество дочерних элементов узла не ограничено, то такой подход не работает, поскольку заранее не известно, для какого количества указателей нужно выделить место. Кроме того, если количество дочерних элементов K ограничено большой константой, но на самом деле у многих узлов потомков намного меньше, то значительный объем памяти будет израсходован напрасно. Однако существует эффективный способ представления деревьев с произвольным количеством дочерних узлов с помощью бинарных деревьев. Такой способ называется представлением с «левым дочерним и правым сестринским узлами» (left-child, right-sibling representation). Как и в представлении бинарного дерева, в каждом узле такого представления дерева содержатся 3 указателя: üуказатель р для ссылки вверх на «родительский» узел, üуказатель lchild для ссылки влево-вниз на крайний левый дочерний узел, üуказатель sibling для ссылки вправо на «родную сестру (брата)» . typedef struct Tree. N { char elem; struct Tree. N *p, *lchild, *sibling; } Tree. N; Лекция 18. 11. 13 г. 14
Сильноветвящиеся деревья На рисунках ниже представлено обычное изображение дерева и изображение с помощью представления с «левым дочерним и правым сестринским узлами» . A B E K D C F L A G H M I B J E K F L D C G H I J M Ниже представлена функция для инициализации сильноветвящегося дерева, которая практически ничем не отличается от аналогичной функции для бинарного дерева. Tree. N* init_TN(char c) { Tree. N *t = (Tree. N*)calloc(1, sizeof(Tree. N)); t->elem = c; t->p = t->lchild = t->sibling = NULL; return t; } Лекция 18. 11. 13 г. 15
Обход сильноветвящегося дерева Можно провести определенную аналогию между обходом бинарного дерева и обходом сильноветвящегося дерева и предложить две стратегии такого обхода: 1)Прямой обход (сверху вниз), при котором мы сначала посещаем узел, а затем все его поддеревья, 2)Обратный обход (снизу вверх), при котором мы сначала посещаем все поддеревья узла, а затем сам узел. 1 2 3 7 B 4 9 C 10 F E L G 6 K 13 A D 12 13 3 11 K 1 Прямой обход дерева 12 C 9 F E L G 4 I J 7 B 8 H M 5 5 A D 10 11 6 H I J M 2 8 Обратный обход дерева Лекция 18. 11. 13 г. 16
Обход сильноветвящегося дерева Ниже представлены тексты рекурсивных функций preorder и postorder, которые в каждом посещенном узле дерева вызывают функцию visit для выполнения некоторой «полезной работы» (например, для печати содержимого узла). Первая из них сначала вызывает visit, а затем проходит по всем дочерним узлам, а вторая – наоборот, сначала проходит по дочерним узлам, а затем вызывает visit. void visit(Tree. N* h) { printf("%c ", h->elem); } void preorder(Tree. N* h) { if (h == NULL) return; Tree. N* p; visit(h); for(p = h->lchild; p != NULL; p = p->sibling) preorder(p); } void postorder(Tree. N* h) { if (h == NULL) return; Tree. N* p; for(p = h->lchild; p != NULL; p = p->sibling) postorder(p); visit(h); } Лекция 18. 11. 13 г. 17
Обход сильноветвящегося дерева На практике прямой обход дерева соответствует, например, порядку чтения книги. Лекция 18. 11. 13 г. 18
Обход сильноветвящегося дерева Обратный обход дерева может использоваться, например, для подсчета суммарного объема дискового пространства, занимаемого файлами на жестком диске. Лекция 18. 11. 13 г. 19