Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
LR17.doc
Скачиваний:
31
Добавлен:
10.03.2016
Размер:
203.26 Кб
Скачать

Реализация деревьев

Пусть деревоT имеет узлы 1, 2, …., n. Возможно, самым простым представлением дерева T будет линейный массив A, где каждый элемент A[i] содержит номер родительского узла (является курсором на родителя). Поскольку корень дерева не имеет родителя, то его курсор будет равен 0.

Рассмотрим пример.

Для приведенного на рисунке дерева построим линейный массив по следующему правилу: A[i]=j, если узел j является родителем узла i, A[i]=0, если узел i является корнем. Тогда массив будет выглядеть следующим образом:

1

2

3

4

5

6

7

8

9

10

0

1

1

1

3

3

4

5

5

6

Другой важный и полезный способ представления деревьев состоит в формировании для каждого узла списка его сыновей. Рассмотрим этот способ для приведенного выше примера.

1

2

3

4

5

6

7

8

9

10

Двоичные деревья

Двоичное или бинарное дерево – это наиболее важный тип деревьев. Каждый узел бинарного дерева имеет не более двух поддеревьев, причем в случае только одного поддерева следует различать левое или правое. Строго говоря, бинарное дерево – это конечное множество узлов, которое является либо пустым, либо состоит из корня и двух непересекающихся бинарных деревьев, которые называются левым и правым поддеревьями данного корня. Тот факт, что каждый сын любого узла определен как левый или как правый сын, существенно отличает двоичное дерево от обычного упорядоченного ориентированного дерева.

Если мы примем соглашение, что на схемах двоичных деревьев левый сын всегда соединяется с родителем линей, направленной влево и вниз от родителя, а правый сын – линией, направленной вправо и вниз, тогда деревья, изображенные на рисунке а) и б) ниже, – это различные деревья, хотя они оба похожи на обычное дерево (рис. в)).

а) б) в)

Двоичные деревья нельзя непосредственно сопоставить с обычным деревом.

Обход двоичных деревьев в прямом и обратном порядке в точности соответствует таким же обходам обычных деревьев. При симметричном обходе двоичного дерева с корнем n левым поддеревом T1 и правым поддеревом T2 сначала проходится поддерево T1, затем корень n и далее поддерево T2.

Представление двоичных деревьев

Бинарное дерево можно представить в виде динамической структуры данных, состоящей из узлов, каждый из которых содержит кроме данных не более двух ссылок на правое и левое бинарное дерево. На каждый узел имеется одна ссылка. Начальный узел называется корнем.

По аналогии с элементами списков, узлы дерева удобно представить в виде записей, хранящих информацию и два указателя:

Type

Tree=^s;

S=record

Inf: <тип хранимой информации>;

Left, right: tree;

End;

Изобразим схематично пример дерева, организованного в виде динамической структуры данных:

Дерево поиска

Обратите внимание на рисунок, приведенный выше. Данное дерево организовано таким образом, что для каждого узла все ключи (значения узлов) его левого поддерева меньше ключа этого узла, а все ключи его правого поддерева больше. Такой способ построения дерева называется деревом поиска или двоичным упорядоченным деревом.

С помощью дерева поиска можно организовать эффективный способ поиска, который значительно эффективнее поиска по списку.

Поиск в упорядоченном дереве выполняется по следующему рекурсивному алгоритму:

  • Если дерево не пусто, то нужно сравнить искомый ключ с ключом в корне дерева:

- если ключи совпадают, поиск завершен;

- если ключ в корне больше искомого, выполнить поиск в левом поддереве;

- если ключ в корне меньше искомого, выполнить поиск в правом поддереве.

  • Если дерево пусто, то искомый элемент не найден.

Дерево поиска может быть использовано для построения упорядоченной последовательности ключей узлов. Например, если мы используем симметричный порядок обхода такого дерева, то получим упорядоченную по возрастанию последовательность: 1 6 8 10 20 21 25 30.

Можно организовать «зеркально симметричный» обход, начиная с правого поддерева, тогда получим упорядоченную по убыванию последовательность: 30 25 20 10 8 6 1.

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

Операции с двоичными деревьями

Для работы с двоичными деревьями важно уметь выполнять следующие операции:

  • Поиск по дереву;

  • Обход дерева;

  • Включение узла в дерево;

  • Удаление узла из дерева.

1. Алгоритм поиска по дереву мы рассмотрели выше. Функция поиска:

function find(root:tree; key:integer; var p, parent:tree):Boolean;

begin

p:=root;

while p<>nil do begin

if key=p^.inf then {узел с таким ключом есть}

begin find:=true; exit; end;

parent:=p {запомнить указатель на предка}

if key<p^.inf then

p:=p^.left {спуститься влево}

else p:=p^.right; {спуститься вправо}

end;

find:=false;

end;

2. Мы рассмотрели несколько способов обхода дерева. Наибольший интерес для двоичного дерева поиска представляет симметричный обход, т.к. он дает нам упорядоченную последовательность ключей. Логично реализовать обход дерева в виде рекурсивной процедуры.

