Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Пособие часть 1.doc
Скачиваний:
63
Добавлен:
24.09.2019
Размер:
6.98 Mб
Скачать

3.7.2. Рекурсивные функции обхода бинарных деревьев

Первые три порядка обхода, которые приводятся в таблице 3.5, легко реализуются рекурсиным вызовом функции обхода для левого и правого поддеревьев в том порядке, которого требует алгоритм. Требуется только пояснить шаг «посетить корень». Поскольку обходы деревьев могут потребоваться для любой обработки значений, находящихся в узлах, то будем считать, что этот шаг обозначает именно ту обработку, которая нужна для конкретной задачи.

Функции имеют очень компактный код, который приведен в листинге 3.4, а для простоты предполагается, что вся обработка состоит только в выводе на экран значения, которое хранится в узле. Для того, чтобы сделать данные функции универсальными, можно добавить еще один параметр— внешнюю функцию Visit, в которой будет сосредоточена вся обработка узла.

Листинг 3.4. Реализация рекурсивных обходов бинарного дерева

// Дополнение к файлу bintree.cpp

void forward(bt t)//прямой порядок обхода

{ if(t) {cout<<root(t); forward(left(t)); forward(right(t));}

}

void reverse(bt t)// обратный порядок обхода

{ if(t) {reverse (left(t));reverse(right(t));cout << root(t);}

}

void central(bt t)// центрированный порядок обхода

{ if (t) {central(left(t)); cout << root(t); central(right(t));}

}

Заметим, что обход в ширину не имеет простой рекурсивной реализации. Ниже мы приведем алгоритм обхода в ширину с использованием вспомогательной структуры данных — очереди.

3.7.3. Нерекурсивные функции обхода бинарных деревьев

Учитывая важность эффективной реализации обходов бинарных деревьев, рассмотрим нерекурсивный способ обходов, использующий цикл обхода по дереву. Вспомним, что рекурсивные функции используют внутренний (системный) стек программы, в который при каждом рекурсивном вызове помещается параметр (в данном случае указатель на узел) и адрес возврата. Следовательно, для реализации нерекурсивных алгоритмов логично использовать вспомогательный (внешний) стек, которая будет использоваться для тех же целей, что и системный стек программы в рекурсивных способах.

Введем вспомогательный стек s. При использовании языков высокого уровня мы не сможем в точности воспроизвести в нем содержимое системного стека, да это и не нужно. В литературе [8,10,14] приводятся различные алгоритмы нерекурсивного обхода, в которых вспомогательный стек используется только для хранения указателей на узлы дерева. Различные способы обхода различаются порядком, в котором эти указатели заталкиваются в стек и извлекаются из него.

Предположим, что уже имеется реализация шаблона стека с типом данных bt — указатель на узел бинарного дерева (для этого можно воспользоваться реализацией типа stack, которая была рассмотрена в предыдущей главе). Напомним базовые функции (методы) структуры stack:

push(p) – заталкиваем p в стек

pop() – извлекаем элемент с вершины стека

isnull() – проверяем стек на пустоту.

Наиболее прост в реализации алгоритм прямого обхода, который приводится в [14]. Напомним, что любая функция обхода получает в качестве параметра корень дерева. Поместим его в стек. Затем на каждом шаге итерации сначала извлекаем элемент с верхушки стека. При прямом обходе корень обрабатывается первым, после чего указатель на корень уже не нужен, а в стек помещается сначала указатель на правое поддерево, а затем на левое (пустые указатели в стек не помещаются). На следующем шаге итерации из стека извлекается и обрабатывается именно корень левого поддерева (он на вершине), после чего в стек заталкиваются указатели на его правое и левое поддеревья. Цикл повторяется до тех пор, пока стек не опустеет (это произойдет после извлечения крайнего правого листа). В листинге 3.5. приведена функция прямого обхода.

Листинг 3.5. Нерекурсивная реализация прямого обхода

void forwardstack(bt t) // нерекурсивный обход в прямом порядке

{ stack<bt> s; // структура stack должна быть реализована!

s.push(t); bt p;

while (!s.isnull())

{ p=s.pop();

cout << root(p)<<" "; //любая обработка узла

if(right(p)) s.push(right(p));

if(left(p)) s.push(left(p));

}

}

