- •3. Деревья
- •3.1. Основные определения
- •Пример 2.В качестве примера древовидной структуры можно рассматривать арифметические выражения. Правила соответствия имен узлов элементам выражений следующие:
- •3.2. Обходы бинарных деревьев
- •Пример 3. Рассмотрим 3 вида обхода на примере дерева, изображенного на рис.3.3.
- •Пример 4.Рассмотрим 3 вида обхода для дерева на рисунке 3.2
- •3.3. Операции с деревьями бинарного поиска
- •Пример 5. Если мы будем вводить с клавиатуры узлы 10, 7, 25, 31, 18, 6, 3, 12, 22, 8, то получим дерево, представленное на рис. 3.3.
- •Поиск по дереву
- •Удаление узла из дерева
- •Удаление дерева
- •Замечания
- •3.4. Операции с идеально сбалансированными деревьями
- •Поиск по дереву
- •Удаление дерева
- •3.3. Решение практических задач с использованием деревьев
3.2. Обходы бинарных деревьев
Обойти дерево – это побывать в каждом из его узлов точно по одному разу. Рассмотрим три наиболее часто используемых способов обхода бинарных деревьев – это обход в прямом, симметричном и обратном порядке. Все три обхода будем определять рекурсивно.
а) Прямой обход:
попасть в корень;
пройти левое поддерево данного корня;
пройти правое поддерево данного корня.
Подпрограмму, составляющую список узлов дерева при прохождении его в прямом порядке, можно записать следующим образом:
void preorder (tree *root)
{
if (root)
{
cout<<root->inf<<"\t";
preorder(root->left);
preorder(root->right);
}
}
В случае дерева выражений при прямом обходе получаем префиксную форму выражений, где оператор предшествует и левому, и правому операнду.
б) Симметричный обход:
пройти левое поддерево данного корня;
попасть в корень;
пройти правое поддерево данного корня.
Подпрограмму, составляющую список узлов дерева при прохождении его в симметричном порядке, можно записать следующим образом:
void inorder (tree *root)
{
if (root)
{
inorder(root->left);
cout<<root->inf<<"\t";
inorder(root->right);
}
}
В случае дерева выражений при симметричном обходе получаем инфиксную форму выражений, совпадающую с привычной формой записи выражений.
в) Обратный обход:
пройти левое поддерево данного корня;
пройти правое поддерево данного корня;
попасть в корень.
Подпрограмму, составляющую список узлов дерева при прохождении его в обратном порядке, можно записать следующим образом:
void postorder (tree *root)
{
if (root)
{
postorder(root->left);
postorder(root->right);
cout<<root->inf<<"\t";
}
}
В случае дерева выражений при обратном обходе получаем постфиксную (польскую) форму выражений, в которой оператор следует за левым и правым операндом.
Пример 3. Рассмотрим 3 вида обхода на примере дерева, изображенного на рис.3.3.
При прохождении в прямом порядке список узлов выглядит следующим образом: 10 7 6 3 8 25 18 12 22 31
При прохождении в симметричном порядке список узлов выглядит следующим образом: 3 6 7 8 10 12 18 22 25 31
При прохождении в обратном порядке список узлов выглядит следующим образом: 3 6 8 7 12 22 18 31 25 10
Замечание. Таким образом, при симметричном обходе дерева бинарного поиска на экран выводится упорядоченная по возрастанию последовательность данных. Этот свойство дерева бинарного поиска можно использовать для сортировки данных.
Пример 4.Рассмотрим 3 вида обхода для дерева на рисунке 3.2
Прямой обход (префиксная запись арифметического выражения):
-a×b+/cd/ef
Симметричный обход (инфиксная запись арифметического выражения):
a-b×c/d+e/f
Обратный обход (постфиксная запись арифметического выражения):
abcd/ef/+×-
Замечание. Напоминаем, что изложенные виды обходов применимы ко всем типам бинарных деревьев.
3.3. Операции с деревьями бинарного поиска
Построение дерева
Рассмотрим подпрограмму add (int x, tree *&root), которая добавляет новый узел в дерево так, что бы формировалось дерево бинарного поиска. Она имеет два формальных параметра: x – информация, которая записывается в новый узел; root – указатель на текущий узел дерева (вначале на корень исходного дерева).
Новый узел должен быть сформирован либо как корень дерева (если дерево было до этого пустое), либо в виде левого или правого сына сформированного раньше узла дерева, у которого этот сын отсутствует. Определение места для вставки нового узла производится на основе значения указателя root:
если root==NUL, то есть дерево пусто или найдено место для нового узла; то выделяется оперативная память для нового узла root=new tree; в нее помещается содержимое нового узла root->inf=x. Сыновей у этого узла нет, поэтому ссылки на них полагаются пустыми: root ->left= root ->right=NUL;
если root!=NUL, то есть в узле, на который он указывает, есть запись; то производится сравнение значения данного узла (root ->inf) и значения нового узла (x):
если x< root ->inf, то есть новое значение меньше значения данного узла, то поиск места новой записи продолжается по левому поддереву данного узла; для этого производится рекурсивный вызов подпрограммы add: add(x, root ->left);
если x> root ->inf, то есть новое значение больше значения данного узла, то поиск места новой записи продолжается по правому поддереву данного узла; для этого производится рекурсивный вызов подпрограммы add: add(x, root ->right).
Листинг подпрограммы выглядит следующим образом:
void add (int x, tree *&root)
{
if (!root)
{
root=new tree;
root->inf=x;
root->left=root->right=NULL;
}
else if (x<root->inf)
add(x,root->left);
else
if (x>root->inf)
add(x,root->right);
}
Для формирования дерева в основной программе можно написать обращение к этой подпрограмме на этапе ввода в цикле узлы дерева с клавиатуры или считывания их из файла.