Procedure obhod(p:tree);

Begin

if p<>nil then

begin

obhod(p^.left);

writeln(p^.inf);

obhod(p^.right);

end;

end;

3. Вставка узла в двоичное дерево поиска не представляет сложности. Для того чтобы вставить узел, необходимо найти его место. Для этого мы сравниваем вставляемый ключ с корнем, если ключ больше, чем ключ корня, уходим в правое поддерево, а иначе – в левое. Тем же образом продвигаемся дальше, пока не дойдем до конечного узла (листа). Сравниваем вставляемый ключ с ключом листа. Если ключ меньше ключа листа, то добавляем листу левого сына, а иначе – правого сына. Например, необходимо вставить в дерево, изображенное на рисунке, узел с ключом 5.

Сравниваем 5 с ключом корня; 5<10, следовательно, уходим в левое поддерево. Сравниваем 5 и 6; 5<6, спускаемся влево. Следующий узел является конечным (листом). Сравниваем 5 и 1; 5>1, следовательно, вставляем правого сына. Получим дерево с новым узлом, которое сохранило все свойства дерева поиска.

4. Удаление узла дерева происходит по-разному в зависимости от его расположения в дереве.

Если узел является конечным (то есть не имеет потомков), то его удаление не вызывает трудностей, достаточно обнулить соответствующий указатель узла-родителя.

Если узел имеет только одного потомка, этот потомок ставится на место удаляемого узла, а в остальном дерево не изменяется.

Сложнее всего случай, когда у удаляемого узла есть оба потомка.

Есть простой особый случай: если у правого потомка удаляемого узла нет левого потомка, удаляемый узел заменяется на своего правого потомка, а его левый потомок подключается вместо отсутствующего левого потомка к замещающему узлу. Рассмотрите этот случай на рисунке, должно стать понятней.

Вобщем же случае на место удаляемого узла ставится самый левый лист его правого поддерева (или наоборот – самый правый лист его левого поддерева). Это не нарушает свойств дерева поиска.

Корень дерева удаляется по общему правилу за исключением того, что заменяющий его узел не требуется присоединять к узлу-родителю.

Рассмотрим реализацию алгоритма удаления.

procedure del(var root:tree; key:integer);

var

p:tree; {удаляемый узел}

parent:tree; {предок удаляемого узла}

y:tree; {узел, заменяющий удаляемый}

function spusk(p:tree):tree;

var

y:tree; {узел, заменяющий удаляемый}

pred:tree; {предок узла “y”}

begin

y:=p^.right;

if y^.left=nil then y^.left:=p^.left {1}

else {2}

begin

repeat

pred:=y; y:=y^.left;

until y^.left=nil;

y^.left:=p^.left; {3}

pred^.left:=y^.right; {4}

y^.right:=p^.right; {5}

end;

spusk:=y;

end;

begin

if not find(root, key, p, parent) then {6}

begin writeln(‘такого элемента нет’); exit; end;

if p^.left=nil then y:=p^.right {7}

else

if p^.right=nil then y:=p^.left {8}

else y:=spusk(p); {9}

if p=root then root:=y {10}

else {11}

if key<parent^.inf then

parent^.left:=y

else parent^.right:=y;

dispose(p); {12}

end;

В функцию del передаются указатель root на корень дерева и ключ key удаляемого элемента. С помощью функции find определяются указатели на удаляемый элемент p и его предка parent. Если искомого элемента в дереве нет, то выдается сообщение ({6}).

В операторах {7}-{9} определяется указатель на узел y, который должен заменить удаляемый. Если у узла p нет левого поддерева, на его место будет поставлена вершина (возможно пустая) его правого поддерева ({7}).

Иначе, если у узла p нет правого поддерева, на его место будет поставлена вершина его левого поддерева ({8}).

В противном случае, когда оба поддерева существуют, для определения замещающего узла вызывается функция spusk, выполняющая спуск по дереву ({9}).

В этой функции первым делом проверяется особый случай, описанный выше ({1}). Если же этот случай (отсутствие левого потомка у правого потомка удаляемого узла) не выполняется, организуется цикл ({2}), на каждой итерации которого указатель на текущий элемент запоминается в переменной pred, а указатель y смещается вниз и влево до того момента, пока не станет ссылаться на узел, не имеющий левого потомка (он-то нам и нужен).

В операторе {3} к этой пустующей ссылке присоединяется левое поддерево удаляемого узла. Перед тем как присоединять к этому узлу правое поддерево удаляемого узла ({5}), требуется «пристроить» его собственное правое поддерево. Мы присоединяем его к левому поддереву предка узла y, заменяющего удаляемый ({4}), поскольку этот узел перейдет на новое место.

Функция spusk возвращает указатель на узел, заменяющий удаляемый. Если мы удаляем корень дерева, надо обновить указатель на корень ({10}), иначе – присоединить этот указатель к соответствующему поддереву предка удаляемого узла ({11}).

После того как узел удален из дерева, освобождается занимаемая им память ({12}).

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]