Алгоритмы обхода двоичных деревьев

Модуль "Деревья" • урок 8 из 11 • ~8 минут

Суть урока. Обход дерева - это детерминированный алгоритм систематического посещения абсолютно всех узлов таким образом, чтобы каждый узел был обработан ровно один раз. Различают обходы в глубину (через рекурсию/стек) и в ширину (через очередь).
Алгоритмы обхода двоичных деревьев на дереве поиска 4, 2, 6, 1, 3, 5 и 7
Один и тот же набор узлов, четыре разных порядка обработки

Алгоритмы обхода двоичных деревьев в глубину (DFS)

Так как каждый узел дерева состоит из трех логических частей (сам Корень, Левое поддерево, Правое поддерево), существует 3 главных способа рекурсивного обхода. Они отличаются лишь моментом, когда обрабатывается (печатается или читается) сам Корень по отношению к своим детям.

1. Прямой (префиксный / Pre-order) обход: КЛП

Порядок: Корень (К) → Левое поддерево (Л) → Правое поддерево (П).
Применение: Создание точной копии (клонирования) дерева, сериализация дерева в файл для последующего восстановления.

2. Симметричный (инфиксный / In-order) обход: ЛКП

Порядок: Левое поддерево (Л) → Корень (К) → Правое поддерево (П).
Применение: Для "Деревьев поиска" именно этот обход выдает все элементы строго в отсортированном по возрастанию порядке.

3. Обратный (постфиксный / Post-order) обход: ЛПК

Порядок: Левое поддерево (Л) → Правое поддерево (П) → Корень (К).
Применение: Уничтожение (освобождение памяти) дерева, так как корень удаляется только после того, как гарантированно и безопасно удалены все его потомки. Также применяется для вычисления математических выражений в деревьях разбора.

Обход в ширину (BFS)

Дерево обходится "слоями" по уровням: сначала корень, затем все дети корня (2-й уровень слева направо), затем внуки (3-й уровень) и так далее.

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

Тренажер: четыре обхода одного дерева

Обойдем дерево поиска 4, 2, 6, 1, 3, 5, 7 всеми четырьмя способами - порядок обхода переключается кнопками.

Порядок обработки:
Стек вызовов:
узел дерева узел текущего шага уже обработан
Обработано: 0 / 7 Глубина: 0 Шаг: 0 / 0

Реализация на языке C (три обхода в глубину)

Обратите внимание, как элегантно и просто выглядит рекурсивный код. Три алгоритма отличаются ровно одной строчкой - тем местом, где происходит обработка узла (Process).

Три обхода дерева: код целиком C, 35 строк
#include <stdio.h>

typedef struct Node {
    int data; 
    struct Node *left, *right; 
} Node;

// Функция полезной нагрузки (обработки) узла
void Process(Node* n) { 
    printf("%d ", n->data); 
}

// 1. Прямой обход (КЛП)
void PreOrder(Node* root) {
    if (root == NULL) return;
    Process(root);         // Сначала Корень (К)
    PreOrder(root->left);  // Затем Левое поддерево (Л)
    PreOrder(root->right); // Затем Правое поддерево (П)
}

// 2. Симметричный обход (ЛКП)
void InOrder(Node* root) {
    if (root == NULL) return;
    InOrder(root->left);   // Л
    Process(root);         // К
    InOrder(root->right);  // П
}

// 3. Обратный обход (ЛПК)
void PostOrder(Node* root) {
    if (root == NULL) return;
    PostOrder(root->left);  // Л
    PostOrder(root->right); // П
    Process(root);          // К
}
А как в Python? Алгоритмы обхода в глубину (DFS) чаще всего пишут через рекурсию. Но в Python есть встроенный лимит на глубину рекурсии (около 1000 вызовов, sys.getrecursionlimit()) для защиты от переполнения стека ОС. Если дерево несбалансированное и вырождается в длинную цепочку из 2000 элементов, рекурсивный обход упадет с RecursionError. В таких случаях в Python (а иногда и в C++ на критичных системах) обходы переписывают с использованием обычного цикла while и искусственного списка-стека.

Коротко: главное

  • Прямой обход (КЛП): Обрабатываем корень, затем спускаемся к детям. Идеально для клонирования или сериализации.
  • Симметричный обход (ЛКП): Корень обрабатывается строго между детьми. В дереве поиска это гарантирует обход элементов в порядке возрастания.
  • Обратный обход (ЛПК): Корень обрабатывается самым последним, после всех потомков. Используется для безопасного удаления дерева из памяти.
  • Обход в ширину (по уровням): Реализуется не рекурсией, а с помощью структуры данных "Очередь".

Проверьте себя

Какой из алгоритмов обхода необходимо использовать для правильного уничтожения дерева и возврата памяти операционной системе?

Как вам урок? Спасибо за отзыв!