315 lines
24 KiB
HTML
315 lines
24 KiB
HTML
<html>
|
||
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=windows-1251">
|
||
<title></title>
|
||
<link rel="StyleSheet" href="../../default.css">
|
||
</head>
|
||
|
||
<body>
|
||
|
||
<a name="main"></a>
|
||
|
||
<h1>Задания на обработку деревьев
|
||
</h1>
|
||
<hr>
|
||
<h2>Пример 1. Анализ бинарного дерева
|
||
</h2>
|
||
<hr>
|
||
<p>В заданиях группы Tree, как и в заданиях группы Dynamic, мы встречаемся с двумя новыми видами данных:
|
||
это <i>древовидные динамические структуры</i>, реализованные в виде наборов связанных друг с
|
||
другом записей типа <code>TNode</code>, и <i>указатели</i> типа <code>PNode</code> на записи <code>TNode</code>: <code>PNode =
|
||
^TNode</code>. <a href="..\PT4\IO.html#node">Типы <code>TNode</code> и <code>PNode</code></a> не являются стандартными типами языка Паскаль; они
|
||
определены в задачнике Programming Taskbook.
|
||
</p>
|
||
<p>Особенности, связанные с использованием новых типов данных, рассмотрим на
|
||
примере задания Tree2.
|
||
</p>
|
||
<p class="task">Tree2°.
|
||
Дан адрес <i>P</i><sub>1</sub> записи типа TNode — корня дерева.
|
||
Эта запись связана полями Left и Right с другими записями того же типа (дочерними вершинами),
|
||
они, в свою очередь, — со своими дочерними вершинами, и так далее до записей,
|
||
поля Left и Right которых равны nil (у некоторых вершин может быть равно nil одно из полей Left или Right).
|
||
Вывести количество вершин дерева.
|
||
</p>
|
||
<hr>
|
||
<h3>Создание программы-заготовки и знакомство с заданием</h3>
|
||
<p>Напомним, что программу-заготовку для решения этого задания можно создать с помощью
|
||
команды
|
||
меню «Модули | Создать шаблон программы», кнопки
|
||
<img border="0" src="gif/Load.gif" width="16" height="17"> или клавиатурной
|
||
комбинации <b>Shift+Ctrl+L</b>. Приведем текст созданной заготовки:
|
||
</p>
|
||
|
||
<blockquote><code><pre>
|
||
<b>uses</b> PT4;
|
||
|
||
<b>begin</b>
|
||
Task('Tree2');
|
||
<b>end</b>.
|
||
</pre></code></blockquote>
|
||
<p>После запуска программы на экране появится <a href="../PT4/window.html">окно задачника</a>:
|
||
</p>
|
||
<blockquote>
|
||
<img border="0" src="gif/Tree2-1.png">
|
||
</blockquote>
|
||
|
||
|
||
<p>Это окно содержит в качестве исходных и результирующих данных новые
|
||
элементы: бинарные деревья и указатели.
|
||
</p>
|
||
<p>Начнем с описания того, как отображается на экране <i>дерево</i>. Для его вывода используется
|
||
несколько экранных строк. На каждой строке изображаются вершины дерева,
|
||
находящиеся на определенном уровне (номер уровня указывается слева от изображения дерева).
|
||
Для каждой вершины выводится ее значение, т. е. значение поля Data соответствующей записи типа TNode.
|
||
Любая вершина соединяется линиями со своими дочерними вершинами, расположенными на следующем уровне дерева;
|
||
левая дочерняя вершина изображается слева от родительской вершины, а правая — справа.
|
||
Отсутствие у вершины одной или обеих дочерних вершин означает, что ее поля <code>Left</code> и/или <code>Right</code> равны <code>nil</code>.
|
||
</p>
|
||
<p>
|
||
Рассмотрим в качестве примера дерево, приведенное на рисунке.
|
||
Корень этого дерева имеет значение 15, левая дочерняя вершина корня равна 58,
|
||
правая дочерняя вершина равна 42, глубина дерева равна 4. Все листья дерева находятся на уровнях
|
||
3 и 4; листья на уровне 3 имеют значения 15 и 11, листья на уровне 4 — значения 38 и 84.
|
||
Некоторые из внутренних вершин дерева имеют по две
|
||
дочерние вершины (это корень и вершины со значениями 55 и 20), некоторые по одной:
|
||
левой (вершины 42, 87 и 60) или правой (вершина 58).
|
||
</p>
|
||
<p>Поскольку это дерево указано в разделе исходных данных, следовательно, после инициализации
|
||
задания оно уже существует и размещается в некоторой области динамической памяти.
|
||
Для доступа к данным, размещенным в динамической памяти,
|
||
необходимо знать их адрес, поэтому в любом задании на обработку деревьев в набор исходных данных входят
|
||
<i>указатели</i>, содержащие адреса каких-либо вершин этих деревьев (как правило, указывается адрес корня дерева).
|
||
</p>
|
||
<p>Работа с исходными и результирующими
|
||
данными типа указателя подробно обсуждается в разделе, посвященном
|
||
<a href="ctpointer.html">линейным динамическим структурам</a>.
|
||
</p>
|
||
|
||
</p>
|
||
|
||
<hr>
|
||
<h3>Решение задачи</h3>
|
||
|
||
<p>В задании Tree2 не требуется ни создавать, ни
|
||
преобразовывать исходное дерево;
|
||
его необходимо лишь проанализировать, а именно определить количество его вершин.
|
||
</p>
|
||
<p>Для выполнения этого задания, как и для подавляющего большинства других заданий на обработку деревьев,
|
||
следует воспользоваться вспомогательной <i>рекурсивной</i> подпрограммой (функцией или процедурой).
|
||
Рекурсивная природа алгоритмов,
|
||
связанных с обработкой деревьев (в частности, бинарных деревьев), объясняется тем, что сами
|
||
определения деревьев общего вида и бинарных деревьев являются рекурсивными.
|
||
Так, дать словесное описание функции <code>NodeCount(P)</code>, подсчитывающей число вершин дерева с корнем,
|
||
с которым связан указатель <code>P</code>, можно следующим образом: если указатель <code>P</code> равен <code>nil</code>,
|
||
то следует вернуть значение <code>0</code>; в противном случае следует вернуть значение
|
||
<code>1 + NodeCount(P^.Left) + NodeCount(P^.Right)</code>
|
||
(в этом выражении первое слагаемое соответствует корню дерева, второе —
|
||
его левому поддереву, а третье — его правому поддереву; при этом не требуется проверять,
|
||
что указанные поддеревья существуют, так как при их отсутствии соответствующее слагаемое просто будет равно нулю).
|
||
</p>
|
||
<p>Таким образом, решение задачи будет иметь следующий вид:
|
||
</p>
|
||
<blockquote><code><pre>
|
||
<b>uses</b> PT4;
|
||
<b>function</b> NodeCount(P: PNode): integer;
|
||
begin
|
||
<b>if</b> P = nil <b>then</b>
|
||
result := 0
|
||
<b>else</b>
|
||
result := 1 + NodeCount(P^.Left) + NodeCount(P^.Right);
|
||
<b>end</b>;
|
||
<b>var</b> P1: PNode;
|
||
<b>begin</b>
|
||
Task('Tree2');
|
||
read(P1);
|
||
write(NodeCount(P1));
|
||
<b>end</b>.
|
||
</pre></code></blockquote>
|
||
<p>Цепочка рекурсивных вызовов функции <code>NodeCount</code> завершается при достижении терминальной вершины (листа),
|
||
у которой поля <code>Left</code> и <code>Right</code> равны <code>nil</code>. Благодаря наличию функции <code>NodeCount</code>,
|
||
раздел операторов программы является очень кратким: в нем считывается адрес <code>P1</code> корня исходного дерева,
|
||
после чего вызывается функция <code>NodeCount(P1)</code>, возвращаемое значение которой сразу выводится процедурой write.
|
||
</p>
|
||
<p>Запустив эту программу пять раз, мы получим сообщение «<i>Задание
|
||
выполнено!</i>».
|
||
</p>
|
||
<hr>
|
||
<a name="parent"></a>
|
||
<h2>Пример 2. Бинарные деревья с обратной связью
|
||
</h2>
|
||
<hr>
|
||
<p>Рассмотренная выше реализация бинарных деревьев позволяет легко переходить от родительских вершин
|
||
к их дочерним вершинам, но не допускает обратного перехода. В то же время, для некоторых задач,
|
||
связанных с обработкой деревьев, возможность обратного перехода от потомков к их предку позволяет
|
||
получить более простое решение. Ясно, что для обеспечения возможности обратного перехода
|
||
каждую вершину дерева надо снабдить еще одним полем связи, в котором должна храниться ссылка на
|
||
ее родительскую вершину. Это поле связи естественно назвать <code>Parent</code>. Поскольку корень дерева
|
||
предка не имеет, его поле <code>Parent</code> должно быть равно <code>nil</code>.
|
||
</p>
|
||
<p>Деревья, вершины которых содержат информацию о своих родителях, будем называть <i>деревьями с обратной связью</i>.
|
||
Особенности работы с подобными деревьями рассмотрим на примере задания Tree49.
|
||
</p>
|
||
<p class="task">Tree49°.
|
||
Дан указатель <i>P</i><sub>1</sub> на корень дерева, вершинами которого являются записи типа TNode,
|
||
связанные между собой с помощью полей Left и Right. Используя поле Parent записи TNode,
|
||
преобразовать исходное дерево в <i>дерево с обратной связью</i>, в котором каждая вершина
|
||
связана не только со своими дочерними вершинами (полями Left и Right), но и с родительской
|
||
вершиной (полем Parent). Поле Parent корня дерева положить равным nil.
|
||
</p>
|
||
<p>Запустив программу-заготовку, созданную для задания Tree49,
|
||
мы увидим в области исходных данных изображение «обычного» бинарного дерева,
|
||
в то время как в области результатов будет изображено дерево с обратной связью,
|
||
вершины которого связаны не одинарными, а двойными линиями.
|
||
</p>
|
||
<blockquote>
|
||
<img border="0" src="gif/Tree49-1.png">
|
||
</blockquote>
|
||
|
||
<p>Обратите также внимание на то, что в области результатов отсутствуют какие-либо данные, кроме
|
||
измененного дерева. Это означает, что в программе, решающей задачу, не требуется использовать
|
||
процедуры вывода; достаточно лишь преобразовать исходное дерево требуемым образом.
|
||
Поскольку при таком преобразовании адрес корня дерева <i>P</i><sub>1</sub>
|
||
не изменится, задачник сможеть получить доступ к
|
||
этому дереву и проверить его правильность.
|
||
</p>
|
||
<p>Для преобразования исходного дерева в дерево с обратной связью необходимо задать правильные значения
|
||
для полей <code>Parent</code> всех вершин дерева, перебирая эти вершины с помощью подходящей рекурсивной процедуры.
|
||
В эту процедуру удобно передавать в качестве параметров не только указатель <code>P</code> на текущую вершину,
|
||
но и указатель <code>Par</code> на предка этой вершины:
|
||
</p>
|
||
<blockquote><code><pre>
|
||
<b>uses</b> PT4;
|
||
<b>procedure</b> SetParent(P, Par: PNode);
|
||
<b>begin</b>
|
||
<b>if</b> P = nil <b>then</b>
|
||
exit;
|
||
P^.Parent := Par;
|
||
SetParent(P^.Left, P);
|
||
SetParent(P^.Right, P);
|
||
<b>end</b>;
|
||
<b>var</b> P1: PNode;
|
||
<b>begin</b>
|
||
Task('Tree49');
|
||
read(P1);
|
||
SetParent(P1, nil);
|
||
<b>end</b>.
|
||
</pre></code></blockquote>
|
||
<p>При стартовом запуске рекурсивной процедуры <code>SetParent</code> в качестве
|
||
второго параметра указывается <code>nil</code>.
|
||
</p>
|
||
<p>
|
||
<b>Примечание.</b> Обозначение для двойной связи может оказаться полезным при анализе ошибочного решения.
|
||
Так, если в изображении дерева с обратной связью имеется вершина, соединенная со своей родительским
|
||
вершиной не двойной, а одинарной линией, значит, у этой вершины поле <code>Parent</code> содержит ошибочное значение
|
||
(например, равно <code>nil</code>).
|
||
</p>
|
||
<hr>
|
||
<a name="general"></a>
|
||
<h2>Пример 3. Деревья общего вида
|
||
</h2>
|
||
<hr>
|
||
<p>С помощью связанных записей типа <code>TNode</code> можно моделировать не только бинарные деревья,
|
||
но и произвольные упорядоченные деревья, вершины которых имеют любое число непосредственных потомков
|
||
(будем называть такие деревья <i>деревьями общего вида</i>; для них также используется название
|
||
«деревья с множественным ветвлением»).
|
||
Рассмотрим задание Tree86 — первое из заданий, связанных с деревьями общего вида, в котором
|
||
описываются особенности подобных деревьев.
|
||
</p>
|
||
<p class="task">Tree86°.
|
||
<i>Дерево общего вида</i> (каждая вершина которого может иметь произвольное число дочерних вершин,
|
||
расположенных в фиксированном порядке в направлении слева направо) реализуется с помощью набора
|
||
связанных записей типа TNode следующим образом: для каждой внутренней вершины ее поле Left
|
||
содержит указатель на ее первую (т. е. левую) дочернюю вершину, а поле Right — указатель на ее
|
||
правую <i>сестру</i>, т. е. вершину, имеющую в дереве общего вида того же родителя.
|
||
Поле Right корня дерева общего вида всегда равно nil, так как корень сестер не имеет.
|
||
Дан указатель <i>P</i><sub>1</sub> на корень непустого бинарного дерева. Создать дерево общего вида,
|
||
соответствующее исходному бинарному дереву, и вывести указатель <i>P</i><sub>2</sub> на его корень.
|
||
</p>
|
||
<p>Приведем пример дерева общего вида, которое реализовано с помощью связанных
|
||
записей типа <code>TNode</code> (аналогичным образом деревья общего вида изображаются в окне задачника):
|
||
</p>
|
||
<blockquote>
|
||
<img border="0" src="gif/Tree86-1.png">
|
||
</blockquote>
|
||
|
||
<p>Корень этого дерева (со значением 13) имеет три дочерние вершины (71, 73 и 29),
|
||
причем вершина 71 не имеет потомков, вершина 73 имеет три непосредственных потомка (18, 93 и 92),
|
||
а вершина 29 — два (24 и 84). На последнем уровне располагается вершина 46, являющаяся
|
||
единственной дочерней вершиной вершины 93.
|
||
</p>
|
||
<p>При ознакомительном запуске задания Tree86 на экране появится окно, подобное следующему.
|
||
</p>
|
||
<blockquote>
|
||
<img border="0" src="gif/Tree86-2.png">
|
||
</blockquote>
|
||
<p>Обратите внимание на то, как выглядит одно и то же дерево в двух различных представлениях:
|
||
вариант, соответствующий обычному бинарному дереву, приводится в разделе исходных данных,
|
||
а вариант, соответствующий дереву общего вида, — в разделе результатов.
|
||
При переходе от бинарного дерева к дереву общего вида часть информации о структуре бинарного дерева
|
||
теряется, поскольку в случае, если некоторая вершина дерева общего вида имеет только одного
|
||
непосредственного потомка, нельзя определить, каким был этот потомок в исходном бинарном дереве — левым или правым.
|
||
</p>
|
||
<p>Напомним, что точки, обрамляющие значения вершин в разделе результатов, означают, что
|
||
все эти вершины должны быть созданы программой учащегося (в отличие от вершин исходного дерева, созданных
|
||
самим задачником при инициализации задания).
|
||
</p>
|
||
<p>При формировании нового дерева будем использовать рекурсивную функцию <code>CreateNode(P)</code>.
|
||
Параметр <code>P</code> содержит указатель на вершину исходного дерева, копия которой создается при вызове функции.
|
||
Возвращаемым значением функции является указатель на созданную вершину (как обычно, если <code>P = nil</code>,
|
||
то функция не выполняет никаких действий и возвращает <code>nil</code>). Для создания дочерних вершин выполняется
|
||
рекурсивный вызов этой функции. Заметим, что цепочка дочерних вершин может быть пустой
|
||
(если вершина <code>P</code> является листом), содержать один элемент (если вершина <code>P</code> имеет только одного
|
||
непосредственного потомка) или два элемента. Перед формированием цепочки дочерних вершин удобно
|
||
занести адреса дочерних вершин вершины <code>P</code> во вспомогательные переменные <code>P1</code> и <code>P2</code>. При этом в случае,
|
||
если вершина <code>P</code> имеет только одного потомка (неважно, левого или правого), адрес этого потомка
|
||
заносится в переменную <code>P1</code>, а переменная <code>P2</code> остается равной <code>nil</code>. Благодаря использованию
|
||
переменных <code>P1</code> и <code>P2</code>, фрагмент кода, отвечающий за формирование списка дочерних вершин,
|
||
удается сделать более кратким. Приведем текст программы, решающей задачу Tree86.
|
||
</p>
|
||
<blockquote><code><pre>
|
||
<b>uses</b> PT4;
|
||
<b>function</b> CreateNode(P: PNode): PNode;
|
||
<b>var</b> P1, P2: PNode;
|
||
<b>begin</b>
|
||
<b>if</b> P = nil <b>then</b>
|
||
<b>begin</b>
|
||
result := nil;
|
||
exit;
|
||
<b>end</b>;
|
||
New(result);
|
||
result^.Data := P^.Data;
|
||
result^.Right := nil;
|
||
P1 := P^.Left;
|
||
P2 := P^.Right;
|
||
<b>if</b> P1 = nil <b>then</b>
|
||
<b>begin</b>
|
||
P1 := P2;
|
||
P2 := nil;
|
||
<b>end</b>;
|
||
<i>{ формирование списка дочерних вершин }</i>
|
||
result^.Left := CreateNode(P1);
|
||
<b>if</b> P1 <> nil <b>then</b>
|
||
result^.Left^.Right := CreateNode(P2);
|
||
<b>end</b>;
|
||
<b>var</b> P1: PNode;
|
||
<b>begin</b>
|
||
Task('Tree86');
|
||
read(P1);
|
||
write(CreateNode(P1));
|
||
end.
|
||
</pre></code></blockquote>
|
||
<p>
|
||
<b>Примечание.</b> Фрагмент дерева общего вида, содержащий все дочерние вершины некоторой вершины,
|
||
можно рассматривать как <i>односвязный список</i>, элементы которого связаны между собой
|
||
с помощью поля <code>Right</code> (у последнего элемента списка поле <code>Right</code> равно <code>nil</code>).
|
||
Каждый элемент подобного списка может содержать «подсписок» своих дочерних элементов;
|
||
адрес начала этого подсписка хранится в поле <code>Left</code> данного элемента.
|
||
Поэтому в алгоритмах, связанных с обработкой вершин деревьев общего вида,
|
||
для перебора непосредственных потомков некоторой вершины удобно использовать <i>цикл</i>
|
||
(как при переборе элементов списка), в то время как для обработки каждой дочерней вершины следует,
|
||
как обычно, использовать <i>рекурсию</i>.
|
||
</p>
|
||
|
||
|