title: Lowest Common Ancestor of a Binary Search Tree
slug: lowest-common-ancestor-of-a-binary-search-tree
difficulty: medium
leetcode_id: 235
leetcode_url: https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/
categories:
  - trees
patterns:
  - tree-traversal
  - binary-search

description: |
  Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST.

  According to the definition of LCA on Wikipedia: "The lowest common ancestor is defined between two nodes `p` and `q` as the lowest node in `T` that has both `p` and `q` as descendants (where we allow **a node to be a descendant of itself**)."

constraints: |
  - The number of nodes in the tree is in the range `[2, 10^5]`
  - `-10^9 <= Node.val <= 10^9`
  - All `Node.val` are **unique**
  - `p != q`
  - `p` and `q` will exist in the BST

examples:
  - input: "root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8"
    output: "6"
    explanation: "The LCA of nodes 2 and 8 is 6."
  - input: "root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4"
    output: "2"
    explanation: "The LCA of nodes 2 and 4 is 2, since a node can be a descendant of itself according to the LCA definition."
  - input: "root = [2,1], p = 2, q = 1"
    output: "2"
    explanation: "The LCA of nodes 2 and 1 is 2."

explanation:
  intuition: |
    The key insight is to **leverage the BST property**: for any node, all values in its left subtree are smaller, and all values in its right subtree are larger.

    Think of it like searching for a meeting point. Imagine you're standing at the root, and two people are trying to find each other — one at node `p` and one at node `q`. As you traverse down the tree, at some point you'll reach a node where the two people would need to go in **different directions** to reach their respective nodes. That splitting point is the LCA.

    More concretely:
    - If both `p` and `q` are **smaller** than the current node, the LCA must be in the left subtree
    - If both `p` and `q` are **larger** than the current node, the LCA must be in the right subtree
    - If `p` and `q` are on **opposite sides** (or one equals the current node), then the current node is the LCA

    This is fundamentally different from finding the LCA in a general binary tree, where you'd need to search both subtrees. The BST ordering gives us a guaranteed direction at each step.

  approach: |
    We solve this using the **BST Property** to guide our traversal:

    **Step 1: Start at the root**

    - Begin traversal at the root node
    - We'll move down the tree based on how `p` and `q` compare to the current node

    &nbsp;

    **Step 2: Compare values and decide direction**

    - If both `p.val` and `q.val` are **less than** `current.val`, move to the left child
    - If both `p.val` and `q.val` are **greater than** `current.val`, move to the right child
    - Otherwise, we've found the split point — return the current node

    &nbsp;

    **Step 3: The split point is the LCA**

    - When `p` and `q` lie on different sides of the current node (or one of them equals the current node), the current node is the lowest common ancestor
    - Return this node as the answer

    &nbsp;

    This works because the BST property guarantees that once `p` and `q` "split" to different subtrees, they can never reunite at a lower node.

  common_pitfalls:
    - title: Ignoring the BST Property
      description: |
        A common mistake is treating this like a general binary tree LCA problem and recursing into both subtrees to find `p` and `q`.

        In a general binary tree, you'd need to search both children and check which subtree contains which node. But in a BST, you can determine which direction to go with a simple value comparison — O(1) per node instead of potentially visiting both subtrees.

        This makes the BST solution O(h) instead of O(n).
      wrong_approach: "Search both subtrees like in a general binary tree"
      correct_approach: "Use value comparisons to choose one direction at each step"

    - title: Forgetting a Node Can Be Its Own Ancestor
      description: |
        The problem states that a node can be a descendant of itself. If `p = 2` and `q = 4`, and node 2 is an ancestor of node 4 in the BST, then the LCA is 2, not some parent of 2.

        When checking the split condition, remember to handle the case where the current node equals `p` or `q`. In this case, the current node is the LCA because one node is an ancestor of the other.
      wrong_approach: "Only return when p and q are on opposite sides"
      correct_approach: "Return when p and q split OR when current equals p or q"

    - title: Incorrect Comparison Logic
      description: |
        Be careful with the comparison operators. The condition for moving left is when **both** values are less than current. Similarly for moving right.

        A common bug is using OR instead of AND:
        - Wrong: `if p.val < current.val or q.val < current.val` (might miss the split)
        - Correct: `if p.val < current.val and q.val < current.val`
      wrong_approach: "Using OR logic for direction decisions"
      correct_approach: "Using AND logic — both must be less/greater to continue"

  key_takeaways:
    - "**Exploit BST ordering**: The BST property lets you make O(1) direction decisions, avoiding the need to search both subtrees"
    - "**Split point = LCA**: The moment two values would need to go different directions, you've found their common ancestor"
    - "**Iterative vs recursive**: Both approaches work, but iterative uses O(1) space vs O(h) for the recursive call stack"
    - "**Foundation for harder problems**: This pattern extends to problems like finding paths between nodes or validating BST structure"

  time_complexity: "O(h) where h is the height of the tree. In a balanced BST, h = log(n). In the worst case (skewed tree), h = n."
  space_complexity: "O(1) for the iterative solution. We only use a single pointer to traverse the tree, regardless of input size."

solutions:
  - approach_name: Iterative Traversal
    is_optimal: true
    code: |
      class TreeNode:
          def __init__(self, val=0, left=None, right=None):
              self.val = val
              self.left = left
              self.right = right

      def lowest_common_ancestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
          # Start at the root and traverse down
          current = root

          while current:
              # Both nodes are in the left subtree
              if p.val < current.val and q.val < current.val:
                  current = current.left
              # Both nodes are in the right subtree
              elif p.val > current.val and q.val > current.val:
                  current = current.right
              # Split point found — p and q are on different sides
              # (or one of them equals current)
              else:
                  return current

          return None  # Should never reach here if p and q exist in tree
    explanation: |
      **Time Complexity:** O(h) — We traverse at most the height of the tree, making one comparison per level.

      **Space Complexity:** O(1) — Only a single pointer variable is used; no recursion stack.

      We exploit the BST property to navigate directly to the LCA. At each node, we compare both `p` and `q` values to decide whether to go left, right, or stop. The moment they would diverge (or one matches the current node), we've found the LCA.

  - approach_name: Recursive Traversal
    is_optimal: false
    code: |
      class TreeNode:
          def __init__(self, val=0, left=None, right=None):
              self.val = val
              self.left = left
              self.right = right

      def lowest_common_ancestor(root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
          # Both nodes are in the left subtree
          if p.val < root.val and q.val < root.val:
              return lowest_common_ancestor(root.left, p, q)

          # Both nodes are in the right subtree
          if p.val > root.val and q.val > root.val:
              return lowest_common_ancestor(root.right, p, q)

          # Split point — this is the LCA
          return root
    explanation: |
      **Time Complexity:** O(h) — Same traversal pattern as iterative, visiting at most h nodes.

      **Space Complexity:** O(h) — Recursive call stack can grow up to the height of the tree.

      This recursive version follows the same logic but uses the call stack instead of a loop. While elegant, it uses more space than the iterative approach. The recursive calls naturally unwind once we find the split point.
