6. Двоичные деревья
Типы деревьев: Двоичные (бинарные), ориентированные, упорядоченные, мультивариантные (дерево Байера).
Дерево - динамическая нелинейная структура данных, каждый элемент которой содержит собственно информацию (или ссылку на то место в памяти ЭВМ, гди хранится информация) и ссылки на несколько (не менее двух) других таких же элементов. Для бинарного дерева таких ссылок две: на правый соседний и левый соседний элементы.
Основные операции, выполняемые над деревьями (те же, что и над списками) - вставка, удаление и поиск элементов.
Преимущество бинарных деревьев: в случае, если дерево отсортировано, то операция поиска выполняется быстро, с эффективностью, близкой к эффективности двоичного поиска.
Чем же отличается бинарное дерево от двусвязного списка?
Иногда ничем, не только от двусвязного, но и от односвязного списка.
Основное отличие - у списка соседними являются предшествующий и последующий элементы, и структура линейна; а у бинарного дерева соседними являются элементы с меньшим и большим ключом, и структура, в общем случае ветвящаяся - в виде дерева, откуда она и получила свое название.
Нужно помнить, что дерево - это только метод логической организации информации в памяти, а память - линейна. В случае, если информация хранится в произвольном (и неизвестном программисту) месте памяти, а ключ и информация - разные понятия (иногда они могут и совпадать), структура элемента дерева может быть представлена следующим образом:
Этой структуре соответствует набор типов языка Паскаль:
Бинарное дерево, образованное такими элементами показано там же.
Элемент дерева получил название "узел" (node), иногда его называют листом. Единственный начальный элемент (откуда дерево развивается) - корень (root). Фрагмент (часть) дерева - поддерево или ветвь. Узел, от которого не отходят ветви - конечный или терминальный узел. Количество уровней - высота дерева.
Применение деревьев: для хранения информации, как таковой; в процедурах поиска; для организации файлов на дисковых носителях; в построении эффективных кодов для сжатия информации (коды Шеннона-Фано и Хаффмэна).
Рассмотрим принцип построения дерева при занесении в него информации. Пусть последовательно вводятся записи с ключами 70, 60, 85, 87, 90, 45, 30, 88, 35, 20, 86, 82.
При поступлении очередной записи с ключом k, k сравнивают, начиная с корня, с ключом очередного узла. В зависимости от результата сравнения, процесс продолжают в левой или правой ветви до тех пор, пока не будет достигнут один из узлов, с которым можно связать входящую запись. В зависимости от результата сравнения входящего ключа с ключом этого узла, входящая запись будет размещена слева или справа от него и станет новым терминальным узлом.
Порядок следования узлов дерева зависит от того, каким образом нужно организовать доступ к узлам. Процесс получения доступа ко всем узлам - прохождение дерева.
Существует три способа прохождения дерева:
Под корнем здесь понимается как корень всего дерева, так и корень текущей ветви (поддерева).
Хотя бинарное дерево не обязательно должно быть отсортировано, в большинстве практических случаев это требуется.
Порядок сортировки дерева определяется методом, которым дерево будет проходиться.
Максимальный эффект использования дерева достигается, если оно сбалансировано - когда все узлы, кроме терминальных, имеют и правого, и левого соседа; все поддеревья, начинающиеся с одного и того же уровня, имеют одинаковую высоту.
Сбалансированное бинарное дерево - максимально широкое и низкое.
Обратный случай - вырожденное дерево - выродившееся в линейный односвязный список. Такое дерево получается, если заносимые в него данные упорядочены по-возрастанию, или по-убыванию.
Если данные случайны, то получается дерево, приближенное к сбалансированному.
Дерево - рекурсивная структура данных, так как любое поддерево - это тоже дерево. В связи с этим, наиболее эффективными при работе с деревьями оказываются рекурсивные процедуры. Хотя возможно применение и процедур без рекурсии. Рассмотрим разные варианты построения этих процедур.
Рекурсивная процедура добавления узла в дерево на языке "Паскаль"procedure addnode_r(r,prev:ptr; newkey:integer); begin
begin
r^.left:=nil; r^.right:=nil; r^.key:=newkey; if tree=nil then
begin
exit if newkey<r^.key then
|
Процедура вставляет информацию в бинарное дерево, отслеживая ссылки на узлы и выбирая левые или правые ветви, в зависимости от ключа, пока не будет найдено соответствующее место в иерархии дерева.
Фактически этот процесс сортирует ключ, прежде чем добавить узел в дерево. Это вариант алгоритма сортировки матодом простых вставок. При построении нового дерева или обслуживании уже упорядоченного дерева рекомендуется добавлять новые узлы именно такой процедурой.
Рекурсивная функция построения идеально сбалансированного дерева с заданным числом узлов на языке "Паскаль"function maketree(n_node:integer):ptr; var
newkey,nl,nr:integer;
begin
nr:=n_node-nl-1; {Ввод ключа и данных} read(newkey); new(newnode); with newnode^ do begin
left:=maketree(nl); right:=maketree(nr) maketree:=newnode
tree:=maketree(n); {Первый вызов} |
Рекурсивные процедуры прохождения дерева в последовательном (inorder), нисходящем (preorder) и восходящем (postorder) порядках на языке "Паскаль"procedure inorder(r:ptr); begin
inorder(r^.left); writeln( ... ); {Вывод информации} inorder(r^.right) procedure preorder(r:ptr); begin
writeln( ... ); {Вывод информации} preorder(r^.left); preorder(r^.right) procedure postorder(r:ptr); begin
postorder(r^.left); postorder(r^.right) writeln( ... ); {Вывод информации} |
Аргумент этих процедур - указатель на корень ветви, которую требуется найти. Если нужно найти все дерево, необходим указатель на корень дерева. Выход из рекурсивной процедуры происходит, когда встречается терминальный узел.
Возможно добавление нового узла в дерево и без использования рекурсии:
Нерекурсивная процедура добавления нового узла в дерево на языке "Паскаль"procedure addnode2(skey:integer; var tree:ptr; newdata:^info); var
t:^info;
begin
t:=newdata; {данных} new(s); {новый} s^.key:=skey; s^.left:=nil; {узел} s^.right:=nil; s^.data:=t; if tree=nil then
|
Поиск в бинарных деревьях выполняется достаточно просто:
Программа поиска в бинарномдереве на языке "Паскаль"function search_tree(skey:integer; var tree,fnode:ptr):boolean; var
b:boolean;
p:=tree; if tree<>nil then repeat
if p^.key=skey then
begin
search_tree:=b; fnode:=q; |
Длительность поиска зависит от структуры дерева. Для сбалансированного дерева (изображено на рисунке) поиск аналогичен двоичному - то есть нужно посмотреть не более Log2N узлов.
Для вырожденного дерева (изображено на рисунке) поиск аналогичен поиску в односвязном списке - всреднем нужно посмотрет половину узлов.
Удаление узла из дерева - существенно более сложный процесс, чем поиск, так как удаляемый узел может быть корневым, левым или правым. Узел может давать начало другим деревьям (их может быть от 0 до двух).
Наиболее простым случаем является удаление терминального узла, или узла, из которого выходит только одна ветвь.
Для этого достаточно скорректировать соответствующий указатель у предшествующего узла.
Наиболее трудный случай - удаление корневого узла поддерева с двумя ветвями, поскольку приходится корректировать несколько указателей. Нужно найти подходящий узел, который можно было бы вставить на место удаляемого, причем этот подходящий узел должен просто перемещаться. Такой узел всегда существует.
Если это самый правый узел левого поддерева, то для достижения этого узла нужно перейти в следующий от удаляемого узел по левой ветви, а затем переходить в очередные узлы только по левой ветви до тех пор, пока очередной левый указатель не будет равен nil.
Такой узел может иметь не более одной ветви.
Пример удаления из дерева узла с ключом 50.
Процедура удаления узла должна различать три случая:
Процедура удаления узла (автор - Н. Вирт)
Процедура удаления узла двоичного дерева на языке "Паскаль"procedure delnode(var d:ptr; key:integer); var
procedure del1(var r:ptr); begin
begin
q^.data:=r^.data; q:=r; r:=r^.left else
begin
else
begin
if q^.right=nil then
|
Вспомогательная рекурсивная процедура del1 вызывается только в третьем случае. Она проходит дерево до самого правого узла левого поддерева, начиная с удаляемого узла q^, и заменяет значения полей key и data в q^ соответствующими записями полей узла r^. После этого узел, на который указывает r можно исключить (r:=r^.left).