Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ТП лекции Раздел 4.doc
Скачиваний:
16
Добавлен:
28.09.2019
Размер:
2.56 Mб
Скачать

4.5.4. Бинарные деревья.

Бинарное дерево — это конечное множество элементов, которое либо пусто, либо содержит один элемент, называемый корнем дерева, а остальные элементы множества делятся на два непересекающихся подмножества, каждое из которых само является бинар­ным деревом. Эти подмножества называются левым и правым поддеревьями исходного дерева. Каждый элемент бинарного дерева называется узлом дере­ва. Общепринятый способ изображения бинарного дерева представлен на рис. 5.15, а на рис. 5.16 показаны структуры, которые таковыми не являют­ся.

Если А — корень бинарного дерева, а В — корень его левого или правого поддерева, то говорят, что А является отцом В, а В — левый или правый сын. Два узла являются братьями, если они сыновья одного и того же отца. Узел, не имеющий сыновей (узлы D, G, Н и I на рис. 5.15), называется листом. Если каждый узел бинарного дерева, не являющийся листом, имеет непус­тые правое и левое поддеревья, то дерево называется строго бинарным деревом.

Уровень узла в бинарном дереве определен следующим образом: корень де­рева имеет уровень 0, а уровень любого другого узла дерева на 1 больше уровня своего отца. Глубина бинарного дерева — это максимальный уровень листа дерева, что равно длине самого длинного пути от корня к листу дерева.

Графическое представление (рис. 5.15) наглядно и им удобно пользоваться при работе с бинарными деревьями. Однако следует помнить, что память линейна и с этой точки зрения можно считать, что бинарное дерево пред­ставляет собой разновидность связного списка. Поэтому основные операции над ними во многом те же самые, что и для списков: элементы дерева мож­но добавлять, удалять, а также осуществлять к ним доступ. Интерфейс для программ работы с бинарными деревьями приведен в листинге 5.10. Ин­формационные поля могут быть произвольными. В данном случае, чтобы не загромождать функции дополнительной обработкой "сложных" полей, мы ограничились двумя целыми полями.

/* Интерфейс для работы с бинарными деревьями Модуль Tree.h */

#define TREE struct tree

TREE

{

int iField; // информационное значение

int iCount; // счетчик вхождений элемента

TREE *pLeft, *pRight; // указатели на левое и правое поддеревья

};

extern void insert(TREE **ppRoot, TREE *pItem);

extern int remove(TREE **ppRoot, TREE *pItem) ;

extern void destroy(TREE *pRoot);

extern void display(TREE *pRoot);

Структура для работы с бинарными деревьями приведена на рис. 5.17. Обра­тите внимание на ее сходство со структурой для работы с двунаправленным списком (рис. 5.13).

При работе с деревьями одной из основных операций является прохождение дерева — обход всего дерева, при котором каждый узел посещается один раз. Существуют три способа, отличающиеся порядком посещения корня и про­хождения его левого и правого поддеревьев (рис. 5.18):

  • Прямой порядок (нисходящий)

  • попасть в корень;

  • пройти в прямом порядке левое поддерево;

  • пройти в прямом порядке правое.

  • Симметричный порядок (последовательный)

  • пройти в симметричном порядке левое поддерево;

  • попасть в корень;

  • пройти в симметричном порядке правое поддерево.

  • Обратный порядок (восходящий)

  • пройти в обратном порядке левое поддерево;

  • пройти в обратном порядке правое поддерево;

  • попасть в корень.

Функции для работы с бинарными деревьями те же, что и для списка. Реа­лизация этих функций приведена в листинге 5.11.

/* Реализация функции работы с деревом

Модуль Tree.с */

#include <string.h>

#include <malloc.h>

#include <stdio.h>

#include "tree.h"

static TREE* create(TREE *pItem)

{

TREE *pNewItem = (TREE *)malloc(sizeof(TREE));

*pNewItem = *pItem;

return pNewItem;

}

void insert(TREE **ppRoot, TREE *pltem)

