Основные операции над деревьями.
Над деревьями определены следующие основные операции, для которых приведены реализующие их программы.
- 1) Поиск узла с заданным ключом ( Find ).
- 2) Добавление нового узла ( Dob ).
- 3) Удаление узла ( поддерева ) ( Udal ).
- 4) Обход дерева в определенном порядке:
- Нисходящий обход ( процедура Preorder , рекурсивная процедура r_Preoder);
- Смешанный обход (процедура Inorder, рекурсивная процедура r_Inorder);
- Восходящий обход ( процедура Postorder, рекурсивная процедура r_Postorder).
Приведенные ниже программы процедур и функций могут быть непосредственно использованы при решении индивидуальных задач. Кроме выше указанных процедур приведены следующие процедуры и функции:
- процедура включения в стек при нисходящем обходе (Push_st);
- функция извлечения из стека при нисходящем обходе (Pop_st);
- процедура включения в стек при восходящем и смешанном обходе (S_Push);
- функция извлечения из стека при восходящем и смешанном обходе (S_Pop).
Для прошитых деревьев:
- функция нахождения сына данного узла ( Inson );
- функция нахождения отца данного узла ( Inp );
- процедура включения в дерево узла слева от данного (leftIn);
ПОИСК ЗАПИСИ В ДЕРЕВЕ( Find ).
Нужная вершина в дереве ищется по ключу. Поиск в бинарном дереве осуществляется следующим образом.
Пусть построено некоторое дерево и требуется найти звено с ключом X. Сначала сравниваем с X ключ, находящийся в корне дерева. В случае равенства поиск закончен и нужно возвратить указатель на корень в качестве результата поиска. В противном случае переходим к рассмотрению вершины, которая находится слева внизу, если ключ X меньше только что рассмотренного, или справа внизу, если ключ X больше только что рассмотренного. Сравниваем ключ X с ключом, содержащимся в этой вершине, и т.д. Процесс завершается в одном из двух случаев:
- 1) найдена вершина, содержащая ключ, равный ключу X;
- 2) в дереве отсутствует вершина, к которой нужно перейти для выполнения очередного шага поиска.
В первом случае возвращается указатель на найденную вершину. Во втором - указатель на звено, где остановился поиск, (что удобно для построения дерева ).
Реализация функции Find приведена в программном примере 6.2.
{=== Программный пример 6.2. Поиск звена по ключу === } Function Find(k:KeyType;d:TreePtr;var rez:TreePtr):bollean; { где k - ключ, d - корень дерева, rez - результат } Var p,g: TreePtr; b: boolean; Begin b:=false; p:=d; { ключ не найден } if d <> NIL then repeat q: =p; if p^.key = k then b:=true { ключ найден } else begin q:=p; { указатель на отца } if k < p^.key then p:=p^.left { поиск влево } else p:=p^.right { поиск вправо} end; until b or (p=NIL); Find:=b; rez:=q; End; { Find }
ДОБАВЛЕНИЕ НОВОГО УЗЛА ( Dop ).
Для включения записи в дерево прежде всего нужно найти в дереве ту вершину, к которой можно "подвести" (присоединить) новую вершину, соответствующую включаемой записи. При этом упорядоченность ключей должна сохраняться.
Алгоритм поиска нужной вершины, вообще говоря, тот же самый, что и при поиске вершины с заданным ключом. Эта вершина будет найдена в тот момент, когда в качестве очередного указателя, определяющего ветвь дерева, в которой надо продолжить поиск, окажется указатель NIL ( случай 2 функции Find ). Тогда процедура вставки записывается так, как в программном примере 6.3.
{=== Программный пример 6.3. Добавление звена ===} Procedure Dob (k:KeyType; var d:TreePtr; zap:data); { k - ключ, d - узел дерева, zap - запись } Var r,s: TreePtr; t: DataPtr; Begin if not Find(k,d,r) then begin (* Занесение в новое звено текста записи *) new(t); t^:=zap; new(s); s^.key:=k; s^.ssil:=t; s^.left:=NIL; s^.right:=NIL; if d = NIL then d:=s (* Вставка нового звена *) else if k < r^.key then r^.left:=s else r^.right:=s; end; End; { Dop }
ОБХОД ДЕРЕВА.
Во многих задачах, связанных с деревьями, требуется осуществить систематический просмотр всех его узлов в определенном порядке. Такой просмотр называется прохождением или обходом дерева.
Бинарное дерево можно обходить тремя основными способами: нисходящим, смешанным и восходящим ( возможны также обратный нисходящий, обратный смешанный и обратный восходящий обходы).
Принятые выше названия методов обхода связаны с временем обработки корневой вершины: До того как обработаны оба ее поддерева (Preorder), после того как обработано левое поддерево, но до того как обработано правое (Inorder), после того как обработаны оба поддерева (Postorder). Используемые в переводе названия методов отражают направление обхода в дереве: от корневой вершины вниз к листьям - нисходящий обход; от листьев вверх к корню - восходящий обход, и смешанный обход - от самого левого листа дерева через корень к самому правому листу.
Схематично алгоритм обхода двоичного дерева в соответствии с нисходящим способом может выглядеть следующим образом:
- 1. В качестве очередной вершины взять корень дерева. Перейти к пункту 2.
- 2. Произвести обработку очередной вершины в соответствии с требованиями задачи. Перейти к пункту 3.
- 3.а) Если очередная вершина имеет обе ветви, то в качестве новой вершины выбрать ту вершину, на которую ссылается левая ветвь, а вершину, на которую ссылается правая ветвь, занести в стек; перейти к пункту 2;
- 3.б) если очередная вершина является конечной, то выбрать в качестве новой очередной вершины вершину из стека, если он не пуст, и перейти к пункту 2; если же стек пуст, то это означает, что обход всего дерева окончен, перейти к пункту 4;
- 3.в) если очередная вершина имеет только одну ветвь, то в качестве очередной вершины выбрать ту вершину, на которую эта ветвь указывает, перейти к пункту 2.
- 4. Конец алгоритма.
Для примера рассмотрим возможные варианты обхода дерева (рис.6.26).
При обходе дерева представленного на рис.6.26 этими тремя методами мыполучим следующие последовательности: ABCDEFG ( нисходящий ); CBAFEDG ( смешанный ); CBFEGDA ( восходящий ).
Рис.6.26. Схема дерева
НИСХОДЯЩИЙ ОБХОД (Preorder, r_Preorder).
В соответствии с алгоритмом, приведенным выше, текст процедуры имеет вид:
{=== Программный пример 6.4. Нисходящий обход ===} Procedure Preorder (t: TreePtr); Type Stack=^Zveno; Zveno = record next: Stack; el: pointer; end; Var st: stack; p: TreePtr; (*------------ Процедура занесения в стек указателя ------*) Procedure Push_st (var st:stack; p:pointer); Var q: stack; begin new(q); q^.el:=p; q^.next:=st; st:=g; end; (*----------- Функция извлечения из стека указателя ------*) Function Pop_st (var st: stack):pointer; Var e,p: stack; begin Pop_st:=st^.el; e:=st; st:=st^.next; dispose(e); end; Begin if t = NIL then begin writeln('Дерево пусто'); exit; end else begin st:=nil; Push_st(St,t); end; while st <> nil do { контроль заполнения стека } begin p:=Pop_st(st); while p <> nil do begin { Обработка данных звена } if p^.right <> nil then Push_st(st,p^.right); p:=p^.left; end; end; End; { Preorder }
Трассировка нисходящего обхода приведена в табл.6.1:
Таблица 6.1
РЕКУРСИВНЫЙ НИСХОДЯЩИЙ ОБХОД.
Алгоритм существенно упрощается при использовании рекурсии. Так, нисходящий обход можно описать следующим образом:
- 1). Обработка корневой вершины;
- 2). Нисходящий обход левого поддерева;
- 3). Нисходящий обход правого поддерева.
Алгоритм рекурсивного нисходящего обхода реализован в программном примере 6.5.
{=== Программный пример 6.5. Рекурсивное выполнение нисходящего обхода ===} Procedure r_Preorder (t: TreePtr); begin if t = nil then begin writeln('Дерево пусто'); exit; end; (*------------------- Обработка данных звена --------------*) ................................ if t^.left <> nil then r_Preorder(t^.left); if t^.right <> nil then r_Preorder(t^.right); End; { r_Preorder }
CМЕШАННЫЙ ОБХОД (Inorder, r_Inorder).
Смешанный обход можно описать следующим образом:
- 1) Спуститься по левой ветви с запоминанием вершин в стеке;
- 2) Если стек пуст то перейти к п.5;
- 3) Выбрать вершину из стека и обработать данные вершины;
- 4) Если вершина имеет правого сына, то перейти к нему; перейти к п.1.
- 5) Конец алгоритма.
В программном примере 6.6. реализован алгоритм смешанного обхода дерева.
{=== Программный пример 6.6. Процедура смешанного обхода ===} Procedure Inorder (t: TreePtr); label 1; type Stack=^Zveno; { тип стека } Zveno = record next: Stack; El: pointer; end; Var st: stack; p: TreePtr; (*---------- Процедура занесения в стек указателя ---------*) Procedure Push_st (var st:stack; p:pointer); Var q: stack; begin new(q); q^.el:=p; q^.next:=st; st:=g; end; (*------------ Функция извлечения из стека указателя ------*) Function Pop_st (var st: stack):pointer; Var e,p: stack; begin Pop_st:=st^.el; e:=st; st:=st^.next; dispose(e); end; Begin if t = NIL then begin writeln('Дерево пусто'); exit; end else begin p:=t; st:=nil; end; 1: while p^.left <> nil do begin (* Спуск по левой ветви и заполнение очереди *) Push_st(st,p); p:=p^.left; end; if st = NIL { контроль заполнения стека } then exit; p:=Pop_st(st);{выборка очередного элемента на обработку} (*--------------- Обработка данных звена --------------*) ................................
if p^.right <> nil then begin p:=p^.right; { переход к правой ветви } goto 1; end; End; { Inorder }
Трассировка смешанного обхода приведена в табл. 6.2:
Таблица 6.2
Рекурсивный смешанный обход описывается следующим образом:
- 1) Смешанный обход левого поддерева;
- 2) Обработка корневой вершины;
- 3) Смешанный обход правого поддерева.
Текст программы рекурсивной процедуры ( r_Inorder ) демонстрируется в программном примере 6.7.
{=== Программный пример 6.7. Рекурсивное выполнение смешанного обхода ===} Procedure r_Inorder(t: TreePtr); begin if t = nil then begin writeln('Дерево пусто'); exit; end; if t^.left <> nil then R_inorder (t^.left); (*--------------- Обработка данных звена --------------*) ................................ if t^.right <> nil then R_inorder(t^.right); End;
ВОСХОДЯЩИЙ ОБХОД ( Postorder, r_Postorder ).
Трудность заключается в том, что в отличие от Preorder в этом алгоритме каждая вершина запоминается в стеке дважды: первый раз - когда обходится левое поддерево, и второй раз - когда обходится правое поддерево. Таким образом, в алгоритме необходимо различать два вида стековых записей: 1-й означает, что в данный момент обходится левое поддерево; 2-й - что обходится правое, поэтому в стеке запоминается указатель на узел и признак (код-1 и код-2 соответственно).
Алгоритм восходящего обхода можно представить следующим образом:
- 1) Спуститься по левой ветви с запоминанием вершины в сте ке как 1-й вид стековых записей;
- 2) Если стек пуст, то перейти к п.5;
- 3) Выбрать вершину из стека, если это первый вид стековых записей, то возвратить его в стек как 2-й вид стековых запи сей; перейти к правому сыну; перейти к п.1, иначе перейти к п.4;
- 4) Обработать данные вершины и перейти к п.2;
- 5) Конец алгоритма.
Текст программы процедуры восходящего обхода ( Postorder) представлен в программном примере 6.8.
{=== Программный пример 6.8. Восходящий обход ====} Procedure Postorder (t: TreePtr); label 2; Var p: TreePtr; top: point; { стековый тип } Sign: byte; { sign=1 - первый вид стековых записей} { sign=2 - второй вид стековых записей} Begin (*------------- Инициализация ------------------*) if t = nil then begin writeln('Дерево пусто'); exit; end else begin p:=t; top:=nil; end; {инициализация стека} (*------- Запоминание адресов вдоль левой ветви -------*) 2: while p <> nil do begin s_Push(top,1,p); { заносится указатель 1-го вида} p:=p^.left; end; (*-- Подъем вверх по дереву с обработкой правых ветвей ----*) while top <> nil do begin p:=s_Pop(top,sign); if sign = 1 then begin s_Push(top,0,p); { заносится указатель 2-го вида } p:=p^.right; goto 2; end else (*---- Обработка данных звена ---------*) ................................
end; End; { Postorder }
РЕКУРСИВНЫЙ СМЕШАННЫЙ ОБХОДописывается следующим образом:
- 1). Восходящий обход левого поддерева;
- 2). Восходящий обход правого поддерева;
- 3). Обработка корневой вершины.
Текст программы процедуры рекурсивного обхода (r_Postorder) демонстрируется в програмном примере 6.9.
{ ==== Программный пример 6.9. ===== } (*--------- Рекурсивное выполнение нисходящего обхода -----*) Procedure r_Postorder (t: TreePtr); Begin if t = nil then begin writeln('Дерево пусто'); exit; end; if t^.left <> nil then r_Postorder (t^.left); if t^.right <> nil then r_Postorder (t^.right); (*-------------- Обработка данных звена --------------*) ................................ End; { r_Postorder }
Если в рассмотренных выше процедурах поменять местами поля left и right, то получим процедуры обратного нисходящего, обратного смешанного и обратного восходящего обходов.
ПРОЦЕДУРЫ ОБХОДА ДЕРЕВА, ИСПОЛЬЗУЮЩИЕ СТЕК.
Тип стека при нисходящем обходе.
Stack=^Zveno; Zveno = record next: Stack; El: pointer; end;
Процедура включения элемента в стек при нисходящем и смешанном обходе ( Push_st ) приведена в программном примере 6.10.
{ === Програмнный пример 6.10 ====} Procedure Push_st (var st: stack; p: pointer); Var q: stack; begin new(q); q^.el:=p; q^.next:=st; st:=q; end;
Функция извлечения элемента из стека при нисходящем и смешанном обходе ( Pop_st ) приведена в программном примере 6.11.
{ === Програмнный пример 6.11 ====} Function Pop_st (var st: stack):pointer; Var e,p: stack begin Pop_st:=st^.el; e:=st; { запоминаем указатель на текущую вершину } st:=st^.next;{сдвигаем указатель стека на следующий элемент} dispose(e); { возврат памяти в кучу } end;
При восходящем обходе может быть предложен следующий тип стека:
point=^st; st = record next: point; l: integer; add: pointer; end;
Процедура включения элемента в стек при восходящем обходе ( S_Push ) приведена в программном примере 6.12.
{ === Програмнный пример 6.12 ====} Procedure S_Push (var st: point; Har: integer; add: pointer); Var q: point; begin new(q); { выделяем место для элемента } q^.l:=Har; { заносим характеристику } q^.add:=add; { заносим указатель } q^.next:=st; { модифицируем стек } st:=q; end;
Функция извлечения элемента из стека при восходящем обходе (S_Pop) демонстрируется в программном примере 6.13.
{ === Програмнный пример 6.13 ====} Function S_Pop (var st: point; var l: integer):pointer; Var e,p: point; begin l:=st^.l; S_Pop:=st^.add; e:=st; { запоминаем указатель на текущую вершину} st:=st^.next; {сдвигаем указатель стека на след. элемент } dispose(e); { уничтожаем выбранный элемент } end;
ПРОШИВКА БИНАРНЫХ ДЕРЕВЬЕВ.
Под прошивкой дерева понимается замена по определенному правилу пустых указателей на сыновей указателями на последующие узлы, соответствующие обходу.
Рассматривая бинарное дерево, легко обнаружить, что в нем имеются много указателей типа NIL. Действительно в дереве с N вершинами имеется ( N+1 ) указателей типа NIL. Это незанятое пространство можно использовать для изменения представления деревьев. Пустые указатели заменяются указателями - "нитями", которые адресуют вершины дерева, и расположенные выше. При этом дерево прошивается с учетом определенного способа обхода. Например, если в поле left некоторой вершины P стоит NIL, то его можно заменить на адрес, указывающий на предшественника P, в соответствии с тем порядком обхода, для которого прошивается дерево. Аналогично, если поле right пусто, то указывается преемник P в соответствии с порядком обхода.
Поскольку после прошивания дерева поля left и right могут характеризовать либо структурные связи, либо "нити", возникает необходимость различать их, для этого вводятся в описание структуры дерева характеристики левого и правого указателей (FALSE и TRUE).
Таким образом, прошитые деревья быстрее обходятся и не требуют для этого дополнительной памяти (стек), однако требуют дополнительной памяти для хранения флагов нитей, а также усложнены операции включения и удаления элементов дерева.
Прошитое бинарное дерево на Паскале можно описать следующим образом:
type TreePtr:=^S_Tree; S_Tree = record key: KeyType; { ключ } left,right: TreePtr; { левый и правый сыновья } lf,rf: boolean; { характеристики связей } end;
где lf и rf - указывают, является ли связь указателем на элемент или нитью. Если lf или rf равно FALSE, то связка является нитью.
До создания дерева головная вершина имеет следующий вид: Здесь пунктирная стрелка определяет связь по нити. Дерево подшивается к левой ветви.
Рис. 6.27.
В программном примере 6.14 приведена функция ( Inson ) для определения сына (преемника) данной вершины.
{ === Програмнный пример 6.14 ====} (*------ Функция, находящая преемника данной вершины X ----*) (*-------- в соответствии со смешанным обходом ------------*) Funtion Inson (x: TreePtr):TreePtr; begin Inson:=x^.right; if not (x^.rf) then exit; { Ветвь левая ?} while Insonon^.lf do { связь не нить } Inson:=Inson^.left; { перейти по левой ветви } end; { Inson }
В программном примере 6.15 приведена функция (Int) для определения отца (предка) данной вершины.
{ === Програмнный пример 6.15 ====} (*---------- Функция, выдающая предшественника узла ------*) (*------- в соответствии со смеш1анным обходом ------------*) Function Inp (x:TreePtr):TreePtr; begin Inp:=x^.left; if not (x^.lf) then exit; while Inp^.rf do Inp:=Inp^.right; { связка не нить } end;
В программном примере 6.16 приведена функция, реализующая алгоритм включения записи в прошитое дерево ( leftIn ). Этот алгоритм вставляет вершину P в качестве левого поддерева заданной вершины X в случае, если вершина X не имеет левого поддерева. В противном случае новая вершина вставляется между вершиной X и вершиной X^.left. При этой операции поддерживается правильная структура прошивки дерева, соответствующая смешанному обходу.
{ === Програмнный пример 6.16 ====} (*- Вставка p слева от x или между x и его левой вершиной -*) Procedure LeftIn (x,p: TreePtr); Var q: TreePtr; begin (*--------------- Установка указателя ------------------*) p^.left:=x^.left; p^.lf:=x^.lf; x^.left:=p; x^.lf:=TRUE; p^.right:=x; p^.rf:=FALSE; if p^.lf then (*-------- Переустановка связи с предшественником --------*) begin q:=TreePtr(Inp(p)); q^.right:=p; q^.rf:=FALSE; end; end; { LeftIn }
Для примера рассмотрим прошивку дерева приведенного на рис.6.20. при нисходящем обходе.
Машинное представление дерева при нисходящем обходе с прошивкой приведено на рис.6.28. Рис. 6.28. Машинное связное представление исходного дерева, представленного на рис.6.20 при нисходящем обходе с прошивкой
Трассировка нисходящего обхода с прошивкой приведена в табл.6.3.
Рассмотрим на примере того же дерева прошивку при смешанном обходе. Машинное представление дерева при смешанном обходе с прошивкой приведено на рис.6.28.
@ указателя | Узел | Обработка узла | Выходная строка |
PT:=H | H | ||
LPH | A | A | A |
LPA | B | B | AB |
LPB | C | C | ABC |
-LPC | |||
-RPC | D | D | ABCD |
LPD | E | E | ABCDE |
LPE | F | F | ABCDEF |
-LPF | |||
-RPF | G | G | ABCDEFG |
-LPG | |||
-RPG | H | Конец алгоритма |
Рис. 6.29. Машинное связное представление дерева при смешанном обходе с прошивкой
Трассировка смешанного обхода с прошивкой приведена в табл.6.4.
@ указателя | Узел | Обработка узла | Выходная строка |
P:=PT | H | ||
LPH | A | ||
LPA | B | ||
LPB | C | ||
-LPC | C | C | C |
-RPC | B | B | CB |
-RPB | A | A | CBA |
RPA | D | ||
LPD | E | ||
LPE | F | ||
-LPF | F | F | CBAF |
-RPF | E | E | CBAFE |
-RPE | D | D | CBAFED |
RPD | G | ||
-LPG | G | G | CBAFEDG |
-RPG | H | Конец алгоритма |