Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Билеты на зачет.docx
Скачиваний:
51
Добавлен:
01.06.2015
Размер:
554.66 Кб
Скачать

32. Рекурсивный обход деревьев, удаление рекурсии Рекурсивный Обход Дерева

Как было замечено выше, самым простым способом обхода узлов дерева является рекурсивный метод. Например, следующая программа посещает все узлы бинарного дерева методом «текущий между».

procedure traverse( t : link );beginift <> zthen begintraverse(t^.l); visit(t); traverse(t^.r);end;end;

Программа точно отражает определение метода «текущий между»: Если дерево непустое, то сначала обойти левую ветвь, затем посетить корень, затем обойти правую ветвь. Метод «текущий первым» можно реализовать, поместив вызов к visit перед двумя рекурсивными вызовами, а метод «текущий последним» – поместив его после этих вызовов.

Эта рекурсивная программа обхода дерева гораздо более естественна, чем аналогичная реализация при помощи стека, во-первых, потому, что дерево само по себе – рекурсивно определенная структура, во-вторых потому, что «текущий первым», «между» и «последним» – рекурсивно определенные процессы. Напротив, заметьте, что не существует удобного способа рекурсивного описания поуровнего обхода дерева.

procedure visit( t : link );begin x := x+1; t^.x := x; t^.y := y;end;

procedure traverse( t : link );beginy := y+1;ift > zthenbegintraverse(t^.l); visit(t); traverse(t^.l);end; y := y-1;end;

Простейшие изменения в вышеприведенной программе и соответствующая реализация visit могут позволить подсчитывать самые разнообразные характеристики деревьев в простой и элегантной форме. Например, следующая программа иллюстрирует, как можно подсчитывать координаты для рисования узлов бинарного дерева. Предположим, что узел содержит два целых поля – координаты x и y узла на странице. Во избежание проблем с масштабированием, мы будем считать координаты относительными. Если дерево состоит из N узлов и его высота h, то координата x может изменяться от 1 до N, а координата y – от 1 до h в направлении сверху вниз. Или другими словами ось y направлена вниз, а само дерево заключено в прямоугольнике со сторонами N и h). Следующая программа заполняет эти поля в каждом узле дерева соответствующими значениями:

Программа использует две глобальные переменные x иy,предполагая, что обе они проинициализированы нулем. Переменнаяxхранит количество узлов «посещенных» методом «текущий между», аy – высоту дерева. Каждый раз какtraverseидет вниз по деревуyувеличивается на единицу, и каждый раз как она идет вверх – уменьшается на единицу.

Похожим способом можно написать программу подсчета длины пути дерева, рисования дерева, вычисления выражения представленного в виде дерева выражения и так далее.

Удаление Рекурсии

Как соотносятся рекурсивная и нерекурсивная реализации? Несомненно, они сильно связаны, поскольку на одно и то же входное дерево они генерируют одну и ту же последовательность вызовов процедуры visit. В этом разделе мы изучим этот вопрос более детально посредством «механического» удаления рекурсии из данной выше программы основанной на методетекущий первымс целью получения нерекурсивной программы.

procedure traverse( t : link );beginift <> zthen begintraverse(t^.l); visit(t); traverse(t^.r);end;end;

Это задача, с которой сталкивается компилятор, транслирующий программу в язык машинных команд. Наша задача в данном случае состоит не в том, чтобы изучить технику компиляции (хотя мы и получим некоторое представление об этом), а в том, чтобы изучить взаимосвязь между рекурсивными и нерекурсивными реализациями алгоритмов.

Начнем с рекурсивной реализации метода текущий первым, здесь приведена копия процедуры, данной выше.

Сначала мы удалим второй рекурсивный вызов. Это легко сделать, поскольку после него нет программного кода.

procedure traverse( t : link );label L0, L1;beginL0:ift = zthen goto L1; traverse(t^.l); visit(t); t := t^.r;gotoL0; L1: end;

