Хорошо, давайте начнем с определения наихудшего варианта для этого алгоритма. covers
ищет дерево слева направо, поэтому вы получаете наихудший вариант поведения, если искомый узел является самым правым листом или его нет в поддереве вообще. В этот момент вы посетили все узлы в поддереве, поэтому covers
равно O (n) , где n - количество узлов в дереве.
Аналогично, commonAncestor
демонстрирует поведение в худшем случае, когда первый общий предок p
и q
находится глубоко внизу в дереве. В этом случае он сначала вызовет covers
дважды, получая наихудшее время в обоих случаях. Затем он снова вызовет себя на правом поддереве, которое в случае сбалансированного дерева имеет размер n/2
.
Предполагая, что дерево сбалансировано, мы можем описать время выполнения с помощью отношения повторения T(n) = T(n/2) + O(n)
. Используя основную теорему, мы получаем ответ T(n) = O(n)
для сбалансированного дерева.
Теперь, если дерево не сбалансировано, мы могли бы в худшем случае только уменьшить размер поддерева на 1 для каждого рекурсивного вызова, что приведет к повторению T(n) = T(n-1) + O(n)
. Решение этой проблемы: T(n) = O(n^2)
.
Вы можете добиться большего, чем это, хотя.
Например, вместо простого определения, какое поддерево содержит p
или q
с помощью cover
, давайте определим полный путь к p
и q
. Это займет O(n)
, как и cover
, мы просто храним больше информации. Теперь пройдите эти пути параллельно и остановитесь там, где они расходятся. Это всегда O(n)
.
Если у вас есть указатели от каждого узла на их родителя, вы можете даже улучшить это, сгенерировав пути «снизу вверх», что даст вам O(log n)
для сбалансированного дерева.
Обратите внимание, что это компромисс между пространством и временем, поскольку, пока ваш код занимает O(1)
пробел, этот алгоритм занимает O(log n)
пробел для сбалансированного дерева и O(n)
пробел в целом.