Представление и обработка деревьев общего вида
Что такое дерево общего вида?
До сих пор мы рассматривали двоичные деревья, где каждый узел имеет не более двух потомков (левого и правого). Однако в реальных задачах степени ветвления узлов могут быть произвольными и заранее неизвестными.
Классические примеры деревьев общего вида (сильно ветвящихся деревьев):
- Файловая система: Директория может содержать от 0 до нескольких тысяч файлов и поддиректорий.
- HTML DOM: Тег
<body>может содержать десятки<div>, внутри которых произвольное число параграфов. - Реестр Windows: Иерархическое хранилище параметров, где ключи содержат произвольное количество вложенных папок и значений.
Представление и обработка деревьев общего вида: проблема хранения
Как программно описать структуру узла node для такого дерева?
Вариант 1: Массив указателей.
Можно задать массив указателей на сыновей максимального размера: struct node *children[MAX_DEGREE]. Это приведет к гигантскому перерасходу памяти. Если MAX_DEGREE = 500, то для большинства папок, содержащих 2-3 файла, 497 указателей будут просто хранить NULL.
Вариант 2: Связный список потомков.
Внутри каждого узла хранить динамический список (или вектор): List* children. Это решает проблему памяти, но сильно усложняет структуру: дерево начинает зависеть от другой структуры данных (списка), что замедляет работу и делает обходы запутанными.
Преобразование LCRS (Левый сын - Правый брат)
Существует гениальный и крайне простой метод сведения любого дерева общего вида к двоичному дереву без потери информации. Этот метод называется Left-Child Right-Sibling (LCRS). В отечественной литературе его часто описывают правилом: "левое поддерево - старший сын, правое - следующий брат".
Суть алгоритма привязки указателей:
- Указатель
leftуказывает на первого (самого старшего/левого) ребенка данного узла. - Указатель
rightуказывает на следующего родного брата (соседа справа на том же уровне).
Структура узла на C при этом остается точно такой же, как у обычного двоичного дерева:
struct TreeNode {
int data;
struct TreeNode* firstChild; // (left) Указатель на первого ребенка
struct TreeNode* nextSibling; // (right) Указатель на соседа-брата
};
Пример преобразования
Допустим, у нас есть узел A, у которого три ребенка: B, C, D.
A->firstChildуказывает на B. (Потому что B - первый ребенок).- У A нет братьев, поэтому
A->nextSiblingравноNULL. - Кто следующий брат у B? Это C. Значит
B->nextSiblingуказывает на C. - Кто брат у C? Это D.
C->nextSiblingуказывает на D. - У D братьев больше нет,
D->nextSibling = NULL.
Подобно теореме Бойма-Джакопини-Миллса в программировании, это преобразование имеет концептуальное значение: оно доказывает эквивалентность двух разновидностей деревьев. Любое 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). Что это означает?