Представление и обработка деревьев общего вида

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

Суть урока. Дерево общего вида может иметь неограниченное количество ветвей у каждого узла. Хранить такие деревья напрямую (в виде массива указателей на детей) крайне расточительно. Мы разберем элегантный алгоритм приведения любого N-арного дерева к классическому двоичному виду с помощью концепции LCRS (Left-Child Right-Sibling).
Представление и обработка деревьев общего вида: массив указателей, список потомков и пара полей firstChild с nextSibling
Массив расточителен, список громоздок, LCRS обходится двумя указателями

Что такое дерево общего вида?

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

Классические примеры деревьев общего вида (сильно ветвящихся деревьев):

Представление и обработка деревьев общего вида: проблема хранения

Как программно описать структуру узла node для такого дерева?

Вариант 1: Массив указателей.
Можно задать массив указателей на сыновей максимального размера: struct node *children[MAX_DEGREE]. Это приведет к гигантскому перерасходу памяти. Если MAX_DEGREE = 500, то для большинства папок, содержащих 2-3 файла, 497 указателей будут просто хранить NULL.

Вариант 2: Связный список потомков.
Внутри каждого узла хранить динамический список (или вектор): List* children. Это решает проблему памяти, но сильно усложняет структуру: дерево начинает зависеть от другой структуры данных (списка), что замедляет работу и делает обходы запутанными.

Преобразование LCRS (Левый сын - Правый брат)

Существует гениальный и крайне простой метод сведения любого дерева общего вида к двоичному дереву без потери информации. Этот метод называется Left-Child Right-Sibling (LCRS). В отечественной литературе его часто описывают правилом: "левое поддерево - старший сын, правое - следующий брат".

Суть алгоритма привязки указателей:

  1. Указатель left указывает на первого (самого старшего/левого) ребенка данного узла.
  2. Указатель right указывает на следующего родного брата (соседа справа на том же уровне).

Структура узла на C при этом остается точно такой же, как у обычного двоичного дерева:

struct TreeNode {
    int data;
    struct TreeNode* firstChild; // (left) Указатель на первого ребенка
    struct TreeNode* nextSibling; // (right) Указатель на соседа-брата
};

Пример преобразования

Допустим, у нас есть узел A, у которого три ребенка: B, C, D.

Подобно теореме Бойма-Джакопини-Миллса в программировании, это преобразование имеет концептуальное значение: оно доказывает эквивалентность двух разновидностей деревьев. Любое N-арное дерево можно обработать стандартными алгоритмами для двоичных деревьев, просто изменив семантику ссылок left и right.

Обход преобразованного дерева

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

void printChildren(struct TreeNode* parent) {
    if (parent == NULL || parent->firstChild == NULL) {
        printf("Детей нет\n");
        return;
    }
    
    // Переходим к первому ребенку
    struct TreeNode* current = parent->firstChild;
    
    // Идем по цепочке "братьев"
    while (current != NULL) {
        printf("%d ", current->data);
        current = current->nextSibling;
    }
    printf("\n");
}

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

  • Деревья общего вида неудобно хранить напрямую: массивы ведут к перерасходу памяти, а списки усложняют код.
  • Алгоритм LCRS преобразует любое N-арное дерево в двоичное.
  • В LCRS левый указатель ведет к первому потомку узла, а правый - к следующему брату узла.
  • Это преобразование доказывает полную функциональную эквивалентность деревьев с произвольным ветвлением и бинарных деревьев.
  • Обход детей в такой структуре превращается в простую итерацию по связному списку братьев (по правым указателям).

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

В дереве, преобразованном по правилу LCRS (левый сын - правый брат), мы исследуем некий узел X. Выясняется, что у него правый указатель равен NULL (X->nextSibling == NULL). Что это означает?

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