Problem overview
The method lowestCommonAncestor receives the root of a binary search tree and two values, p and q, that both occur in the tree. It returns the value of their lowest common ancestor: the deepest node that has both values in its subtree. A node counts as its own ancestor, so if one value sits above the other, the answer is the upper one. The two values can also be equal.
In the example tree, with root 6, children 2 and 8, then 0, 4, 7 and 9 below them, and 3 and 5 under 4:
p = 2,q = 8gives 6, because 2 is on the left of 6 and 8 is on the right.p = 2,q = 4gives 2, because 4 is inside the subtree of 2.p = 5,q = 3gives 4, because they are the two children of 4.
The follow-up asks for O(h) time, where h is the height of the tree, with O(1) extra space. This article's code does that.
Approach: follow the values down
For a general binary tree you would have to search both subtrees. A BST gives more information: at any node, every value smaller than the node's value is in its left subtree, and every larger value is in its right subtree. That lets the values themselves choose the direction.
Start at the root, which is always a common ancestor. Then ask where p and q are relative to the current node:
- Both smaller than the node: both are in the left subtree, so the lowest common ancestor is there as well. Move left.
- Both larger: the mirror case. Move right.
- Otherwise, they are on different sides, or one of them equals the node's value. Going either way would leave one of them behind, so this node is the deepest one that has both. Return it.
The walk never goes back up and never looks at two branches, so each pass goes one level down. It makes at most h passes and needs no stack, which is why no recursion is used.
On the example with p = 5 and q = 3: at 6 both are smaller, so go left to 2. At 2 both are larger, so go right to 4. At 4, the value 5 is larger and 3 is smaller, so the answer is 4.
Step by step
- Create
nodeand set it toroot. It is the node the walk is currently at. - Loop while
nodeis notNone. Each pass looks at one node and either returns or moves one level down. - Both values smaller than
node.val: setnodetonode.left. - Both values larger: set
nodetonode.right, usingelifso only one branch runs. - Anything else: return
node.val. This covers the split case and the case whereporqequals the current node. - Return -1 after the loop. Both values are in the tree, so the loop always returns earlier and this line is never reached. It only makes the function return a number on every path.
The solution in Python
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: int, q: int) -> int:
node = root
while node:
if p < node.val and q < node.val:
node = node.left
elif p > node.val and q > node.val:
node = node.right
else:
return node.val
return -1
The Python details worth a closer look:
pandqare numbers, not nodes. The method compares them tonode.valdirectly, and it returns a value, not a node.while node:A tree node is a normal object and is truthy, and a missing child isNone, which is falsy. The loop stops when the walk falls off the tree.- Rebinding, not mutating.
node = node.leftonly moves the local namenode. The tree isn't changed androotstill refers to the top. - Chained conditions.
p < node.val and q < node.valcan't be shortened top and q < node.val, which tests whetherpis non-zero instead of comparing it. Python does allowmax(p, q) < node.val, which means the same as the two comparisons. - The final
elseneeds no condition. By then neither "both smaller" nor "both larger" holds, and that is exactly the "split or equal" case.
Complexity
Time: O(h). Every pass moves one level down or returns, and a root-to-leaf path has at most h nodes. For a balanced tree with n nodes that is O(log n). For a tree that is a single chain, such as values inserted in increasing order, it is O(n).
Space: O(1). The loop uses one variable, node. A recursive version of the same idea would use O(h) stack space.
Edge cases and common mistakes
- One value is an ancestor of the other. For
p = 2andq = 4the walk reaches 2, where 4 is larger and 2 is not smaller than itself, so neither move applies and 2 is returned. This is why the comparisons are strict. - Equal values. With
p = q = 3the walk goes down to the node holding 3 and returns 3, since a node is its own ancestor. - A single node. A tree holding only 1 with
p = q = 1returns 1. - Using
<=and>=. The walk would move away from a node that equalsporqand could skip past the right answer. - Treating the tree as a general binary tree. Searching both subtrees works but costs
O(n). This code relies on the BST ordering and is wrong if the tree isn't ordered. - Swapped inputs. The order of
pandqdoesn't matter, because both conditions treat them the same.p = 8,q = 2also gives 6.
Testing
On the tree [6, 2, 8, 0, 4, 7, 9, None, None, 3, 5]:
p = 2,q = 8gives6.p = 2,q = 4gives2.p = 5,q = 3gives4.p = 3,q = 3gives3.p = 7,q = 9gives8.p = 0,q = 5gives2.
