Start learning free

LESSON 347 OF 445 · BINARY TREES

Lowest Common Ancestor in a BST in Rust: O(h) Iterative Walk

Practice O(n) Read the full explanation

Given the root of a binary search tree (BST) and two values p and q that are both in the tree, return the value of their lowest common ancestor.

The lowest common ancestor of p and q is the deepest node that has both of them in its subtree. A node counts as its own ancestor: if the node holding p is above the node holding q, the answer is p.

In a BST, every value in a node's left subtree is smaller than the node's value, and every value in its right subtree is larger.

The trees below are written in level order (top to bottom, left to right), with None for a missing child.

 

Example 1:

        6
      /   \
     2     8
    / \   / \
   0   4 7   9
      / \
     3   5

Input: root = [6,2,8,0,4,7,9,None,None,3,5], p = 2, q = 8
Output: 6
Explanation: 2 is in the left subtree of 6 and 8 is in its right subtree, so 6 is the deepest node that has both below it.

Example 2:

Input: root = [6,2,8,0,4,7,9,None,None,3,5], p = 2, q = 4
Output: 2
Explanation: 4 is in the subtree of 2, and a node counts as its own ancestor.

Example 3:

Input: root = [6,2,8,0,4,7,9,None,None,3,5], p = 5, q = 3
Output: 4
Explanation: 3 and 5 are the two children of 4, one on each side.

 

Constraints:

  • The number of nodes in the tree is in the range [1, 1000].
  • Each node value is between -104 and 104, and all values are different.
  • p and q are both values of nodes in the tree. They can be equal.

 

Follow up: Can you find it in O(h) time, where h is the height of the tree, using only O(1) extra space?
STEP 1 OF 6
    Ctrl + Enter to check

    RUST · BINARY TREES

    Lca In Bst in Rust, explained

    Problem overview

    The function lowest_common_ancestor receives the root of a binary search tree, as an Option<Box<TreeNode>>, and two values p and q that are 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, and p and q may be equal.

    Use the tree [6, 2, 8, 0, 4, 7, 9, None, None, 3, 5]. For p = 2 and q = 8 the answer is 6, because 2 sits in the left subtree of 6 and 8 in the right one, and 6 is the deepest node with both below it. For p = 2 and q = 4 the answer is 2: 4 is in the subtree of 2, and 2 is its own ancestor. For p = 5 and q = 3 the answer is 4, their common parent.

    Approach: let the values choose the direction

    For an ordinary binary tree you would have to search both subtrees. A binary search tree gives much 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 its value tells you where they are without looking.

    Start at the root, which is always a common ancestor. Then at each node there are three cases:

    • Both values are smaller than the node. They are both in the left subtree, so the lowest common ancestor is there too. Move left.
    • Both values are larger. Mirror image: move right.
    • Otherwise the values are on different sides of the node, or one of them equals the node's value. Going down either way would leave one value behind, so this node is the answer.

    Trace p = 5, q = 3. At 6 both are smaller, so go left to 2. At 2 both are larger, so go right to 4. At 4, 5 is larger and 3 is smaller, so 4 is the answer.

    Each step moves one level down, so the walk takes at most as many steps as the tree is tall, O(h). It needs no stack and no recursion, which meets the follow-up's O(1) space goal.

    Step by step

    1. Create the cursor. node is a mutable variable holding root.as_deref(). The method turns Option<Box<TreeNode>> into Option<&TreeNode>, a borrowed view of the nodes, so you can walk the tree without moving anything out of it.
    2. Loop while there is a node. while let Some(current) = node runs the body with current bound to the node and ends when node becomes None.
    3. Go left when both p and q are smaller than current.val, by setting node to current.left.as_deref().
    4. Go right in the mirrored case, with an else if.
    5. Otherwise return current.val. The two values are split by this node, or one of them is this node, and a return is needed because it leaves the function from inside the loop.
    6. Add a tail value. After the loop, -1 without a semicolon gives the function a value on every path. It is never reached, because p and q are in the tree and the loop always returns first.

    The solution in Rust

    impl Solution {
        fn lowest_common_ancestor(root: Option<Box<TreeNode>>, p: i32, q: i32) -> i32 {
            let mut node = root.as_deref();
            while let Some(current) = node {
                if p < current.val && q < current.val {
                    node = current.left.as_deref();
                } else if p > current.val && q > current.val {
                    node = current.right.as_deref();
                } else {
                    return current.val;
                }
            }
            -1
        }
    }
    

    Rust details worth noticing:

    • Borrowing, not moving. root is taken by value, but as_deref() only borrows from it, so node and current are references into the tree. Nothing is cloned and nothing is dropped early.
    • Reassigning a copy of the cursor. Option<&TreeNode> is Copy because shared references are, so while let Some(current) = node doesn't consume node, and the assignments inside the loop are fine.
    • Why .as_deref() on the children. current.left has type Option<Box<TreeNode>>. Writing node = current.left fails with a mismatched-types error: expected Option<&TreeNode>, found Option<Box<TreeNode>>. as_deref() gives the type node already has.
    • The final expression. A line without a semicolon at the end of the function body is its return value. A return with a semicolon is only for leaving early.

    Complexity

    Time: O(h), where h is the height of the tree. Each loop pass goes one level down and does two comparisons. For a balanced tree that is about log n steps, and for a tree that is a single chain it is n.

    Space: O(1). The only extra memory is the node reference and the arguments. A recursive version would use O(h) stack space.

    Edge cases and common mistakes

    • One value is an ancestor of the other. For p = 2 and q = 4, the walk reaches 2 and finds 2 is not larger than itself, and 4 is not smaller. Neither move applies, and it returns 2.
    • Equal values. For p = 3 and q = 3 the answer is 3, the node itself: at every node on the way down both values are on the same side, until the walk reaches 3.
    • A single node. The root is the answer for every valid pair.
    • Using <= instead of <. If you test p <= current.val, you move away from a node that equals p and lose the case where the ancestor is one of the two values.
    • Treating it as a general binary tree. Searching both subtrees works but takes O(n) time. The ordering of a BST makes that unnecessary.
    • Mixing up the sides. Smaller values go left and larger values go right. Swapping them walks away from both targets.

    Testing

    On the tree [6, 2, 8, 0, 4, 7, 9, None, None, 3, 5]:

    • p = 2, q = 8 gives 6.
    • p = 2, q = 4 gives 2.
    • p = 5, q = 3 gives 4.
    • p = 3, q = 3 gives 3.
    • p = 6, q = 9 gives 6.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free