После того, как второй рекурсивный вызов закончен (вызватьtraverse(с аргументомt^.r), текущийtraverseтакже завершен.

Но та же самая последовательность событий может быть получена с использованием goto вместо рекурсивного вызова:

По сути дела, второй рекурсивный вызов заменяется циклом. Этот метод хорошо известный под названием метод удаления конечной рекурсии, используемый во многих компиляторах. Рекурсивные программы менее живучи на системах без такой возможности, поскольку многие из программ могут работать с огромной неэффективностью, подобной той, что возникала с числами Фибоначчи.

proceduretraverse( t : link );labelL0, L1;beginL0:ift = zthengotoL2; visit(t); push(t); t := t^.l;gotoL0; L2:ifStackEmptythengotoL1; t := Pop; t := t^.r; goto L0; L1:end;

Удаление другого рекурсивного вызова требует дополнительной работы. В общем случае большинство компиляторов генерируют код, который выполняет ту же последовательность действий, что и любой другой вызов процедуры: «Положить локальные переменные и адрес следующей инструкции на стек, установить значение параметров процедуры и перейти на начало процедуры». Когда процедура завершена, она восстанавливает со стека локальные переменные, адрес следующей инструкции и передает по нему управление. На самом деле все гораздо сложнее, тем не менее, мы можем удалить первый рекурсивный вызов этим способом:

Здесь используется только одна переменная t, поэтому мы сохраняем ее на стеке. Адрес возврата тоже только один, поэтому мы не кладем его на стек. В конце процедуры мы восстанавливаемtcо стека и передаем управление на меткуL3(возврат из процедуры). Когда стек пуст, мы возвращаемся из первого вызова кtraverse.

procedure traverse( t : link );beginpush(t);repeat t := pop; while t<>zdo begin visit(t); push(t^.r); t := t^.l; end; until stackempty;end;

Итак, рекурсия удалена, но мы все еще остались с целым набором уродливых goto, компрометирующих хорошую программу. Но и они могут быть «механически» удалены, для получения хорошо структурированной программы. Для начала можно перенести часть программы между меткой L3 и вторым goto L0 на место goto L3, поскольку она стоит в окружении goto. Это позволяет нам избавиться как от самой метки L3, так и от соответствующего goto. После этого часть программы между меткой L0 и первым goto становится ничем иным, как циклом while:

Теперь у нас остался еще один цикл, который можно преобразовать в цикл repeatдобавлением еще одногоpush (изначального значения параметраt), что дает нам программу безgoto.

Эта версия алгоритма – «стандартный» нерекурсивный метод обхода деревьев. Для читателя было бы полезно забыть на момент о том как был выведен этот алгоритм и затратить немного времени на то, чтобы убедиться, что эта программа на самом деле обходит дерево методом текущийпервымкак и было обещано.

Заметим, что структура цикл-внутри-цикла может быть еще немного упрощена (за счет нескольких push на стек):

procedure traverse( t : link ); begin push(t); repeat t := pop; if t<>zthen begin visit(t); push(t^.r); push(t^.l); end; until stackempty;end;

Удивительно, не правда ли? Эта программа стала очень похожа на рекурсивную программу обхода методомтекущийпервым, но в действительности они вомногомотличаются. Самое главное различие состоит в том, что эту программу можно запустить практически в любой среде программирования в то время, как рекурсивную программу можно запустить в среде, которая поддерживает рекурсию. Даже в такой среде, метод основанный на стеке, скорее всего, будет более эффективным.

Наконец, мы замечаем, что эта программа складывает на стек пустые поддеревья, что является прямым результатом нашего решения тестировать изначально в исходной программе, не пустое ли дерево ей было передано. (В противном случае программе пришлось бы проверять t^.l и t^.r и делать рекурсивные вызовы только для непустых ветвей).

procedure traverse( t : link ); begin push(t); repeat t := pop; visit( t ); ift^.r <> z then push( t^.r ); ift^.l <> z then push( t^.l ); untilstackempty; end;

Изменив предыдущую программу с целью избежать ненужные push для пустых поддеревьев, мы получим нерекурсивную программу из предыдущей главы.

Любой рекурсивный алгоритм может быть изменен подобным образом, хотя на самом деле, это задача компилятора. «Ручное» удаление рекурсии подобное тому, что описано здесь, хотя и является сложной задачей, часто ведет к более эффективным нерекурсивным программам и к лучшему пониманию природы рекурсивных вычислений.

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]