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 may even be equal.
Take the tree [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5]. For 2 and 8 the answer is 6, since 2 is in the left subtree of 6 and 8 in the right. For 2 and 4 the answer is 2, since 4 is below 2. For 5 and 3 the answer is 4, their shared parent, which has 3 on its left and 5 on its right.
Approach: let the values choose the direction
In a general binary tree, finding a common ancestor means searching both subtrees. A binary search tree gives more information: everything in a node's left subtree is smaller than the node, and everything in its right subtree is larger. So at any node, comparing p and q with the node's value says where they are.
- If both are smaller than the node's value, both are in the left subtree, and so is the lowest common ancestor. Move left.
- If both are larger, move right.
- Otherwise they are on different sides of the node, or one of them is the node itself. Going down either way would leave one value behind, so this node is the answer.
The root is above everything, so it is always a common ancestor. The walk goes down while the current node is not the lowest one, and stops at the first node that splits the two values. Each step goes one level down, so the walk takes at most as many steps as the tree is tall.
For 5 and 3: the root 6 is larger than both, so move left to 2. Both are larger than 2, so move right to 4. Now 5 is larger than 4 and 3 is smaller, so they split, and the answer is 4.
Step by step
- Declare
nodewithletand set it toroot. It changes as we walk, and TypeScript gives it the type ofroot,TreeNode | null. - Loop with
while (node), which stops whennodeisnull. Inside the loop, TypeScript knowsnodeis a real node, so its fields can be read. - Both values smaller: if
p < node.val && q < node.val, setnodetonode.left. - Both values larger: with
else if, setnodetonode.right. - Otherwise return
node.val. The values split here, or one of them equalsnode.val. - Return -1 after the loop. Both values are in the tree, so the loop always returns first, but the line makes every path return a
number, as the signature promises.
The solution in TypeScript
class Solution {
lowestCommonAncestor(root: TreeNode | null, p: number, q: number): number {
let node = root;
while (node) {
if (p < node.val && q < node.val) {
node = node.left;
} else if (p > node.val && q > node.val) {
node = node.right;
} else {
return node.val;
}
}
return -1;
}
}
The TypeScript details worth knowing:
- Type inference.
let node = root;has no annotation, because TypeScript infersTreeNode | nullfromroot. Assigningnode.leftandnode.right, which are alsoTreeNode | null, is allowed. - Narrowing with
while (node). A node object is truthy andnullis falsy, so the loop condition checks fornull. Inside the body, the compiler treatsnodeas aTreeNode. This is a compile-time convenience only. The safety at run time comes from the condition itself. - The parameters are values, not nodes.
pandqare numbers, so they are compared directly withnode.val. In versions of this problem where they are nodes, you would comparep.valandq.val. - Why the last
return -1is needed. The compiler can't know that the loop always returns. Without a final return, the function could returnundefinedon some path. - Iteration instead of recursion. The loop keeps only the single variable
node, so there is no call stack to grow.
Complexity
Time: O(h), where h is the height of the tree. Each pass moves down one level and the walk never goes back up. For a balanced tree of 1,000 nodes that is about 10 steps. For a tree shaped like a list, h equals the number of nodes.
Space: O(1). Only the variable node is kept.
Edge cases and common mistakes
- One value is an ancestor of the other. For 2 and 4 in the example tree, the walk stops at 2:
p < node.valis false, andp > node.valis false, so theelsereturns 2. - Equal values. With
p = q = 7, the walk goes right at 6 and then reaches 7. Neither strict comparison holds there, so the answer is 7. - A single-node tree. The root is the answer for any
pandq, which must both equal its value. - Using
<=and>=. Non-strict comparisons let the walk move past the node that holdsporq. For 2 and 4 in the example tree, the walk would leave 2 to the right, then turn left at 4 and run off the tree, so the method would return -1 instead of 2. - Comparing only one value. Moving by
palone ignores whereqis, and can go below the split point. - Ignoring the BST property. A search of both subtrees works for any tree but costs
O(n). The ordered values are what let each step discard one subtree.
Testing
On the tree [6, 2, 8, 0, 4, 7, 9, null, null, 3, 5]:
p = 2,q = 8gives6.p = 2,q = 4gives2.p = 5,q = 3gives4.p = 7,q = 7gives7.p = 0,q = 5gives2.p = 3,q = 9gives6.