Проанализируем выполнение данной функции, поскольку алгоритм достаточно универсален и его можно приспособить и для некоторых других видов обхода. Пусть функция применяется к дереву из семи узлов, которое изображено на рис.3.9. Тогда будет выполнено семь шагов цикла (итераций), а последовательность извлекаемых элементов и содержимое стека в начале каждого шага представлены в табл. 3.6. На каждом шаге итерации в стек добавляются поддеревья очередного узла (1,2 или 0 в зависимости от их количества) и извлекается ровно один узел с вершины.

Таблица 3.6

Содержимое стека при прямом порядке обхода

№ итерации

Извлекаемый узел

Содержимое стека

1

a

a

2

b

bc

3

d

dec

4

e

ec

5

g

gc

6

c

c

7

f

f

Алгоритмы центрированного и обратного обходов несколько сложнее, поскольку в этом случае корень каждого поддерева нельзя обрабатывать сразу, поэтому придется поместить его в стек и двигаться дальше по левой ветви, помещая в стек все узлы до первого листа. В [7] приводится подобный алгоритм для выполнения центрированного обхода (листинг 3.6).

Листинг 3.6. Нерекурсивная реализация центрированного обхода

void centralstack(bt t) // в центрированном порядке

{ stack<bt> s; bt p=t;

do // помещаем в стек узел и переходим к левому сыну

{ if (p) { s.push(p); p=left(p);}

else

// дошли до левого листа, начинаем извлекать узлы

{if (!s.isnull())p=s.pop(); else return;

cout << root(p) <<" "; // обработали корень

p=right(p); //правое поддерево проходим после корня

}

}

while (true);

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

Таблица 3.7

.Содержимое стека при центрированном порядке обхода

№ итерации

Извлекаемый узел

Содержимое стека

4

d

dba

5

d

ba

8

g

gea

9

e

ea

10

a

a

12

c

c

14

f

f

Алгоритм обратного обхода на основе вспомогательного стека s реализовать сложнее, чем два других способа. Однако в этом случае можно применить небольшую хитрость, вспомнив, что слово «обратный» означает противоположный порядок по отношению к прямому. Поэтому можно использовать приведенный выше алгоритм прямого обхода, модифицировав его следующим образом.

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

  2. Поскольку обратный порядок означает ЛПК, а не ПЛК (правое-левое-корень — такой порядок действительно обрабатывал бы узлы в противоположном по отношение к прямому порядке), то внесем еще одно изменение — сначала будем помещать в стек указатель на левого сына, а затем на правого.

Листинг 3.7. Нерекурсивная реализация обратного обхода

void reversestack(bt t)// нерекурсивный обход в обратном порядке

// используем дополнительный стек для вывода в обратном порядке

{ stack<bt> s,sout;

s.push(t); bt p;

while (!s.isnull())

{ p=s.pop();

sout.push(p);//вместо обработки узла помещаем его в стек

if(left(p)) s.push(left(p));

if(right(p)) s.push(right(p));

}

while (!sout.isnull()) // обработка узлов в обратном порядке

{p=sout.pop(); cout << root(p)<<" ";}

}

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

  • Вместо стека используем очередь, которая как раз подходит для этих целей, поскольку при продвижении от корня к листьям в нее будут попадать все более удаленные от корня элементы. При этом, чем раньше узел попал в очередь, тем раньше он будет обработан.

  • Вторая модификация такая же, как и для обратного обхода — сначала помещаем в очередь левого сына, а после него правого. Это тоже понятно, поскольку узлы обходятся слева направо.

Предполагаем, что используется структура queue, реализованная в предыдущей главе. Напомним основные операции:

enqueue, dequeue – помещение элемента в очередь и удаление из очереди;

isnull – проверка на пустоту

Листинг 3.8. Реализация обхода бинарного дерева в ширину

void widthstack(bt t) // обход в ширину

{ queue<bt> q; // структура queue должна быть реализована!

q.enqueue(t); bt p;

while (!q.isnull())

{ p=q.dequeue();

cout << root(p)<<" ";

if(left(p)) q.enqueue (left(p));

if(right(p)) q.enqueue (right(p));

}

}

Все приведенные в данном разделе функции обхода в конце работы оставляют вспомогательные структуры пустыми, т. е. не требуют никакой дополнительной «уборки» памяти.