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 p sits above q, the answer is p. The two values may also be equal.
The tree in the examples is:
6
/ \
2 8
/ \ / \
0 4 7 9
/ \
3 5
For 2 and 8 the answer is 6: one value is in the left subtree and the other in the right, and no deeper node has both. For 2 and 4 the answer is 2, because 4 lies below 2 and a node is its own ancestor. For 5 and 3 the answer is 4, the node whose two children they are.
Approach: let the values choose the direction
In a binary search tree, every value in a node's left subtree is smaller than the node's value, and every value in its right subtree is larger. That ordering tells us where a value lives without searching.
Start at the root. It is above everything, so it is a common ancestor, but maybe not the lowest. Compare p and q with the current node's value:
- If both are smaller, both live in the left subtree, so the lowest common ancestor is there too. Move to the left child.
- If both are larger, move to the right child.
- Otherwise the values are on different sides of the node, or one of them equals the node's value. Going down to either side would leave one of the values behind, and this node has both below it (or is one of them). This is the answer.
The walk moves one level per step and never goes back, so it takes at most as many steps as the tree is tall, O(h). It needs no recursion and no stack, so the extra space is O(1): this meets the follow-up's goal.
Trace p = 5, q = 3 on the tree above. At 6, both are smaller, so go left to 2. At 2, both are larger, so go right to 4. At 4, 3 is smaller and 5 is larger: they are on different sides, so the answer is 4.
Step by step
- Declare
nodewithvarand set it toroot. It changes as the walk goes down. - Loop while
node != null. Each pass looks at one node and either stops or moves down by one level. - Both smaller: go left. If
p < node.value && q < node.value, setnode = node.left. - Both larger: go right. Otherwise, in an
else if, whenp > node.value && q > node.value, setnode = node.right. - Otherwise return the node's value. The
elsecovers values on both sides and the case where one of them is this very node. In both cases it is the deepest node with both below it or at it. - Return -1 after the loop. Because both values are in the tree, the loop always returns first. The line exists because the method must produce an
Inton every path.
The solution in Scala
class Solution {
def lowestCommonAncestor(root: TreeNode, p: Int, q: Int): Int = {
var node = root
while (node != null) {
if (p < node.value && q < node.value) {
node = node.left
} else if (p > node.value && q > node.value) {
node = node.right
} else {
return node.value
}
}
return -1
}
}
The Scala details worth knowing:
- Values, not nodes. Here
pandqareInts, and the method returns anInt. There is no need to find the nodes first. The comparison withnode.valueis enough. if/else if/elseas statements. Each branch either assigns tonodeor leaves the method withreturn. Theifis used for its effects here, and its value is dropped.returninside the loop. A value found in the middle of a loop can only leave the method throughreturn. The finalreturn -1is the other exit, which the loop never reaches for valid input.nullchildren. A missing child isnull, sonode != nullasks whether there is still a node to look at.var node = rootmakes a new reference to the root, so reassigningnodenever changes the tree.
Complexity
Time: O(h), where h is the height of the tree. Each pass moves one level down, and the walk stops at the lowest common ancestor, which is no deeper than the deepest leaf. For a balanced tree this is about log n. For a tree shaped like a list it is n.
Space: O(1). Only the reference node is kept. A recursive version would use O(h) stack space for the same walk.
Edge cases and common mistakes
- One value is an ancestor of the other. For 2 and 4 the walk reaches node 2, where 2 is not smaller than 2. Neither branch for "both smaller" or "both larger" applies, so the method returns 2.
- Equal values. With
p = q = 7, the walk goes right at 6, reaches 8, goes left to 7 and stops there, since7 < 7is false and so is7 > 7. The answer is 7. - A single-node tree. The only possible values are the root's own, and the method returns it right away.
- Using
<=or>=in the first two tests. This would send the walk below a node that equalsporq, past the real answer. - Using
||instead of&&. The walk must move only when both values are on the same side. With||it would move when just one is, losing the other. - Searching the whole tree. A general binary tree needs a search of both subtrees. The ordering of a BST removes that need.
- Different sides with unequal depths. For 0 and 5 the answer is 2, and for 9 and 7 the answer is 8, whatever the depths of the values.
Testing
On the tree from the examples:
p = 2,q = 8gives6.p = 2,q = 4gives2.p = 5,q = 3gives4.p = 7,q = 7gives7.p = 0,q = 5gives2.p = 9,q = 7gives8.
