Опубликован: 05.01.2015 | Доступ: свободный | Студентов: 2069 / 0 | Длительность: 63:16:00
Лекция 5:

Рекурсия и деревья

Обход дерева

Прежде чем приступить к изучению алгоритмов, в которых создаются бинарные деревья и деревья общего вида, рассмотрим алгоритмы для реализации самой основной функции обработки деревьев - обхода дерева (tree traversal): имея указатель на дерево, требуется систематически обработать все узлы в дереве. В связном списке переход от одного узла к другому выполняется по единственной ссылке; однако в случае деревьев придется принимать решения, поскольку может существовать несколько ссылок для перехода.

Начнем рассмотрение с обхода бинарных деревьев.

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

  • Прямой обход (сверху вниз), при котором посещается узел, а затем левое и правое поддеревья
  • Поперечный обход (слева направо), при котором посещается левое поддерево, потом узел, а затем правое поддерево
  • Обратный обход (снизу вверх), при котором посещаются левое и правое поддеревья, а затем узел.

Эти методы можно легко реализовать с помощью рекурсивной программы (программа 5.14), которая является непосредственным обобщением программы 5.5 обхода связного списка. Для реализации обходов в другом порядке достаточно соответствующим образом переставить вызовы функций в программе 5.14. Примеры посещения узлов дерева при использовании каждого из порядков обхода показаны на рис. 5.26. На рис. 5.25 приведена последовательность вызовов функций, которые выполняются при вызове программы 5.14 для дерева из рис. 5.26.

Вызовы функций при прямом обходе

Рис. 5.25. Вызовы функций при прямом обходе

Эта последовательность вызовов функций определяет прямой обход для примера дерева, показанного на рис. 5.26.

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

Программа 5.14. Рекурсивный обход дерева

Эта рекурсивная функция принимает в качестве аргумента ссылку на дерево и вызывает функцию visit для каждого из узлов дерева. В приведенном виде функция реализует прямой обход; если поместить вызов visit между рекурсивными вызовами, получится поперечный обход; а если поместить обращение к visit после рекурсивных вызовов - то обратный обход.

void traverse(link h, void visit(link))
  {
    if (h == 0) return;
    visit(h);
    traverse(h ->l, visit);
    traverse(h ->r, visit);
  }
        
Порядки обхода дерева

Рис. 5.26. Порядки обхода дерева

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

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

Полезно также рассмотреть нерекурсивные реализации, в которых используется явный стек. Для простоты мы начнем с рассмотрения абстрактного стека, который может содержать как узлы, так и деревья, и вначале содержит дерево, которое нужно обойти. Затем выполняется цикл, в котором выталкивается и обрабатывается верхний элемент стека, и который продолжается, пока стек не опустеет. Если вытолкнутое содержимое является элементом, мы посещаем его; а если это дерево, мы выполняем последовательность операций вталкивания, которая зависит от требуемого порядка:

  • При прямом обходе заносится правое поддерево, затем левое поддерево, а затем узел.
  • При поперечном обходе заносится правое поддерево, затем узел, а затем левое поддерево.
  • При обратном обходе заносится узел, затем правое поддерево, а затем левое поддерево.

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

В предыдущем абзаце описана концептуальная схема, охватывающая три метода обхода дерева, однако реализации, используемые на практике, несколько проще.

Содержимое стека для алгоритмов обхода дерева

Рис. 5.27. Содержимое стека для алгоритмов обхода дерева

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

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

Четвертая естественная стратегия обхода - просто посещение узлов дерева в порядке, в котором они нарисованы на странице - сверху вниз и слева направо. Этот метод называется обходом по уровням (level -order), поскольку все узлы каждого уровня посещаются вместе, по порядку. Посещение узлов дерева, показанного на рис. 5.26, при обходе по уровням показано на рис. 5.28.

Обход по уровням

Рис. 5.28. Обход по уровням

Эта последовательность показывает результат посещения узлов дерева в порядке сверху вниз и слева направо.

Интересно, что обход по уровням можно получить, заменив в программе 5.15 стек на очередь, что демонстрирует программа 5.16. Для реализации прямого обхода используется структура данных типа "последним вошел, первым вышел " (LIFO); для реализации обхода по уровням используется структура данных типа "первым вошел, первым вышел " (FIFO). Эти программы заслуживают внимательного изучения, поскольку они представляют существенно различающиеся подходы к организации оставшейся невыполненной работы. В частности, обход по уровням не соответствует рекурсивной реализации, связанной с рекурсивной структурой дерева.

Программа 5.15. Прямой обход (нерекурсивная реализация)

Эта нерекурсивная функция с использованием стека функционально эквивалентна ее рекурсивному аналогу - программе 5.14.

void traverse(link h, void visit(link))
  { STACK<link> s(max);
    s.push(h); 
    while (!s.empty())
      {
        visit(h = s.pop());
        if (h ->r != 0) s.push(h ->r);
        if (h ->l != 0) s.push(h ->l);
      }
  }
        

Программа 5.16. Обход по уровням

Замена структуры данных, лежащей в основе прямого обхода (см. программу 5.15), со стека на очередь дает обход по уровням.

void traverse(link h, void visit(link))
  { QUEUE<link> q(max);
    q.put(h);
    while (!q.empty())
      {
        visit(h = q.get());
        if (h ->l != 0) q.put(h ->l);
        if (h ->r != 0) q.put(h ->r);
      }
  }
        

Прямой обход, обратный обход и обход по уровням можно определить и для лесов. Чтобы определения были единообразными, представьте себе лес в виде дерева с воображаемым корнем. Тогда правило для прямого обхода формулируется следующим образом: "посетить корень, а затем каждое из поддеревьев "; а правило для обратного обхода - "посетить каждое из поддеревьев, а затем корень ". Правило для обхода по уровням то же, что и для бинарных деревьев. Непосредственные реализации этих методов - примитивные обобщения программ прямого обхода с использованием стека (программы 5.14 и 5.15) и программы обхода по уровням с использованием очереди (программа 5.16) для бинарных деревьев, которые мы только что рассмотрели. Конкретные реализации не приводятся, поскольку в разделе 5.8 будет рассмотрена более общая процедура.

Упражнения

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


5.80. Приведите содержимое очереди во время обхода по уровням (программа 5.16) дерева с рис. 5.28 в стиле рис. 5.27.

5.81. Покажите, что прямой обход леса эквивалентен прямому обходу соответствующего бинарного дерева (см. лемму 5.4), а обратный обход леса эквивалентен поперечному обходу бинарного дерева.

5.82. Приведите нерекурсивную реализацию поперечного обхода.

5.83. Приведите нерекурсивную реализацию обратного обхода.

5.84. Напишите программу, которая принимает на входе прямой и поперечный обходы бинарного дерева и генерирует обход дерева по уровням.

Александра Боброва
Александра Боброва

Я прошла все лекции на 100%.

Но в https://www.intuit.ru/intuituser/study/diplomas ничего нет.

Что делать? Как получить сертификат?

Никита Андриянов
Никита Андриянов
Владимир Хаванских
Владимир Хаванских
Россия, Москва, Высшая школа экономики
Вадим Рычков
Вадим Рычков
Россия, Москва, МГТУ Станкин