2020
АЛГОРИТМЫ И СТРУКТУРЫ
ДАННЫХ . Динамические
структуры
ЧУЛЮКОВ В.А.
0
Оглавление
1. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ .................................................................2
2. ССЫЛКИ .............................................................................................................................2
3. СВЯЗАННЫЕ СПИСКИ....................................................................................................5
4. ПРОСМОТР СВЯЗАННОГО СПИСКА ...........................................................................8
5. ОЧЕРЕДИ ..........................................................................................................................10
6. ОБЩИЙ АЛГОРИТМ ДОБАВЛЕНИЯ И ИСКЛЮЧЕНИЯ........................................12
7. РЕКУРСИВНАЯ ОБРАБОТКА СПИСКОВ ..................................................................15
8. ДВУСВЯЗНЫЕ КОЛЬЦА................................................................................................16
9. ДЕРЕВЬЯ...........................................................................................................................20
10. ДВОИЧНЫЕ ДЕРЕВЬЯ ...................................................................................................21
11. ДЕРЕВЬЯ ОБЩЕГО ВИДА.............................................................................................24
1
1. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ
Статическими называются такие данные, которые не меняют свои раз-
меры в течение всего времени своего существования. Регулярный и комбини-
рованный типы языка Pascal – это пример статических данных. Мы всегда мо-
жем определить размер статических данных, взглянув на описание данных,
приведенное в программе.
В противоположность статическим данные динамической структуры ме-
няют свои размеры при выполнении программы. Чтобы понять, где могут ока-
заться полезными данные динамической структуры, обсудим вопросы, связан-
ные с обработкой списков. Каждая компонента списка может быть представ-
лена например, переменной типа объект. Тип объект может быть простым ти-
пом, например символьным или вещественным, или сложным типом: регуляр-
ным или комбинированным. Можно представить список в виде массива:
список: Array[1..размер_списка] of объект;
Но такое представление ставит несколько проблем:
1. Необходимо определить количество компонент списка, реально суще-
ствующих в момент начала работы. Это необходимо для того, чтобы
указать в описании значение размеров списка.
2. Добавлять новые компоненты можно только в конец списка. Исклю-
чать компоненты неудобно из-за того, что в массиве остаются дыры,
которые надо каким-то образом отмечать.
3. Очень трудно соблюдать порядок среди компонентов списка, если мы
только не пойдем на то, чтобы сортировать массив после добавления
каждой новой компоненты.
Существует красивое и эффективное решение этих проблем, связанное
с применением данных такой структуры, которая позволяет добавлять и ис-
ключать компоненты, не заботясь о том, куда поместить новую компоненту
или что происходит со свободным пространством, возникающим после исклю-
чения ненужных компонент. Для создания данных такой структуры будем
пользоваться понятием ссылки.
2. ССЫЛКИ
2
Ссылочный тип – такой же простой тип, какими являются целый, веще-
ственный и булевский типы. Но для этого типа не зарезервировано в языке
никакого специального имени. Определение ссылочного типа выглядит так:
Type
связь = ^объект;
Оно читается так: «тип связь есть ссылка на объект». Символ «^»
(стрелка) означает то, что связь является ссылочным типом. Переменная типа
объект может быть связана с некоторой ссылкой типа связь. Схематически та-
кая ситуация изображается так:
Переменная L на этой схеме имеет тип связь, а тот объект, который связан с
ней , обозначается как L^.
Распределение в памяти при этом выглядит так:
То есть, в какой-то ячейке с адресом N, выделенной операционной системой
для хранения значения переменной L типа связь, хранится адрес ячейки па-
мяти, где будет хранится значение типа объект.
Иногда нужны ссылки, с которыми не связана ни одна из переменных
типа объект. В этом случае будем писать:
L := NIL,
где NIL – служебное слово, обозначающее константу ссылочного типа,
которая определяет пустую ссылку, т.е. ссылку, которая "никуда не указы-
вает". Схема, соответствующая этому случаю:
3
Важно понимать разницу между ссылкой и тем объектом, на который
она ссылается. На следующем рисунке показаны две переменные P и Q типа
связь, ссылающиеся на различные объекты:
В результате выполнения присваивания P:=Q произойдет следующее:
Значение ссылки Q будет присвоено ссылке P. Обе ссылки теперь ссылаются
на квадрат, треугольник оказался потерянным.
С другой стороны, после присваивания P^ := Q^ произойдет следую-
щее:
Теперь мы копируем значение объекта Q^ в объект P^. Ссылки не изменились,
но изменилось значение объекта P^.
Ссылка иногда называется указателем, поскольку она указывает на объ-
ект, а не представляет его. Операция ^ часто называется в литературе опера-
цией разыменования. На предыдущем рисунке указателем является P, а ре-
зультатом разыменования P будет P^, т.е. треугольник.
Для определения данных динамической структуры нужно задать объект,
в состав которого будет входить ссылка. Кандидатами могут быть объекты
трех следующих типов: ссылочного, массива и записи. Ссылочный тип можно
исключить сразу, поскольку объекты этого типа не могут содержать никакой
информации, кроме значения самой ссылки. Компоненты массива должны
4
быть одного типа, что представляет собой довольно значительное ограниче-
ние. Поэтому остановимся на записях. Мы можем определить, например, сле-
дующий объект, содержащий кроме ссылки еще некоторую информацию:
Type
связь = ^ объект;
объект = Record
следующий: связь;
данные: тип_данных
end;
Здесь возникает проблема «курицы и яйца». Что надо определять в
первую очередь: связь или объект? Разработчик Паскаля Н.Вирт предвидел эту
проблему, и поэтому разрешено определять ссылки на объекты перед опреде-
лением самих объектов.
Обратим внимание еще на одну особенность определения ссылочных ти-
пов. Ссылка жестко связана с переменными того типа, для которого она была
определена. Например:
Type
P = ^A;
Q = ^B;
Var
Pv : P;
Qv : Q;
Имея такие описания, запрещается делать присваивания:
Pv:=Qv и Qv:=Pv.
3. СВЯЗАННЫЕ СПИСКИ
Связанный список является простейшим типом данных динамической
структуры. Компоненты связанного списка можно вставлять и исключать про-
извольным образом. Схематически список можно изобразить так:
Здесь изображена структура данных, построенная на основе одиночной
ссылочной переменной «начало» и трех компонент типа «объект». Данные
такой структуры и называются связанным списком.
5
Теперь рассмотрим, как можно строить связанные списки. Предполо-
жим, что ссылочная переменная «начало» принимается в качестве исходной
точки для построения списка, как это изображено на рисунке. Сначала список
должен быть пуст. Поэтому запишем:
начало:=Nil.
Этому соответствует следующая схема:
Теперь надо вставить в список одну компоненту. Компоненты создаются
динамически с помощью процедуры New, аргументом которой является
ссылка. Пусть у нас есть описание:
Var: p: связь.
Выполним процедуру:
New(p)
То есть мы создали пока пустую компоненту с именем p^. Следующим
шагом будет присваивание новой компоненте некоторого значения. Так как p^
- это запись, то обращение к полю «данные» будет выглядеть так: p^.данные.
Предполагая, что в поле «данные» можно поместить одиночный символ, запи-
шем:
p^.данные:=’X’
На следующем шаге заполняем поле «следующий»:
p^.следующий:=начало
6
До этого у переменной «начало» было значение Nil, поэтому в резуль-
тате выполнения последнего оператора присваивания значением поля
p^.следующий будет тоже Nil.
Теперь необходимо выполнить оператор:
начало:=p
Таким образом, построен список, состоящий из одной компоненты.
Добавим в список еще одну компоненту:
New(p);
p^.данные:=’Y’;
p^.следующий:=начало
7
начало:=p
Получен список, состоящий из двух компонент. При этом новая компо-
нента Y вставилась в начало списка.
4. ПРОСМОТР СВЯЗАННОГО СПИСКА
Доступ к первой компоненте списка осуществить легко, т.к. ее имя есть
«начало». Поэтому начало^.данные = ‘Z’. Вторая компонента списка стано-
вится доступной благодаря тому, что в первой компоненте есть ссылка с име-
нем «следующий», т.е. именем второй компоненты будет начало^.следую-
щий^. Поэтому
начало^.следующий^.данные = ‘Y’.
Можно продолжить этот процесс, но для доступа к компонентам длин-
ного списка такой метод совершенно не подходит. Нужен алгоритм динамиче-
ского доступа. Этот алгоритм основан на том, что если p ссылается на некото-
рую компоненту списка, то после выполнения присваивания:
p:=p^.следующий
p будет ссылаться на компоненту, следующую за данной. Выполнение
этого оператора можно выполнять до тех пор, пока значением p не станет nil,
т.е. пока мы не достигнем конца списка. В соответствии с этим, алгоритм про-
смотра списка будет выглядеть так:
8
p:= начало;
while p<>nil do p:=p^.следующий;
Процесс последовательного обращения к компонентам списка называ-
ется просмотром списка.
Следующая программа читает последовательность символов, строит из
них список, а затем печатает их в обратном порядке.
Program переупорядочитьсписок;
Type связь=^объект;
oбъект=record
следующий: связь;
данные: char
end;
var начало, p: связь;
c: char;
begin
read(c);
начало:=nil;
while c<>’.’ do
begin
new(p);
p^.данные:=c;
p^.следующий:=начало;
начало:=p;
read(c)
end;
{просмотр списка}
p:=начало;
while p<>nil do
begin
write(p^.данные);
p:=p^.следующий
end
end.
9
На примере видно, что простой связанный список соответствует так
называемой структуре «последний пришел, первый вышел» или LIFO (Last In-
put First Output) Структура с такими свойствами называется стеком.
5. ОЧЕРЕДИ
Применение простого связанного списка ограничивается из-за недоста-
точно удобного доступа ко всем компонентам списка, кроме первой. Не
трудно реорганизовать список так, чтобы превратить его в очередь.
Очередь – структура. у которой доступна лишь компонента. находяща-
яся в этой очереди наибольшее время. Такие структуры называются FIFO (пер-
вым пришел – первым вышел). Такая структура представляет собой связанный
список, но требуется еще одна ссылка на конец очереди.
связь = ^объект;
объект =record
следующий: связь;
данные: типданных;
end;
var головной, замыкающий: связь;
Следующая процедура исключает их очереди первую компоненту и
настраивает на нее ссылку "первый". Если при этом исключается последняя
компонента очереди и очередь становится пустой, то предпринимаются неко-
торые специальные действия.
procedure убратьизочереди (var первый, головной, замыкающий: связь);
begin
первый:= головной;
if головной<>nil then
begin
головной:=головной^.следующий;
if головной=nil then замыкающий := nil
10
end
end;
Если при вызове процедуры "убратьизочереди" оказывается, что оче-
редь пуста, будет возвращено значение nil, указывающее, что убирать из оче-
реди нечего.
Если очередь становится пустой в результате исключения из нее послед-
ней компоненты, значение nil получают обе переменные – и "головной", и "за-
мыкающий".
Процедура "вставитьвочередь" помещает в конец очереди новую компо-
ненту. Ссылка на новую компоненту содержится в переменной "новый", при-
чем устанавливается эта ссылка в вызывающей программе.
procedure вставитьвочередь (новый: связь; var головной, замыкающий: связь);
begin
if головной = nil then головной := новый
else замыкающий^.следующий := новый;
замыкающий := новый
end;
Представим графически случай непустой очереди:
Из текста процедуры видно, что в случае работы с пустой очередью
также выполняются некоторые специальные действия:
11
6. ОБЩИЙ АЛГОРИТМ ДОБАВЛЕНИЯ И ИСКЛЮЧЕНИЯ
Рассмотрим теперь проблему добавления и исключения компонент,
находящихся в середине списка.
Достаточно просто вставить компоненту в середину списка, когда в
нашем распоряжении имеется ссылка на компоненту, предшествующую дан-
ной в реорганизованном списке. Предположим, что в списке у нас есть компо-
нента ваня^, после которой надо вставить компоненту вася^.
procedure вставитьпосле (ваня, вася: связь);
begin
вася^.следующий := ваня^.следующий;
ваня^.следующий := вася
end;
На рисунке это будет выглядеть так:
Процедура "вставитьпосле" будет правильно работать и в том случае,
если компонента ваня^ будет последней компонентой списка.
12
Труднее реализовать процедуру "вставитьперед", которая используется
в случаях, когда в нашем распоряжении оказывается ссылка на компоненту
списка перед которой надо вставить новую компоненту. Трудности эти свя-
заны с тем, что у нас нет непосредственного доступа к ссылке, подлежащей
модификации при реорганизации списка. Здесь имеется два решения про-
блемы.
Первый способ заключается в следующем. Надо организовать просмотр
списка:
var
здесь: связь;
…………………….
while здесь^.следующий <> ваня do
здесь := здесь^.следующий;
Компонента ваня^ конечно может оказаться первой компонентой списка
и приведенный алгоритм просмотра не найдет ее. Следовательно, надо будет
специально проверять этот выделенный случай.
procedure вставитьперед ( ваня, вася: связь; var начало: связь);
var здесь: связь;
begin
if ваня = начало then
begin
вася^.следующий := ваня;
начало := вася
end
else
begin
здесь := начало;
while здесь^.следующий <> ваня do
здесь := здесь^.следующий;
здесь^.следующий := вася;
13
вася^.следующий := ваня
end
end;
Вторым способом реализации процедуры "вставитьперед" пользуются в
случае, когда количество информации, содержащейся в компонентах списка,
не очень велико. Сначала с помощью процедуры "вставитьпосле" новая ком-
понента помещается в список. Попадает она, конечно, не на свое место. Затем
меняются местами содержимое компонент ваня и вася.
procedure вставитьперед ( ваня, вася: связь);
var временная : типданных;
begin
вася^.следующий := ваня^.следующий;
ваня^.следующий := вася;
временная := ваня^.данные;
ваня^.данные := вася^.данные;
вася^.данные := временная
end;
Если список достаточно длинный, такая версия процедуры "вставитьпе-
ред" оказывается предпочтительнее, так как отпадает необходимость в про-
смотре списка. Первым способом лучше пользоваться в случае короткого
списка, состоящего из компонент большого размера.
С похожей проблемой приходится сталкиваться и при необходимости
исключить из списка некоторую компоненту. Простой операция исключения
будет только тогда, когда у нас имеется ссылка на компоненту, предшествую-
щую в списке исключаемой компоненте. Пусть она имеет имя "предыдущий":
предыдущий^.следующий := предыдущий^.следующий^.следующий
Чаще встречается ситуация, когда ссылка указывает именно на ту ком-
поненту, которая подлежит исключению. При этом для нахождения предыду-
щей компоненты приходится просматривать весь список. Кроме того, нужно
обращать внимание на проблему исключения первой компоненты списка:
14
procedure исключить (ваня: связь; var начало: связь);
var здесь: связь;
begin
if ваня = начало then начало := ваня^.следующий
else begin
здесь := начало;
while здесь^.следующий <> ваня do
здесь := здесь^.следующий;
здесь^.следующий := ваня^.следующий
end
end;
7. РЕКУРСИВНАЯ ОБРАБОТКА СПИСКОВ
Рекурсивное определение списка выглядит так: список может быть
либо пустым, либо состоять из узла, содержащего ссылку на список.
Для обработки списков можно написать рекурсивную процедуру. Ее
структура должна соответствовать определению. То есть процедура должна
иметь условный оператор с двумя ветвями. Одна ветвь будет выполнять дей-
ствия, соответствующие обработке пустого списка. Другая ветвь будет обра-
батывать информацию, содержащуюся в единичном узле, а для обработки
оставшейся части списка процедура должна рекурсивно обращаться к самой
себе. Для иллюстрации возможностей рекурсивной обработки списков рас-
смотрим программу, содержащую две рекурсивные процедуры. Одна из них
используется для чтения последовательности символов, другая – для печати
их в исходном порядке.
program копироватьсписок;
type связь = ^объект
объект = Record
15
следующий: связь;
данные: char
end;
var начало: связь;
procedure добавить (var ссылка: связь);
begin
if ссылка = nil then
begin
new(ссылка);
ссылка^.следующий := nil;
read(ссылка^.данные)
end;
else добавить (ссылка^.следующий)
end;
procedure печататьсписок (ссылка: связь);
begin
if ссылка <> nil then
begin
write (ссылка^.данные);
печататьсписок (ссылка^.следующий)
end
end;
begin
начало := nil;
while not eoln do добавить (начало);
печататьсписок ( начало)
end.
8. ДВУСВЯЗНЫЕ КОЛЬЦА
Несколько изменив структуру списка, можно избавиться от неудобств,
связанных с необходимостью особой обработки специальных случаев. В каж-
дой компоненте списка можно хранить две ссылки: одну на предыдущую ком-
поненту, другую – на следующую. Тогда измененное описание списка будет
выглядеть так:
type
связь = ^объект;
16
объект =record
всс, нсс: связь;
данные: типданных;
end;
Здесь всс – это ссылка на следующую компоненту или ссылка вперед;
нсс – ссылается на предыдущую компоненту и называется ссылкой назад.
Для полной симметрии можно связать первую и последнюю компоненты
списка между собой. В результате получится двусвязное кольцо:
Чтобы упростить процедуры обработки кольца, вводят понятие пустого
кольца. Пустое кольцо – это кольцо, состоящее из фиктивной компоненты,
ссылающейся сама на себя:
Рассмотрим процедуру, которая вставляет объект в произвольное место
кольца после указанного объекта (вставляет объект вася^ после объекта
ваня^):
procedure вставитьпосле (ваня, вася: связь);
begin
вася^.всс := ваня^.всс;
вася^.нсс := ваня;
ваня^.всс^.нсс := вася;
17
ваня^.всс := вася
end;
Рассмотрим теперь процедуру, вставляющую объект вася^ перед объек-
том ваня^.
procedure вставитьперед (ваня, вася: связь);
begin
вася^.всс := ваня;
вася^.нсс := ваня^.нсс;
ваня^.нсс^.всс := вася;
ваня^.нсс := вася
end;
18
Рассмотрим процедуру, исключающую из кольца объект, на который
имеется ссылка:
procedure исключить (ваня: связь);
begin
ваня^.всс^.нсс := ваня^.нсс;
ваня^.нсс^.всс := ваня^.всс
end;
Рассмотренные процедуры работают во всех случаях, и в них совер-
шенно не используется значение переменной "начало", т.е. исходная точка
кольца. Естественно, мы добились этого не бесплатно. Компоненты кольца
стали больше, так как в них появилась вторая ссылка. Процедуры стали рабо-
тать медленнее, так как в них обрабатывается большее количество ссылок. Но
в целом двусвязное кольцо более удобно в работе. Простые операции не тре-
буют последовательного просмотра и не возникает необходимость обработки
специальных случаев.
Поскольку в кольце мы не можем встретиться с пустой ссылкой, имею-
щей значение nil, просмотр кольца отличается от просмотра списка. Нужно
лишь помнить то место, с которого начинается просмотр. Пусть, например,
переменные "кольцо" и "старт" имеют тип "связь". Тогда алгоритм просмотра
вперед будет иметь следующий вид:
кольцо := старт^.всс;
while кольцо <> старт do
begin
S;
кольцо := кольцо^.всс
end;
19
Оператор S будет выполняться по одному разу для каждой компоненты
кольца. В момент выполнения этого оператора переменная "кольцо" будет ука-
зывать на текущую компоненту. Оператор S не будет выполняться вовсе, если
кольцо пустое.
9. ДЕРЕВЬЯ
Ссылки можно использовать не только для представления списков, оче-
редей и колец, но также для представления структур более общего вида. Пред-
положим, что у нас есть структура, состоящая из записей, связанных между
собой системой ссылок. При этом каждая запись может содержать ссылки на
несколько других записей. Так представляются направленные графы. Верши-
нами или узлами графа являются записи, а ссылки играют роль ребер. Частным
случаем графа является дерево, которое имеет следующие свойства:
1. подструктуры, связанные с некоторым узлом, не связаны между со-
бой;
2. существует единственный узел, называемый корнем дерева;
3. из корня, просмотром конечного числа ребер, можно достичь любого
узла дерева.
Узлы Г, Д, Е, И, К, Л называют терминальными узлами или листьями
дерева.
Узел А – это корень дерева.
Дерево – это рекурсивная структура. Его можно определить так:
дерево либо пусто, либо состоит из узла, содержащего ссылки на непересека-
ющиеся деревья. Это определение похоже на рекурсивное определение списка,
20
но, если списки легко обрабатывать как рекурсивными, так и итеративными
алгоритмами, то деревья легче обрабатывать рекурсивными алгоритмами.
10.ДВОИЧНЫЕ ДЕРЕВЬЯ
Каждый узел двоичного дерева имеет не больше двух узлов-отростков.
Если у узла действительно два отростка, то они называются левым и правым
узлом.
Левый и правый узла неравноценны, так что следующие два дерева раз-
ные:
На языке Pascal описание дерева имеет вид:
type
связь = ^узел;
узел = record
левое, правое: связь;
данные: типданных;
end;
Узел и два его отростка могут быть просмотрены шестью разными спо-
собами. Но традиционно считается, что левое поддерево всегда просматрива-
21
ется раньше правого поддерева. Поэтому остается три способа просмотра де-
рева, которые имеют специальные названия: прямой просмотр, обратный
просмотр и концевой просмотр.
При прямом просмотре сначала просматривается узел, а затем левое и
правое поддерево: Д Б А Г В Е З Ж И.
При обратном просмотре просматривается левое поддерево, затем узел,
а потом правое поддерево: А Б В Г Д Е Ж З И.
При концевом просмотре узел просматривается в последнюю очередь: А
В Г Б Ж И З Е Д.
Видно, что обратный просмотр привел к возникновению упорядоченно-
сти узлов по алфавиту. Это не случайное совпадение, так как двоичное дерево
на рисунке заполнено символами именно так, чтобы добиться этого эффекта.
Такое дерево называют двоичным деревом поиска. Такая структура оказы-
вается очень полезной в программах, связанных с упорядочиванием данных.
Рассмотрим процедуру, осуществляющую прямой просмотр дерева:
procedure просмотр (дерево: связь);
begin
if дерево <> nil then
begin
исследовать (дерево);
просмотр (дерево^.левое);
просмотр (дерево^.правое)
end
end;
Оператор "исследовать(дерево)" может, например, печатать содержимое
информационной части узла.
Другие виды просмотра могут быть получены путем перестановки трех
операторов, входящих во внутренний составной оператор.
Следующая процедура добавляет к двоичному дереву поиска один узел,
сохраняя упорядоченность дерева:
procedure вставить (var дерево: связь; новыеданные: типданных);
begin
if дерево = nil then
begin
new(дерево);
with дерево^ do
22
begin
левое := nil;
правое := nil;
данные := новыеданные
end
end
else with дерево^ do
if новыеданные < данные then
вставить (левое, новыеданные)
else if новыеданные > данные then
вставить (правое, новыеданные)
else {сообщение о дублировании данных}
end;
Следующая функция выдает в результате работы ссылку на узел дерева,
содержащий необходимые данные, или пустую ссылку, если нужная инфор-
мация в дереве отсутствует:
function найти (дерево: связь; ключ: типданных): связь;
begin
if дерево = nil the найти := nil
else with дерево^ do
if ключ < данные then найти := найти (левое, ключ)
else if ключ > данные then найти := найти (правое, ключ)
else найти := дерево
end;
Единственной операцией, требующей значительных усилий при про-
граммировании, является операция исключения узла, не являющегося узлом
дерева.
Двоичное дерево поиска представляет собой более удобную структуру
для хранения и поиска данных, чем массив. При работе с таким деревом надо
предварительно убедиться, что новые данные не поступают в порядке убыва-
ния или возрастания. Иначе дерево вырождается в линейный список. Однако
во многих практических случаях данные поступают в существенно случайном
порядке, что позволяет строить эффективные деревья поиска. В случайном де-
реве, состоящем из N узлов, время, нужное для добавления или исключения
узла, пропорционально log2(N). Соответствующее время проведения линей-
ного поиска по массиву пропорционально N.
23
11.ДЕРЕВЬЯ ОБЩЕГО ВИДА
Рассмотрим теперь проблему представления деревьев, узлы которых мо-
гут содержать ссылки более, чем на два поддерева. Если максимальное число
поддеревьев ограничено малым значением, то практически бывает возможно
для указания на поддеревья пользоваться массивом ссылок. В этом случае опи-
сание структуры данных будет выглядеть так:
const
максимальноечислоузлов = 6;
type
связь = ^узел;
узел = record
подузел: array [1..максимальноечислоузлов] of связь;
данные: типданных;
end;
Заранее устанавливая значение максимального числа узлов, мы вносим
в программу довольно серьезное ограничение. Если сделать это значение
слишком малым, то рано или поздно возникает ситуация, когда дерево пред-
ставить невозможно. Если же значение максимального значения поддеревьев
сильно увеличить, возрастает объем памяти, теряемой из-за неиспользуемых
ссылок.
Можно предложить альтернативное представление, заключающееся в
том, что каждый узел дерева должен содержать ссылку на связанный список
его поддерева. При таком представлении каждый узел будет содержать только
две ссылки, каждая из которых может быть пустой. Первая указывает на бли-
жайший из узлов-отростков данного узла, а вторая – на один из нескольких
узлов того же уровня, что и данный.
24
На рисунке – ссылки на поддеревья-отростки показаны вертикальными
линиями. Горизонтальными линиями показаны ссылки на поддеревья, начина-
ющиеся от узлов, имеющих общий предшествующий узел с данным. Таким
образом, мы преобразовали наше дерево в двоичное.
25