ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ Федеральное государственное образовательное учреждение высшего профессионального об...
8 downloads
160 Views
384KB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ Федеральное государственное образовательное учреждение высшего профессионального образования «ЮЖНЫЙ ФЕДЕРАЛЬНЫЙ УНИВЕРСИТЕТ»
АМЕЛИНА Н.И., ЧЕРДЫНЦЕВА М.И. СТРУКТУРЫ ДАННЫХ. ДЕРЕВЬЯ
МЕТОДИЧЕСКИЕ УКАЗАНИЯ по курсу «Языки программирования и методы трансляции» для студентов 1 и 2 курсов дневного и вечернего отделений факультета математики, механики и компьютерных наук
Ростов-на-Дону 2007 3
Методические указания разработаны сотрудниками кафедры прикладной математики и программирования: кандидатом технических наук, доцентом М.И. Чердынцевой и старшим преподавателем Н.И. Амелиной.
В методических указаниях даны основные понятия и определения для деревьев, бинарных деревьев, деревьев поиска и сбалансированных деревьев; приведены примеры реализации на языке Паскаль итеративных и рекурсивных алгоритмов обработки деревьев, а также упражнения для самостоятельной работы. Методические указания предназначены для студентов, изучающих курс «Языки программирования и методы трансляции», и преподавателей, ведущих занятия по «Практикуму на ЭВМ» на 1 и 2 курсах дневного и вечернего отделений факультета математики, механики и компьютерных наук.
Печатается в соответствии с решением кафедры прикладной математики и программирования факультета математики, механики и компьютерных наук ЮФУ, протокол № 9 от 31мая 2007г.
4
СОДЕРЖАНИЕ 1 Основные понятия и определения . . . . . . . . . . . . . . . . . . . . . . . .
4
2 Двоичные деревья . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
3 Основные операции с двоичными деревьями . . . . . . . . . . . . . . .
9
3.1 Обход дерева . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
3.2 Обработка узлов дерева . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14
4 Дерево поиска ( сортировки ) . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19
4.1 Построение дерева поиска . . . . . . . . . . . . . . . . . . . . . . . . . . .
20
4.2 Поиск и включение для дерева сортировки . . . . . . . . . . . . .
24
4.3 Исключение из дерева поиска . . . . . . . . . . . . . . . . . . . . . . . .
27
5 Сбалансированные деревья . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
30
ЛИТЕРАТУРА . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34
5
1. ОСНОВНЫЕ ПОНЯТИЯ И ОПРЕДЕЛЕНИЯ Деревья, в частности бинарные деревья, представляют собой одну из базовых структур данных в программировании. Они используются в компиляторах, системах управления базами данных, файловых системах. Для многих прикладных задач использование древовидной структуры представления информации позволяет существенно повысить временные характеристики алгоритмов обработки. Деревья имеют ярко выраженную рекурсивную структуру. Рекурсивные алгоритмы при работе с деревьями получаются более компактными, элегантными, они легче для понимания, чем итеративные алгоритмы. Деревья часто встречаются в повседневной жизни и хорошо известны по генеалогическим деревьям (рисунки 1 и 2) и структурам с иерархической организацией.
Определим дерево как непустое множество T элементов – узлов ( или вершин ) таких, что а) имеется единственный особый узел, называемый корнем данного дерева; б) остальные узлы содержатся в m >= 0 попарно не пересекающихся множествах T1 , ... , Tm , каждое из которых в свою очередь является деревом. 6
Это определение является рекурсивным, т.е. дерево определяется в терминах самих же деревьев. Можно дать и нерекурсивное определение дерева, но рекурсивное определение более подходящее, т.к. рекурсивность является естественной характеристикой структур типа дерево. Точка зрения на деревья как объекты, состоящие из меньших деревьев (поддеревьев), очень важна и часто используется при решении задач, связанных с деревьями. Существует несколько способов изображения структуры дерева. Структура, представленная в виде графа (рисунок 3) и явно отражающая разветвления, привела к появлению термина “дерево”. При этом принято корень дерева изображать как верхнюю вершину.
7
То есть дерево можно определить как граф, в котором а) имеется одна особая вершина, называемая корнем, в которую не заходит ни одно ребро; б) во все остальные вершины заходит ровно одно ребро, а исходит сколько угодно ребер. По аналогии с деревьями ребра называют ветвями, а вершины на концах ветвей – листьями. Стандартная терминология для структур типа дерева обязана своим происхождением родовой схеме (рисунок 1) генеалогического дерева. Термины предок и потомок для обозначения родства могут простираться на несколько уровней дерева. Итак, терминология, используемая в связи с деревьями, включает следующие понятия: Узел или вершина – каждый элемент дерева. Поддерево называют потомком по отношению к своей вершине – предку. Корень – особая вершина дерева, не имеющая предка. Каждый узел, кроме корня, имеет единственного предка. Вершина связана с каждым из своих поддеревьев ветвью. Узлы, не имеющие поддеревьев ( потомков ), называются листьями или терминальными вершинами. Узлы, не являющиеся терминальными, называются внутренними. Число непосредственных потомков внутреннего узла называется его степенью. Максимальная степень всех узлов есть степень дерева. Вершина (например, D на рисунке 3), находящаяся непосредственно ниже другой вершины (B на рисунке 3), называется непосредственным ее потомком;
8
если непосредственный предок находится на уровне h, то его непосредственный потомок лежит на уровне h + 1 . Корень дерева находится на нулевом уровне. Максимальный уровень какой-либо из вершин дерева называется его глубиной или высотой. То есть глубина дерева определяется числом вершин в самом длинном из путей от корня дерева до листьев. Количество ветвей, по которым нужно пройти от корня дерева до некоторой вершины, называется длиной пути до этой вершины. Упорядоченное дерево – это дерево, у которого ветви, исходящие из каждой вершины, упорядочены. Замечание. Сама природа представления данных в компьютере устанавливает точный порядок для всякого дерева, поэтому будем рассматривать только упорядоченные деревья. 2. ДВОИЧНЫЕ ДЕРЕВЬЯ Особенно важную роль играют упорядоченные деревья второй степени. Их называют двоичными или бинарными деревьями. Определим двоичное дерево как конечное множество элементов – узлов, которое или пусто, или состоит из корня и двух непересекающихся двоичных деревьев, называемых левым и правым поддеревьями данного корня. Заметим, что двоичное дерево не является частным случаем дерева, хотя эти два понятия связаны между собой. Основные отличия между ними: 1) Дерево никогда не бывает пустым, т.е. имеет, по меньшей мере, один узел, а двоичное дерево может быть пустым. 2) Двоичное дерево - упорядоченное дерево, т.е. делается различие между левым и правым поддеревом, даже в том случае, когда узел имеет лишь одного потомка. В графическом изображении дерева (рисунок 4) “наклон” ветвей важен. 9
Реализация таких рекурсивных структур, как двоичные деревья, приводит к использованию ссылок (указателей). Ссылки на пустые деревья будут обозначаться nil. Из определения двоичных деревьев следует естественный способ их описания ( и представления в компьютере ) : для этого достаточно иметь две связи L и R в каждом узле и переменную связи T, которая является указателем на это дерево. Если дерево пусто, то T = nil ; в противном случае T - адрес корня этого дерева, а L и R - указатели соответственно на левое и правое поддеревья этого корня. На языке Паскаль узлы бинарного дерева описываются как записи с одним или несколькими информационными полями и двумя полями – указателями. Если Elem - тип информационной части узлов дерева, то компоненты дерева ( узел и ссылка на узел ) имеют такие типы: type Tree
= ^Node;
{ указатель на узел } { узел дерева }
Node = record inf : Elem; L, R : Tree end;
Таким образом, дерево на рисунке 4 б) можно представить так, как на рисунке 5. 10
Далее будем иметь дело только с двоичными деревьями, поэтому термин “дерево” будет означать двоичное дерево. 3. ОСНОВНЫЕ ОПЕРАЦИИ С ДВОИЧНЫМИ ДЕРЕВЬЯМИ 3.1. Обход дерева Наиболее распространенная задача обработки древовидных структур – выполнение некоторой определенной операции над каждым элементом дерева. При этом происходит “посещение” всех вершин, т.е. обход дерева. При обходе каждый узел проходится, по меньшей мере, один раз, а, вообще говоря, три раза. Полное прохождение дерева дает линейную расстановку узлов. Если, обходя дерево, обрабатывать вершины при первой встрече, то ( см. рис. 4 б) ) получим последовательность A, B, D, E, C, F ; если при второй встрече, то получим D, B, E, A, C, F ; если при третьей встрече, то получим D, E, B, F, C, A. Эти три способа обхода называются соответственно – обходом сверху вниз (в прямом порядке, префиксным обходом, preorder); – обходом слева направо (в обратном порядке, инфиксным обходом, inorder); – обходом снизу вверх (в концевом порядке, постфиксным обходом, postorder или endorder). 11
Способы прохождения деревьев определяются рекурсивно. Если дерево пусто, то никаких действий не выполняется, в противном случае обход выполняется в три этапа Префиксный обход
Инфиксный обход
Обработать узел
Пройти левое поддерево
Пройти левое поддерево
Обработать узел
Пройти правое поддерево
Пройти правое поддерево
Постфиксный обход Пройти левое поддерево Пройти правое поддерево Обработать узел Обход слева направо (инфиксный обход) часто используется при сортировке (см. раздел 4 ). Префиксный и постфиксный способы обхода дерева играют важную роль при анализе текстов на языках программирования. Все три метода легко представляются как рекурсивные процедуры. Пример 3.1. Префиксный обход дерева : procedure PreOrder(T : Tree); begin if T <> nil then begin { операция обработки узла дерева , например, writeln( T^.inf );} PreOrder (T^.L); PreOrder (T^.R) end end;
{ PreOrder } 12
Пример 3.2. Инфиксный обход дерева : procedure InOrder(T : Tree); begin if T <> nil then begin InOrder (T^.L); { операция обработки узла дерева , например, writeln( T^.inf );} InOrder (T^.R) end end;
{ InOrder } Пример 3.3. Постфиксный обход дерева :
procedure PostOrder(T : Tree); begin if T <> nil then begin PostOrder (T^.L); PostOrder (T^.R) { операция обработки узла дерева , например, writeln( T^.inf );} end end;
{ PostOrder } Замечание. Ссылка T передается как параметр - значение, т.е. в проце-
дуре используется ее локальная копия. При реализации нерекурсивных процедур обхода дерева обычно используют вспомогательный стек и операции работы с ним: – очистить стек (создать пустой стек); – проверить, является ли стек пустым; – добавить в стек элемент; – извлечь элемент из стека. 13
В стеке запоминаются ссылки на вершины (поддеревья), обработка которых временно откладывается. Пример 3.4. Описать нерекурсивную процедуру префиксного обхода дерева. Описание вспомогательного стека : type Stack = ^Rec; Rec = record inf_S : Tree; next
: Stack
end; Процедура добавления в стек start элемента T : procedure Puch (var start:Stack; T : Tree); var p:Stack; begin new(p); p^.inf_S:=T; p^.next:=start; start:=p end; Процедура извлечения из стека start элемента T : procedure Pop (var start:Stack; var T : Tree); var p:Stack; begin p:=start; T:=start^.inf_S; start:=start^.next; dispose(p) end; 14
Функция проверки, является ли стек start пустым : function Empty (start:Stack): boolean; begin Empty:=start=nil end; Процедура префиксного обхода дерева : procedure PrefOrd (T : Tree); var start : Stack; flag : boolean; begin start:=nil; { очистить стек } flag:=true; while flag do begin { операция обработки узла дерева , например, writeln( T^.inf );} { перейти к следующему узлу } if T^.L <> nil then { есть левая ветвь } begin { если правая ветвь есть, то ссылку на нее добавить в стек } if T^.R <> nil then Puch(start,T^.R); T:=T^.L
{ по левой ветви вниз }
end else if T^.R <> nil then { есть правая ветвь } T:=T^.R
{ по правой ветви вниз }
{ нет обеих ветвей }
else begin
{ если стек пуст }
if Empty(start) then
flag:=false { конец обхода } 15
else
{ извлечь ветвь из стека и идти по ней }
Pop(start,T) end end {while} end;
{ PrefOrd } Упражнение 3.1. Описать нерекурсивную процедуру а) инфиксного обхода дерева; б) постфиксного обхода дерева. 3.2 Обработка узлов дерева Рассмотрим двоичное дерево, узлы которого содержат в информационной
части целые числа. Назовем их элементами дерева. Пример 3.5. Описать процедуру вывода дерева, выделяющую каждый уровень h с помощью соответствующего отступа. procedure PrintTree(T : Tree; h : integer ); var i : integer; begin if T <> nil then begin PrintTree (T^.R, h+1); for i:=1 to h do write(‘ ‘); writeln( T^.inf ); PrintTree (T^.L, h+1); end end;
{ PrintTree } Здесь используется не инфиксный обход, а обход справа налево, чтобы
дерево не выдавалось в зеркальном отображении. 16
Так как корень дерева Root находится на нулевом уровне, то обращение к процедуре PrintTree будет иметь вид PrintTree(Root,0) Пример 3.6. Описать процедуру MaxEl , определяющую наибольший элемент непустого дерева T. Алгоритм MaxEl использует префиксный обход дерева: наибольший элемент находится или в узле, или в его левом поддереве, или в его правом поддереве. procedure MaxEl(T : Tree; var max : integer); var m : integer; begin if T <> nil then begin max:=T^.inf; if T^.L <> nil then begin MaxEl(T^.L, m); if m > max then max:=m end; if T^.R <> nil then begin MaxEl(T^.R, m); if m > max then max:=m end end end; { MaxEl }
17
Пример 3.7. Описать рекурсивную процедуру Leaf подсчета количества k листьев дерева. procedure Leaf( T : Tree; var k : integer); begin if T <> nil then if (T^.L=nil) and (T^.R=nil) then k:=k+1 else begin Leaf(T^.L,k); Leaf(T^.R,k) end end;
{ Leaf } Количество List листьев дерева Root можно определить, обратившись к
процедуре Leaf : List:=0; Leaf(Root,List); Если описать реализацию в виде функции, то не придется заботиться о присваивании нуля параметру–результату перед ее вызовом. function fLeaf ( T: Tree ) : integer; begin if T=nil then fLeaf:=0 else if (T^.L=nil) and (T^.R=nil) then fLeaf:=1 else fLeaf:= fLeaf(T^.L)+fLeaf(T^.R) end
{fLeaf}; 18
Пример 3.8. Описать рекурсивную функцию Double, которая проверяет, есть ли в дереве T хотя бы два одинаковых элемента. Один из вариантов проверки состоит в использовании функции Count подсчета числа вхождений заданного элемента El в дерево T. Функция Double поочередно проверяет число вхождений текущего значения T^.inf в дерево: если число вхождений больше 1, то результат проверки – истина, если нет, то такая ситуация может возникнуть в левом поддереве или в правом поддереве. function COUNT(T : Tree; El :integer ) : integer; var k : integer; begin if T=nil then
COUNT:=0
else begin if T^.inf = El then k:=1 else k:=0; COUNT:=k+COUNT(T^.L,El)+COUNT(T^.R,El) end end; { COUNT } function Double(T : Tree) : boolean; begin if T=nil then Double:=false else begin if COUNT(T,T^.inf) > 1 then Double:=true else Double:= Double(T^.L) or Double(T^.R) end end; { Double } 19
Пример 3.9. Описать функцию Equal проверки на равенство двух двоичных деревьев одинаковой структуры. function Equal(T1,T2 : Tree ) : boolean; begin if T1=T2 then Equal:=true else if (T1 <> nil) and (T2 <> nil) then if T1^.inf = T2^.inf then Equal:=Equal(T1^.L,T2^.L) and Equal(T1^.R,T2^.R) else Equal:=false else Equal:=false end; { Equal } Пример 3.10.
Описать процедуру
Copy,
которая создает копию T2
дерева T1. procedure Copy(T1 : Tree; var T2 : Tree); begin if T1 = nil then T2:=nil else begin new(T2); T2^.inf:=T1^.inf; Copy(T1^.L,T2^.L); Copy(T1^.R,T2^.R); end end;
{ Copy }
20
Упражнение 3.2. Описать нерекурсивную процедуру или функцию, которая возвращает элемент из самого левого (правого) листа непустого дерева. Упражнение 3.3. Описать нерекурсивную процедуру или функцию, которая а) определяет количество узлов дерева; б) вычисляет сумму (произведение, среднее арифметическое ) всех элементов дерева. в) определяет число вхождений заданного элемента в дерево; г) выдает элементы из всех листьев дерева. Упражнение 3.4. Описать рекурсивную процедуру или функцию, которая а) определяет количество узлов (листьев, внутренних узлов) дерева; б) вычисляет сумму (произведение, среднее арифметическое) всех элементов дерева. в) определяет, входит ли заданный элемент в дерево; г) выдает элементы из всех листьев (внутренних узлов) дерева; д) определяет глубину непустого дерева; е) определяет количество узлов на заданном уровне; ж) удаляет листья дерева со значениями, равными заданному. 4. ДЕРЕВО ПОИСКА ( СОРТИРОВКИ ) Двоичные деревья могут использоваться для представления множества данных, в котором идет поиск элементов по уникальному значению или ключу. Если дерево организовано так, что для каждой вершины все ключи левого поддерева меньше ее ключа, а ключи правого поддерева больше его, то такое дерево называется деревом поиска или деревом сортировки ( рисунок 6 ).
21
4.1 Построение дерева поиска Алгоритм создания дерева поиска основан на следующих правилах : Если новый элемент меньше, чем значение в узле, то он должен добавляться в левое поддерево, если больше, то в правое. Если в дереве есть узел с таким значением, то возможны варианты : а) новый узел добавляется в правое поддерево, б) новый узел добавляется в левое поддерево, в) добавление не происходит. Инфиксный обход такого дерева дает упорядоченную по возрастанию (неубыванию в случае добавления одинаковых элементов ) последовательность. Пример 4.1. Упорядочить по неубыванию последовательность целых чисел, которая вводится из текстового файла. program UP; type Tree
= ^Node;
Node = record inf : integer; L,R : Tree end; var
x : integer; Root : Tree; F : text; imf : string;
{ процедура добавления значения x в дерево поиска T } procedure InTree(var T : Tree; x : integer); var flag : boolean; p,n : Tree;
{ указатели на текущий и новый элементы }
begin { создание нового узла }
new(n); n^.inf:=x; n^.L:=nil; n^.R:=nil; 22
if T=nil then T:=n else begin p:=T; flag:=true; while flag do begin if x < p^.inf then
{ меньший присоединяется слева }
if p^.L=nil then begin p^.L:=n; flag:=false end else p:=p^.L { по левой ветви вниз} else if p^.R=nil then begin { больший или равный присоединяется справа } p^.R:=n; flag:=false end else p:=p^.R
{ по правой ветви вниз }
end end end;
{ InTree }
{ вывод элементов дерева поиска } procedure PrintInf(T : Tree); begin if T <> nil then begin PrintInf(T^.L); writeln(T^.inf); PrintInf(T^.R) end end;
{ PrintInf } 23
begin write('Задайте имя файла - '); readln(imf); assign(F,imf); reset(F); Root:=nil; while not eof(F) do begin read(F,x); InTree(Root,x) end; close(F); writeln('Упорядоченная последовательность чисел :'); PrintInf(Root) end. Если в файле F содержится такая последовательность чисел: 5280963 то программа UP построит дерево поиска, показанное на рисунке 6 а).
24
Другой вариант включения значения x в дерево поиска реализован в процедуре In_Tree( T, x ) : procedure In_Tree(var T : Tree; x : integer); var p,q,n : Tree; {указатели на текущий, предыдущий и новый элементы} begin { создание нового узла }
new(n); n^.inf:=x; n^.L:=nil; n^.R:=nil; if T=nil then T:=n else begin p:=T; while p<>nil do begin
if x < p^.inf then p:=p^.L else p:=p^.R end; if x < q^.inf then q^.L:=n else q^.R:=n end end;
{ In_Tree } Рекурсивная процедура In_Tree_Rec( T,x ) добавления значения x в
дерево поиска соответствует алгоритму: Если новый элемент меньше значения в узле, то он должен добавляться в левое поддерево, иначе (больше или равен) в правое поддерево.
25
procedure In_Tree_Rec(var T : Tree; x : integer); begin if T=nil then begin { создание нового узла }
new(T); T^.inf:=x; T^.L:=nil; T^.R:=nil end else if x < T^.inf
end;
then
In_Tree_Rec( T^.L, x )
else
In_Tree_Rec( T^.R, x )
{ In_Tree_Rec } Упражнение 4.1. Как изменится процедура InTree, если узел с имеющем-
ся в дереве значением будет добавляться в левое поддерево ? Упражнение 4.2. Как изменится процедура In_Tree, если узел с имеющемся в дереве значением будет добавляться в левое поддерево ? Упражнение 4.3. Описать нерекурсивную процедуру включения в дерево неповторяющихся элементов. 4.2. Поиск и включение для дерева сортировки Для дерева сортировки поиск идет по единственному пути от корня к нужной вершине. Поэтому его можно описать с помощью итерации. Пример 4.2. Бинарное дерево с элементами–литерами упорядочено по возрастанию. Определить, имеется ли в нем вершина, содержащая заданную литеру. Если она есть, то возвратить ссылающийся на нее указатель, в противном случае – пустую ссылку nil. 26
function Search_P(T : Tree; ch : char) : Tree; var flag : boolean; { признак удачного поиска } begin flag:=false; while (T <> nil) and not flag do if T^.inf = ch then flag:=true else if ch < T^.inf then T:=T^.L else T:=T^.R; Search_P:=T end; { Search_P } Возможности динамического размещения данных более наглядно проявляются в примерах, где дерево растет или сокращается в ходе выполнения программы. Рассмотрим сначала случай, когда дерево только растет, но не убывает. Типичный пример – построение алфавитного частотного словаря. Задача состоит в чтении текста, выборке из него слов и подсчете частоты их появления. Вначале дерево – пустое. Затем, если слово найдено, то счетчик его вхождений увеличивается на единицу, а если нет, то это - новое слово и оно включается в дерево с единичным значением счетчика. Такой алгоритм часто называют [ 2 ] поиском по дереву с включением. Пример 4.3. Описать процедуру включения слова в частотный словарь. В этом случае тип Elem информационной части inf узлов дерева (см. раздел 2 ) – запись из двух полей: слова ( строки из 20 символов ) и количества его появления в тексте :
27
type t_slovo = string[20]; Elem
= record slovo
: t_slovo;
count_sl : integer end; Путь поиска для дерев слов очевиден. И если он приводит к пустому (nil) поддереву, то заданное слово нужно включить на место пустого поддерева. procedure Slovar(var T : Tree; slovo : t_slovo); begin if T = nil then { включение нового слова }
begin new(T); with T^ do begin
inf.slovo:=slovo; inf.count_sl:=1; L:=nil; R:=nil end end else if slovo < T^.inf.slovo then Slovar(T^.L, slovo) else if slovo > T^.inf.slovo then Slovar(T^.R, slovo) else T^.inf.count_sl:=T^.inf.count_sl+1 end;
{ Slovar } Упражнение 4.4. Описать нерекурсивную логическую функцию, прове-
ряющую, входит ли заданный элемент в дерево поиска. 28
Упражнение 4.5. Описать нерекурсивную процедуру или функцию подсчета числа вхождений заданного элемента в дерево поиска. 4.3 Исключение из дерева поиска Алгоритм исключения из упорядоченного дерева должен описывать три случая: 1) Узла с заданным значением в дереве нет. 2) Узел с заданным значением имеет не более одного потомка, т.е. удаляемый узел или терминальная вершина ( лист ), или вершина с одним потомком. 3) Узел с заданным значением имеет двух потомков. Трудности возникают в случае 3), так как удаляемый узел нужно заменить либо на самый правый узел его левого поддерева, либо на самый левый узел его правого поддерева, причем они должны иметь не более одного потомка. Пример 4.4. Описать процедуру исключения узла с заданным значением x из дерева поиска T . procedure Delete(var T : Tree; x : integer); var q : Tree; procedure Del(var p : Tree); begin if p^.R <> nil then Del(p^.R) else begin q^.inf:=p^.inf; q:=p; p:=p^.L end end; { Del } 29
begin if T = nil then
{ элемента в дереве нет }
else if x < T^.inf then Delete(T^.L, x) else if x > T^.inf then Delete(T^.R, x) else begin
{ исключение узла T^ }
q:=T; if q^.R = nil then T:=q^.L else if q^.L = nil then T:=q^.R else Del(q^.L); { освобождение памяти, выделенной для размещения узла q^ } dispose(q) end end;
{ Delete } Внутренняя процедура Del работает в случае 3). Она "спускается" вдоль
правой ветви левого поддерева узла q^ , который нужно исключить, и заменяет информацию в q^ на информацию из самой правого узла p^ левого поддерева. На рисунке 6 б) изображено дерево, полученное при удалении из дерева на рисунке 6 а) узла со значением 5. Можно реализовать удаление узла для случая 3) и другим способом. Удаляемый узел заменяется любым из поддеревьев, например, левым. Оставшееся правое поддерево добавляется в левое так, чтобы сохранилось свойство дерева поиска. При добавлении правого поддерева в левое поддерево используется алгоритм вставки в дерево поиска. Измененный вариант процедуры исключения Delete1 и вспомогательная процедура вставки в дерево поиска Insert для случая, 30
когда добавляемый элемент является уже готовым узлом (в нашем случае – поддеревом), выглядят следующим образом: procedure Delete1(var T : Tree; x : integer); var q : Tree; procedure Insert(var T : Tree; p: Tree); begin if T = nil then T:=p else if p^.inf < T^.inf then Insert(T^.L,p) else Insert(T^.R,p) end; { Insert } begin if T = nil then
{ элемента в дереве нет }
else if x < T^.inf then Delete1(T^.L, x) else if x > T^.inf then Delete1(T^.R, x) else { исключение узла T^ }
begin q:=T;
if q^.R = nil then T:=q^.L else if q^.L = nil then T:=q^.R else begin T:= q^.L; Insert(T,q^.R) end; { освобождение памяти, выделенной для размещения узла q^ } dispose(q) end end;
{ Delete1 } 31
Результат применения алгоритма Delete1 к дереву, изображенному на рисунке 6 а), выглядит так, как показано на рисунке 6 в). Можно заметить, что при использовании этого алгоритма дерево подвергается большей деформации, чем при применении алгоритма Delete (см. рисунок 6 б) ). Процедура Insert, используемая в Delete1, является универсальной и для данного случая включения может быть упрощена, так как известно, что правое поддерево должно быть подсоединено к самой правой пустой ссылке. В этом случае можно выполнить нерекурсивную реализацию процедуры включения в дерево поиска: procedure Insert1(T : Tree; p: Tree); var s: Tree; begin s:=T; {по условию использования T<>nil} while s^.R <> nil do s:=s^.R; s^.R:=p; end; { Insert1 } Упражнение 4.6. Описать процедуру исключения слова из частотного словаря ( см. раздел 4.2 и пример 4.3 ).
5. СБАЛАНСИРОВАННЫЕ ДЕРЕВЬЯ Дерево называется идеально сбалансированным,
если число вершин
( узлов ) в его левых и правых поддеревьях отличается не более чем на единицу ( рисунок 7 ).
32
Алгоритм построения идеально сбалансированного дерева основан на следующих правилах : Создаем узел дерева. Строим тем же способом левое поддерево. Строим тем же способом правое поддерево. Способ построения определяется поставленной задачей. Процесс построения заканчивается, если исчерпаны данные. Пример 5.1. Построить дерево минимальной глубины, состоящее из n вершин ( на рисунке 7 n = 5,6,7 ). Минимальная глубина при заданном числе вершин достигается, если на всех уровнях, кроме последнего, помещается тоже максимально возможное число вершин. Рекурсивная функция Balance строит идеально сбалансированного дерево с n вершинами, значения которых читаются из файла F : function Balance( n : integer): Tree; var p : Tree; nl,nr : integer; begin if n=0 then p:=nil else 33
begin new(p); with p^ do begin read(F,inf); nl:=n div 2; nr:=n-nl-1; L:=Balance(nl); R:=Balance(nr); end end; Balance:=P end;
{ Balance } Пример 5.2. Построить идеально сбалансированное дерево, изображенное
на рисунке 8 а). Число уровней дерева на рисунке 8 а) – n . Значение k вершины на каждом уровне дерева меняется от 1 до n.
Рекурсивная функция Tree_1n построения дерева T вида 8 а) имеет 3 параметра ( T, n, k ) и может быть описана так :
34
procedure Tree_1n(var T : Tree; n,k : integer); var i : integer; begin if k > n then T:=nil else begin new(T); T^.inf:=k; Tree_1n (T^.L, n, k+1); Tree_1n (T^.R, n, k+1); end end;
{ Tree_1n } Обращение к процедуре Tree_1n для построения дерева Root заданным
способом будет иметь вид:
Tree_1n(Root,n,1);
Процедуры включения и исключения, восстанавливающие идеально сбалансированное дерево, – довольно сложные операции и не всегда оправданы. Менее строгое определение сбалансированного дерева было предложено Г.М. Адельсоном-Вельским и Е.М. Ландисом : Дерево называется сбалансированным тогда и только тогда, когда высоты двух поддеревьев каждой из его вершин отличаются не более чем на единицу. Деревья, удовлетворяющие такому условию, называют равновесными [7] или АВЛ – деревьями. Идеально сбалансированные деревья являются частным случаем АВЛ – деревьев. Процедуры включения и исключения, сохраняющие сбалансированность деревьев, подробно описаны в [ 2, 4 ]. Упражнение 5.1. Описать процедуру построения дерева, изображенного на рисунке 8 а), используя два параметра ( T, n ). Упражнение 5.2. Описать процедуру построения дерева, изображенного на рисунке 8 б). 35
ЛИТЕРАТУРА 1 Абрамов В.Г., Трифонов Н.П., Трифонова Г.Н. Введение в язык паскаль. – М.: Наука,1988.–320 с. 2 Вирт Н. Алгоритмы и структуры данных. – М.: Мир, 1989. –360 с. 3 Вьюкова Н.И., Галатенко В.А., Ходулев А.Б. Систематический подход к программированию. - М.: Наука, 1988.– 208 с. 4 Кнут Д. Искусство программирования для ЭВМ. Том 1. Основные алгоритмы. - М.: Мир, 1976. – 736 с. 5 Кнут Д. Искусство программирования для ЭВМ. Том 3. Сортировка и поиск. - М.: Мир, 1978. – 844 с. 6 Мейер Б., Бодуэн К. Методы программирования: В 2-х томах. Т.1. -М.: Мир, 1982. – 356 с. 7 Мейер Б., Бодуэн К. Методы программирования: В 2-х томах. Т.2. -М.: Мир, 1982. – 368 с. 8 Методы программирования. Учебное пособие / Минакова Н.И., Невская Е.С., Угольницкий Г.А., Чекулаева А.А., Чердынцева М.И. – М.: Вузовская книга, 1999. – 280 с. 9
Пильщиков В.Н. Сборник упражнений по языку Паскаль. – М.: Наука,
1989. – 160 с. 10. Амелина Н.И., Русанова Я.М., Пасечный Л.Г. Языки программирования и методы трансляции. Задания по учебной практике. Методические указания для студентов
2
курса
вечернего
отделения
факультета. – Ростов-на-Дону, УПЛ РГУ, 2006. – 16 с.
36
механико-математического