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

Для анализа алгоритма мне потребуется некоторое двоичное дерево поиска, допустим, такое:

стандартное дерево поиска ( ключи - натуральные числа )

Рисунок — стандартное дерево поиска ( ключи — натуральные числа )

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

Пусть первой вершиной-потомком будет узел двоичного дерева с ключом $12$:

путь от корня до вершины с ключом $12$

Рисунок — путь от корня до вершины с ключом $12$

Пусть второй вершиной-потомком будет узел двоичного дерева с ключом $67$:

путь от корня до вершины с ключом $67$

Рисунок — путь от корня до вершины с ключом $67$

Для заданных двух вершин с ключами $12$ и $67$ искомым минимальным предком будет элемент бинарного дерева с ключом $50$:

наименьший общий предок для вершин $12$ и $67$

Рисунок — наименьший общий предок для вершин $12$ и $67$

А почему, собственно, вершина с ключом $50$ является наименьшим общим предком?

Наименьшим общим предком двух вершин $u$ и $v$ двоичного дерева поиска называется такая вершина $p$, которая лежит на пути от корневой вершины дерева и до вершины $u$, и до вершины $v$, при этом вершина $p$ максимально удалена от корня дерева.

Ключ вершины Путь от корня до вершины
$12$ $100 -$ $50$ $- 25 — 12$
$67$ $100 -$ $50$ $- 75 — 67$

Сейчас я введу некоторые ограничения на входные данные для алгоритма поиска наименьшего общего предка:

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

Поясню картинкой смысл второго ограничения:

весь путь до вершины $50$ является частью пути до вершины $12$

Рисунок — весь путь до вершины $50$ является частью пути до вершины $12$

➡ Если говорить неформально, то искомый минимальный общий предок должен быть «точкой перелома» пути между двумя заданными узлами-потомками. На рисунке выше это условие не выполняется.

Также в формулировке алгоритма встречается слово «минимальный». О чем речь?

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

➡ Кстати, у корневой вершины нет ни одного предка, так как эта вершина сама является супер-родителем для всех вершин двоичного дерева.

Хочу показать картинку, на которой выделены цветом все предки ( прямой родитель красным, а косвенные — оранжевым ) для вершины с ключом $113$:

все предки для узла с ключом $113$

Рисунок — все родительские вершины для узла с ключом $113$

Минимальный предок — это вершина бинарного дерева поиска, расположенная наиболее близко к заданному узлу-потомку. Очевидно, что этот предок будет наиболее удален от корневого элемента дерева.

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

💡 Переходим к основной идее алгоритма поиска наименьшего общего предка в двоичном дереве поиска.

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

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

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

Вот моя реализация поиска минимального общего родителя в двоичном поисковом дереве на языке программирования «чистый» Си ( стандарт языка C89 ):

И, как обычно, рекурсивное решение компактно, наглядно и предельно строго.

И сейчас я протестирую работу функции Least_common_ancestor() на следующем бинарном дереве поиска:

стандартное двоичное дерево поиска для тестирования

Рисунок — стандартное двоичное дерево поиска для тестирования

В итоге получаю следующие результаты:

тест #1

Рисунок — тест #1

тест #2

Рисунок — тест #2

💡 Добавлю несколько фраз про вычислительную сложность данного алгоритма ( в теории программирования его называют lca ).

Учитывая тот факт, что рассматриваемые двоичные деревья поиска не являются сбалансированными по высоте, можно сделать вывод, что худший случай будет $O( n )$, где $n$ — общее количество вершин дерева.

➡ Также не стоит забывать, что алгоритм, рассмотренный в этой статье-заметке, далеко не единственный алгоритм поиска общего наименьшего предка. Существуют более сложные и хитрые алгоритмы, имеющие гораздо лучшие показатели вычислительной сложности, чем $O( n )$. В текущем же алгоритме все по сути сводится к поиску одной из заданных вершин-потомка.

Пишите в комментариях, что непонятно в рамках рассмотренного алгоритма, а также случаи из вашей практики, когда вам пригодился поиск наименьшего предка в бинарном дереве поиска.

ВНИМАНИЕ Для заказа программы на двоичное дерево ( поиска ) пишите на мой электронный адрес proglabs@mail.ru