{

// В качестве ключевого поля в списке используется фамилия.

TREE *pParent = 0, *pCurItem = *ppRoot;

TREE *pNewItem;

int nFound = 0;

// Проходим дерево, пока не достигнут лист или не найден узел

while(pCurItem && !nFound)

{

// Пока не достигнут лист дерева или не найден узел

if(pItem->iField = = pCurItem->iField)

{

// Узел найден — устанавливаем флаг завершения поиска

nFound = 1;

//и увеличиваем значение счетчика числа вхождений

pCurItem->iCount++;

}

else

{

// Запоминаем текущий узел

pParent = pCurItem;

if(pItem->iField < pCurItem->iField)

// Если новый элемент "меньше", чем хранящийся в узле,

// идем по левой ветви

pCurItem = pCurItem->pLeft;

else

// иначе — по правой

pCurItem = pCurItem->pRight;

}

}

// Если элемента нет в дереве

if(!nFound)

{

// Такого элемента в дереве нет — создаем новый узел pNewItem = create(pItem); pNewItem->pLeft = pNewItem->pRight = 0; if(pParent = =0)

{

// Первый узел дерева — создаем его и делаем корнем

*ppRoot = create(pItem);

(*ppRoot)->pLeft = (*ppRoot)->pRight = 0;

}

else {

// Если новый элемент меньше, чем хранящийся в текущем узле,

if(pItem->iField < pParent->iField)

// вставляем его в левую ветвь

pParent->pLeft = pNewItem;

else

// иначе — в правую pParent->pRight = pNewItem; } . } } int remove(TREE **ppRoot, TREE *pltem)

{

TREE *pPreItem = 0, *pPresent = *ppRoot;

TREE *pReplace, *pParent, *pTmp;

int nFound = 0;

// Ищем узел с заданным ключевым полем

while(pPresent && !nFound)

{

if(pItem->iField = = pPresent->iField)

nFound = 1;

else

{

pPreItem = pPresent;

if(pItem->iField < pPresent->iField)

pPresent = pPresent->pLeft;

else

pPresent = pPresent->pRight;

}

if(nFound)

{

if(pPresent->pLeft = = 0)

// Если нет левого сына, то удаляемый элемент

// заменяем на правого сына

pReplace = pPresent->pRight;

else

// Если нет правого сына, то удаляемый элемент

// заменяем на левого сына

if(pPresent->pRight = = 0)

pReplace = pPresent->pLeft;

else {

pParent = pPresent;

pReplace = pPresent->pRight;

pTmp = pPresent->pLeft;

// Ищем самый левый лист

while(pTmp != 0)

{

pParent = pReplace;

pReplace = pTmp;

pTmp = pReplace->pLeft;

}

// Если это корень

if(pParent != pPresent)

{

// Заменяем на найденный лист

pParent->pLeft = pReplace->pRight;

pReplace->pRight = pPresent->pRight;

}

pReplace->pRight = pPresent->pRight;

}

if(pPreItem = 0)

// Если удаляется корень,

//то выбранный ранее сын становится корнем дерева

*ppRoot = pReplace;

else

//В противном случае заменяем значение

// соответствующего указателя

if(pPresent == pPre!tem->pLeft)

pPre!tem->pLeft = pReplace;

else

pPre!tem->pRight = pReplace;

// He забываем освободить память, занимаемую удаленным узлом

free(pPresent);

}

return 1;

}

void destroy(TREE *pRoot)

{

if(pRoot)

{

// Рекурсивно проходим левое поддерево

destroy(pRoot->pLeft);

// Рекурсивно проходим правое поддерево

destroy(pRoot->pRight);

// Удаляем узел

free(pRoot);

}

pRoot = 0;

}

void display(TREE *pRoot)

{

if(pRoot)

{

// Рекурсивно проходим левое поддерево

display(pRoot->pLeft);

// Выводим информацию об узле

printf("\n%s, %s", pRoot->aLastName, pRoot->aFirstName);

printf(" , \t%s", pRoot->aTelephoneNumber);

// Рекурсивно проходим правое поддерево

display(pRoot->pRight);

}

}

Функция destroy для прохождения дерева использует восходящий способ об­хода дерева, "спускаясь" сначала вниз по левому поддереву, а функция display — симметричный (последовательный) обход бинарного дерева. В обо­их случаях используется рекурсивный вызов соответствующей функции.

Рассмотрим теперь более подробно функцию insert, которая добавляет в дерево новый узел. Прежде всего организуем поиск добавляемого узла — может быть, он уже присутствует в дереве.

Поиск заканчивается либо при достижении листа дерева, либо если узел с таким полем уже есть. После завершения поиска необходимо вставить в де­рево новый узел (рис. 5.19), если такого еще там нет (напоминаем, что в пе­ременной pParent хранится указатель на отца вставляемого узла).

Очевидно, что "внешний вид" дерева зависит от порядка включения в него элементов. Но в любом случае это будет упорядоченное бинарное дерево, в котором номер левого сына всегда меньше, а правого — больше, чем номер отца.

Подведем итог: для того чтобы вставить в дерево новый узел, необходимо найти для него отца (иногда называемого родителем) и сделать его соответ­ственно левым или правым сыном, в зависимости от значения ключевого поля нового узла.

Рассмотрим удаление узла из бинарного дерева. В этом случае все несколько сложнее. Естественно, прежде всего необходимо найти узел, который требует­ся удалить. Делается это точно так же, как и при вставке (функция insert). Результатом поиска может быть один из трех вариантов:

1. Узла с заданным ключом в дереве нет.

2. Узел с заданным ключом имеет не более одного потомка.

3. Узел с заданным ключом имеет двух потомков.

Действия в первом случае очевидны — завершаем работу функции. Второй случай (исключаемый элемент — лист или узел с одним потомком) также достаточно прост.

Трудность возникает, если нужно удалить узел с двумя потомками. В этом случае удаляемый узел нужно заменить либо на самый правый элемент его левого поддерева, либо на самый левый элемент его самого правого подде­рева, причем они должны иметь максимум одного потомка.

Рассмотренный алгоритм удаления узла дерева иллюстрируется на рис. 5.20, где приведено исходное дерево (а), из которого последовательно удаляются вершины с ключами 13, 15, 5 и 10.

Тема 4.6. Классы в Visual C++.