DSA sheet · Binary Search Tree · LCA pattern

LCA of a BST

This video takes the "lowest common ancestor" problem we already solved for a normal binary tree and asks it again for a binary search tree. The big lesson: the BST ordering tells us which side to walk into, so we never have to search both sides. That turns a "visit everything" solution into a "walk one path down" solution. The teacher first writes it with recursion, then shows how an interviewer's "now make the space constant" request leads to a tiny while loop version.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the conditions from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

What is a tree node?

Each node is a small box with 3 things: its value, a link to its left child and a link to its right child. A missing child is None.

given by LeetCode, don't write this in the solution
class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

What is a Binary Search Tree (BST)?

A BST is a binary tree with one extra promise, true at every node:

Short form: left subtree < node < right subtree. Note the word subtree: it's not enough that the direct children obey it. The grandchildren, great-grandchildren and so on must obey it too.

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

This is the tree used all through the video (LeetCode 235's example). Check node 6: everything on its left (2, 0, 4, 3, 5) is smaller than 6, and everything on its right (8, 7, 9) is bigger. Check node 2: left side 0 is smaller, right side 4, 3, 5 are all bigger. It holds everywhere.

Why this helps: a BST is like a signpost at every node

Standing at a node, if you want a value smaller than the node, it can only be on the left. If you want a bigger value, it can only be on the right. So you never need to look at both sides. This is the same idea as binary search on a sorted array: at each step you throw away half.

Doubt: why is a BST called "sorted"?
→ If you visit it inorder (left subtree, then the node, then the right subtree), you get the values in increasing order. For the tree above: 0 2 3 4 5 6 7 8 9. The reason: at every node, everything printed before it (its left side) is smaller and everything printed after it (its right side) is bigger. This problem doesn't need the inorder list, but it's the same property at work.

Ancestor, common ancestor, lowest common ancestor

In the tree above: LCA(2, 8) = 6. LCA(0, 4) = 2. LCA(7, 9) = 8. And LCA(2, 4) = 2, because 2 is an ancestor of 4 and also of itself.

What changes from the normal binary-tree LCA (previous video)

In a normal binary tree we had no idea whether p and q were on the left or the right, so we had to search both sides of every node and combine the answers. That costs O(n). Here the BST rule tells us the side straight away, so we follow a single path downward.

Part A · LCA of a BST with recursion

LeetCode 235 · Lowest Common Ancestor of a Binary Search Tree

1The question in simple words

You get the root of a BST and two nodes p and q that are in the tree. Return the node that is their lowest common ancestor.

p = 2, q = 8 → answer 6
         6
       /   \
      2     8
     / \   / \
    0   4 7   9
p = 0, q = 4 → answer 2
         6
       /   \
      2     8
     / \   / \
    0   4 7   9
p = 7, q = 9 → answer 8
         6
       /   \
      2     8
     / \   / \
    0   4 7   9

2What the constraints tell us

3Intuition: the place where p and q split up

Picture p and q as two travellers starting at the root. At each node, each traveller follows the signpost: smaller goes left, bigger goes right.

4Building the conditions from examples

The teacher discovers each case with one pair of nodes.

Example p = 0, q = 4 → both smaller → go left

We stand at 6. The bigger of p and q is 4, and 6 is bigger than even that. So 6 is bigger than both: 0 and 4 must both be in 6's left subtree. We can ignore the right side completely and ask the same question at 2.

Case 1root is bigger than both p and q → the answer is in the left subtree.
if root.val > p.val and root.val > q.val: go left
The same thing in one check: root.val > max(p.val, q.val).

Example p = 7, q = 9 → both bigger → go right

At 6, the smaller of p and q is 7, and 6 is smaller than even that. So 6 is smaller than both: 7 and 9 are both in the right subtree. Go right, to 8.

Case 2root is smaller than both p and q → the answer is in the right subtree.
if root.val < p.val and root.val < q.val: go right
One-check form: root.val < min(p.val, q.val).

Back to p = 0, q = 4, now standing at 2 → neither case → this is the answer

Is 2 bigger than both? 2 is bigger than 0, but not bigger than 4. So not case 1. Is 2 smaller than both? It's smaller than 4, but not smaller than 0. So not case 2. That means 0 is on 2's left and 4 is on 2's right. The travellers split here, so 2 is the LCA. We return it without going any lower.

The same happens with p = 2, q = 8 right at the root: 6 is not bigger than 8 and not smaller than 2, so 6 is the answer immediately.

Case 3neither case 1 nor case 2 → p and q are on different sides (or one of them is this node) → return root.
Doubt 1: the teacher's whiteboard version wrote the conditions with >= and <=. Is that OK?
→ No, that has a bug, and I've fixed it to strict > and < in the code. Try p = 6, q = 2 (6 is the root). With >=: is 6 ≥ 6 and 6 ≥ 2? Yes → go left to 2. But the true answer is 6 itself, since 6 is an ancestor of 2 and of itself. Once we've left 6 we can never come back, so the answer comes out wrong. With strict >: 6 > 6 is false, so case 1 fails, case 2 fails, and we return 6 ✓. In her final editor code (Part B) she does use strict signs, which is correct.
Doubt 2: what if p or q is the current node itself? E.g. p = 2, q = 4.
→ At 6: bigger than both → go left. At 2: is 2 > 2? No, so case 1 fails. Is 2 < 2? No, so case 2 fails. Case 3 → return 2 ✓. The strict signs automatically treat "I am p or q" as the split point, which is exactly right.
Doubt 3: why do I need "and" in case 1? Wouldn't root.val > p.val alone be enough?
→ No. Root 6 with p = 2, q = 8: 6 > 2 is true, but 8 is on the right. Going left would lose q. We may only go left when both are on the left.

5Approach steps

  1. Look at the current node root.
  2. If its value is bigger than both p and q → return the answer from the left subtree (same function, called on root.left).
  3. Else if its value is smaller than both → return the answer from the right subtree.
  4. Otherwise → p and q split here (or one of them is here) → return root.

6Code (Python)

LCA of a BST with recursion
class Solution:
    def lowestCommonAncestor(self, root, p, q):
        if root is None:                                  # safety only
            return None
        if root.val > p.val and root.val > q.val:         # both on the left
            return self.lowestCommonAncestor(root.left, p, q)
        if root.val < p.val and root.val < q.val:         # both on the right
            return self.lowestCommonAncestor(root.right, p, q)
        return root                                       # they split here

7Code line by line

linewhat it means
if root is None: return NoneCan't really happen, since p and q are always in the tree and we always step toward them. It's just a guard.
if root.val > p.val and root.val > q.val:The node is bigger than both, so both live on the left. Strict > so that "root is p" is not treated as "go left".
return self.lowestCommonAncestor(root.left, p, q)Ask the same question one level down on the left, with the same p and q, and pass its answer straight back up.
if root.val < p.val and root.val < q.val:The node is smaller than both, so both live on the right.
return self.lowestCommonAncestor(root.right, p, q)Go right and pass the answer back.
return rootOne is on the left and one on the right, or the node is p or q. Either way this is the LCA.

8Dry run using the call stack

Run 1: p = 2, q = 8.

  1. Call f(6): is 6 bigger than both 2 and 8? No. Smaller than both? No. → return 6. Done in a single call.

Run 2: p = 7, q = 9.

  1. Call f(6): 6 > 7 and 6 > 9? No. 6 < 7 and 6 < 9? Yes → call f(6.right) = f(8). f(6) waits on the stack.
  2. Call f(8): 8 > 7 but not > 9 → not case 1. 8 < 9 but not < 7 → not case 2. → return 8.
  3. f(6) receives 8 and returns it unchanged. The stack is empty. Final answer: 8 ✓
run 2, step 2
f(6)f(8) → 8
run 2, step 3
f(6) → 8

Run 3: p = 0, q = 4. f(6): bigger than both → f(2). f(2): bigger than 0 but not 4, smaller than 4 but not 0 → return 2. f(6) passes 2 up. Answer 2 ✓.

Notice: at each level we called the function on only one child. In the normal binary-tree LCA we called both.

9Complexity & remember

RememberBigger than both → go left. Smaller than both → go right. Anything else → this node is the LCA. Use strict > / <.

Part B · LCA of a BST with a while loop (constant space)

Same question, same constraints, same three cases as Part A. Only the way we move changes.

1Why make an iterative version?

The teacher's interview tip: the interviewer may say "the time is fine, but bring the space down to constant". Whenever you hear constant space, drop the recursion, because recursion always uses the call stack. Instead, keep one variable pointing at the current node and move it down inside a while loop.

2Why is this so easy here?

Look at Part A: every recursive call is the last thing the function does (return self.f(child)). The parent never does any work after the child answers; it just passes the answer up. So we don't need to remember the parents at all. We can simply replace root with the child and loop again.

3The conditions (unchanged)

Doubt: the teacher said she skipped the equals sign "because all values are unique". Is that the real reason?
→ Uniqueness helps (we never have to decide where a duplicate goes), but the main reason for strict signs is the one in Part A, Doubt 1: when the current node equals p or q, we must stop and return it, not walk past it. With <=, p = 2, q = 4 would go from 2 to the right and return 4, which is wrong.

4Approach steps

  1. While root is not None:
  2. If root.val < p.val and root.val < q.val → move root to its right child.
  3. Else if root.val > p.val and root.val > q.val → move root to its left child.
  4. Else → return root.
  5. After the loop → return None.

5Code (Python)

LCA of a BST with a while loop
class Solution:
    def lowestCommonAncestor(self, root, p, q):
        while root is not None:
            if root.val < p.val and root.val < q.val:     # both on the right
                root = root.right
            elif root.val > p.val and root.val > q.val:   # both on the left
                root = root.left
            else:                                         # they split here
                return root
        return None

6Code line by line

linewhat it means
while root is not None:Keep walking down until we find the split point (or fall off the tree, which can't happen with valid input).
if root.val < p.val and root.val < q.val: root = root.rightBoth are bigger than the node → step right. No function call, no stack.
elif root.val > p.val and root.val > q.val: root = root.leftBoth are smaller → step left.
else: return rootThe paths split here, or this node is p or q → it's the LCA.
return NoneOnly for safety, because the method promises to return a node.

7Dry run: a hand table

Tree from Part 0, p = 3, q = 5 (both deep in the tree):

steprootsmaller than both 3, 5?bigger than both?action
16noyesroot = 2
22yesnoroot = 4
34no (4 > 3)no (4 < 5)return 4

3 is on 4's left and 5 is on 4's right, so 4 is where they split ✓. We touched only 3 nodes, one per level, out of 9.

8Complexity

Remember"Constant space" in an interview → turn the recursion into a while root: loop that moves root left or right. It works here because the recursive call was the last step.

Part C · Revision page

LCA in a normal binary treeLCA in a BST (recursion)LCA in a BST (loop)
which side to search?unknown → search boththe BST rule picks one sidethe BST rule picks one side
nodes visitedall none per levelone per level
timeO(n)O(h): log n balanced, n skewedO(h)
spaceO(h) stackO(h) stackO(1)
situation at the current nodemeaningaction
node > p and node > qboth on the leftgo left
node < p and node < qboth on the rightgo right
anything else (p < node < q, or node is p or q)the paths split herereturn node
If you remember only 5 lines 1. BST: everything on the left is smaller, everything on the right is bigger, at every node.
2. The LCA is the first node where p and q would go different ways.
3. Bigger than both → left. Smaller than both → right. Else → return the node.
4. Time O(h): log n balanced, n skewed. Recursion costs O(h) space.
5. A while loop moving root gives O(1) space.
Mistakes to avoid ✗ using >= / <= (walks past the node when it is p or q)
✗ checking only one of p, q before choosing a side
✗ searching both subtrees like the normal binary-tree LCA (works, but wastes the BST property)
✗ saying "always O(log n)": a skewed BST makes it O(n)
test it yourself (paste under either solution above)
n3, n5 = TreeNode(3), TreeNode(5)
n4 = TreeNode(4, n3, n5)
n0 = TreeNode(0)
n2 = TreeNode(2, n0, n4)
n7, n9 = TreeNode(7), TreeNode(9)
n8 = TreeNode(8, n7, n9)
root = TreeNode(6, n2, n8)

s = Solution()
print(s.lowestCommonAncestor(root, n2, n8).val)   # 6
print(s.lowestCommonAncestor(root, n0, n4).val)   # 2
print(s.lowestCommonAncestor(root, n7, n9).val)   # 8
print(s.lowestCommonAncestor(root, n2, n4).val)   # 2  (a node is its own ancestor)
print(s.lowestCommonAncestor(root, root, n2).val) # 6
print(s.lowestCommonAncestor(root, n3, n5).val)   # 4

Based on this video: LCA of a Binary Search Tree