Двоичное дерево. Физическое представление. Прошивка

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

Суть урока. Рекурсивный обход дерева требует дополнительной памяти O(h) (где h - высота дерева) для хранения стека вызовов. Чтобы обойти дерево без рекурсии и без дополнительной памяти, можно использовать пустые указатели (листья), записав в них "нити" - адреса следующих узлов при симметричном обходе.
Двоичное дерево. Физическое представление. Прошивка: из 2N ссылок дерева N + 1 пустует
Больше половины ссылок дерева пустует - прошивка занимает именно их

Двоичное дерево. Физическое представление. Прошивка: зачем она нужна

В любом двоичном дереве, состоящем из N узлов, содержится ровно 2N ссылок (по две в каждом узле). Из них N-1 ссылок указывают на реальных детей, а оставшиеся N+1 ссылок равны NULL (это пустые концы у листьев).

Это означает, что больше половины всей адресной памяти дерева простаивает впустую! Алан Перлис и Джозеф Торнтон придумали идею: а что если использовать эти пустые указатели NULL для хранения ссылок на узлы, к которым алгоритм должен перейти дальше (при симметричном обходе)? Такие ссылки назвали нитями (threads).

Изменение структуры узла

Если мы запишем в правый указатель листа адрес следующего узла, как программа поймет - это настоящий сын или нить обхода? Для этого в структуру узла добавляют два булевых флага (по одному биту):

#include <stdbool.h>

struct ThreadedNode {
    int data;
    struct ThreadedNode *left;
    struct ThreadedNode *right;
    bool lthread; // true, если left - это нить (указывает на предка)
    bool rthread; // true, если right - это нить (указывает на потомка)
};

Примечание: В языке C эти булевы флаги можно упаковать в битовые поля, чтобы структура не занимала лишней памяти.

Как строятся нити (на примере симметричного обхода ЛКП)

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

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

Прошитое дерево из чисел 5, 3, 7, 1 и 4: нити ведут к соседям по симметричному обходу
Нити ведут туда же, куда пошел бы симметричный обход

На схеме выше симметричный обход дает последовательность: 1 -> 3 -> 4 -> 5 -> 7. Как мы видим, от листа 1 идет правая нить к узлу 3. От листа 4 идет правая нить к корню 5. Самый левый указатель и самый правый указатель остаются NULL (хотя в некоторых реализациях их закольцовывают на фиктивный узел).

Алгоритм обхода без рекурсии

Обход прошитого дерева сводится к простому циклу while:

  1. Спускаемся по левым ссылкам до упора (в самый левый узел).
  2. Печатаем узел.
  3. Если правая ссылка является нитью (rthread == true), просто переходим по ней и сразу печатаем (поднимаемся наверх).
  4. Если правая ссылка является настоящим сыном (rthread == false), переходим по ней в правое поддерево и снова спускаемся влево до упора.
  5. Повторяем, пока указатель не станет NULL.

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

  • Прошивка использует неиспользуемые `NULL` указатели в листьях для хранения ссылок возврата наверх.
  • Позволяет выполнить обход дерева без использования рекурсии и без дополнительного стека.
  • Требует введения в структуру узла 1-2 дополнительных бит-флагов для отличия нитей от настоящих сыновей.
  • Делает код вставки и удаления сложнее (так как приходится обновлять нити), но обход становится крайне быстрым (линейный цикл while).

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

Сколько пустых указателей (NULL), которые можно использовать для прошивки, изначально находится в бинарном дереве, состоящем из N узлов?

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