DSA sheet · Trees · BST pattern

Search in a Binary Search Tree

In the previous problem we built a BST. Now the tree is ready, and we have to find a value in it. This is the most basic BST operation, and almost every later BST problem (insert, delete, LCA, floor/ceil) walks the tree the same way. The key lesson: at each node, the BST rule tells you which ONE side to go to, so you never search the whole tree. The teacher solves it twice: first with recursion, then iteratively (a loop) to bring the extra space down to O(1).

Every problem 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 has a value, a left child link and a right child link. 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       # the number in this node
        self.left = left     # left child, or None
        self.right = right   # right child, or None

The BST rule (what makes searching fast)

The BST ruleFor every node:
• all values in its left subtree (the left child and everything under it) are smaller,
• all values in its right subtree are bigger.

It's not enough for each child to be on the correct side of its parent. A value must also fit the limits set by every ancestor above it (parent, grandparent, up to the root). For example, in a tree with root 5, anything inside 5's left subtree must be smaller than 5, even if it's the right child of some smaller node like 2.

Because of this rule, if you're standing at a node and the value you want is smaller than it, the value can only be on the left side. The whole right side can be ignored. That's the trick behind this problem.

Bonus fact: the inorder of a BST is sorted

Inorder visits left subtree → node → right subtree. In a BST the left side is all smaller and the right side is all bigger, at every node, so the visit order is increasing. You can think of a BST as a sorted list folded into a tree, and searching it is just binary search on that list.

Height of a tree

The height is the number of levels, i.e. the number of nodes on the longest root-to-leaf path. We'll use it for the complexity. Call it h.

Part A · Search with recursion (DFS)

LeetCode 700 · Search in a Binary Search Tree

1The question in simple words

You get the root of a BST and a number val. Find the node whose value equals val and return that node. Returning the node means returning the whole subtree that hangs under it. If no node has that value, return None.

The tree used in the video:

          4
        /   \
       2     7
      / \
     1   3

2What the constraints tell us

3Intuition: how to think about it

Think of the number-guessing game: "I'm thinking of a number." You guess 50. "Smaller." You never check 51–100 again. Each answer throws away a whole range.

A BST works the same way. At each node you compare:

So we walk down one single path from the root, never both sides.

4Building the conditions from examples

Search for 2: what do we check first?

We're at the root 4. The natural first thought: "does 4 match 2?" But the teacher stops here: before reading the value, make sure the node exists. If the node is None, reading .val crashes.

Rule 1: node missingif root is None: return root (which is None)
Nothing here means the value isn't on this path. We aren't waiting for anything else, so just return None.

Rule 2: the node matches

If the node exists and its value equals val, we found the answer. No need to look at its children: return this node right away.

Rule 2: foundif root.val == val: return root

Both rules return root, so the teacher joins them into one line:

rules 1 and 2 together
if root is None or root.val == val:
    return root
Doubt: if root is None, won't root.val crash in that same line?
→ No. Python checks or from left to right and stops as soon as it finds True. If root is None is True, it returns None and never reads root.val. The right side is checked only when the left side is False, which means the node exists, so reading .val is safe. That's why the None check must come first.

Rule 3: no match → which side?

At root 4, 4 ≠ 2. Should we search both children? No. 2 is smaller than 4, and by the BST rule everything smaller than 4 is on its left. So 2 can only be on the left, if it exists at all.

Rule 3: go one wayif root.val > val: go to root.left (the value is smaller)
else: go to root.right (the value is bigger)

For val = 3, at node 2: is 2 > 3? No → so 3 isn't on the left of 2, it must be on the right. Go to root.right.

Doubt: in a normal binary tree we'd search left and right. Why only one here?
→ In a normal tree, the value could be anywhere, so we must check everywhere. In a BST, the rule guarantees where it can be. Looking at the other side would only waste time, because the value cannot be there.

What do we do with the answer from the child call? Just return it

When the call on 2 finds the node, it returns it to the call on 4. What should 4 do with it? Nothing except pass it up: "my child found it, so here is the answer". No combining, no attaching. So we write return self.searchBST(...) directly, with no variable in between.

Doubt: in Sorted-Array-to-BST we wrote root.left = self.build(...). Why not root.left = self.searchBST(...) here?
→ Because here we only read the tree. We are not building or changing it. Assigning to root.left would actually damage the tree (it would cut off nodes). For searching, the answer simply travels up unchanged.

5Approach steps

  1. If the node is None or its value equals val → return the node.
  2. If the node's value is bigger than val → return the search result from the left child.
  3. Otherwise → return the search result from the right child.

6Code (Python)

Search in a BST, recursive
class Solution:
    def searchBST(self, root, val):
        if root is None or root.val == val:      # not found here / found it
            return root
        if root.val > val:                       # val is smaller -> left side only
            return self.searchBST(root.left, val)
        return self.searchBST(root.right, val)   # val is bigger -> right side only

The last line doesn't need an else: if the if above was true, we already returned.

7Code line by line

linewhat it means
if root is None or root.val == val: return rootBase case, two situations at once. Fell off the tree (None) → the value doesn't exist, return None. Value matches → return this node (and its subtree). The None check comes first so .val is safe.
if root.val > val: return self.searchBST(root.left, val)The node is bigger than what we want, so the answer can only be on the left. Search there and pass its answer straight up.
return self.searchBST(root.right, val)The node is smaller than what we want, so search only the right side.

8Dry run using the call stack

Search for 2 in the tree 4 → (2 → (1, 3), 7):

  1. Call (4, 2): 4 exists, 4 ≠ 2. Is 4 > 2? Yes → go left. (4,2) waits on the stack.
  2. Call (2, 2): 2 exists, 2 == 2 → found, return node 2.
  3. (4, 2) gets node 2 and simply returns it. Final answer: the subtree 2 → (1, 3) ✓

Search for 3:

  1. (4, 3): 4 > 3 → go left.
  2. (2, 3): 2 ≠ 3. Is 2 > 3? No → go right.
  3. (3, 3): match → return node 3.
  4. (2, 3) passes 3 up → (4, 3) passes 3 up. Final answer: node 3 ✓
search 2, deepest moment
(4, 2)(2, 2) → found
search 3, deepest moment
(4, 3)(2, 3)(3, 3) → found
search 8, deepest moment
(4, 8)(7, 8)(None, 8) → None

Search for 8 (not in the tree): 4 < 8 → right → 7 < 8 → right → 7 has no right child → None → None travels back up. Answer: None ✓

Notice: when searching for 2 we never looked at 7, 1 or 3. When searching for 3 we never looked at 7 or 1.

9Complexity & remember

The teacher's reasoning for time: we do not visit every node. In a full, balanced tree, the first step throws away half the nodes, the next throws away half of what's left, then half again: ½, ¼, ⅛ … That halving takes about log₂ n steps. Put another way, we touch one node per level, so the work equals the number of levels, the height.

Doubt: is it always log n?
→ Only when the tree is balanced, which is what the teacher's full-tree picture assumes. A BST can be lopsided. If the values were inserted in sorted order (1, 2, 3, 4, 5), every node only has a right child and the "tree" is a straight line:
  1
   \
    2
     \
      3
       \
        4
         \
          5      search 5 → visits all 5 nodes

Here h = n, so the worst case is O(n) time and O(n) stack. The safe way to say it in an interview: "O(h), which is O(log n) for a balanced BST and O(n) in the worst case."

Remember (recursive) None or match → return root · bigger node → left · smaller node → right · return the call directly. One path, never both sides.

Part B · Search with a loop (iterative)

1The question

Same question as Part A. The follow-up the teacher asks: can the space be O(1)? The O(h) space in Part A comes only from the recursion stack. If we don't make recursive calls, that space goes away.

2Constraints

Same as Part A: at least 1 node, values only compared, so no overflow worries.

3Intuition

In Part A each call did a comparison and then handed over to one child, and the parent did nothing afterwards except pass the answer up. Nothing needs to be remembered on the way back. So instead of calling a function for the child, we can just move a pointer to the child and repeat, like walking down the tree with one finger.

4Building the conditions

Doubt: why does the "go right" line need an else here, when the recursive version didn't?
→ In the recursive code, each branch returned, so the code below a branch never ran after it. Here the branches only move the pointer and don't return. Without else, after moving left we'd immediately also run root = root.right on the new node and jump twice (or crash on None). if / else makes sure exactly one move happens per round.
Doubt: in the video it sounds like "move your left to root.left". What moves?
→ That's a slip of the tongue. The thing that moves is the pointer root itself: root = root.left or root = root.right. The final code in the video does exactly that.
Doubt: is it OK to overwrite root? Don't we lose the tree?
→ Here it's fine. root is just our local variable. Changing it doesn't change the tree, and we never need the top again because we return the found node, not the root. (In Insert into a BST we do need the top at the end, so there we'll walk with a separate pointer cur.)

5Approach steps

  1. While root is not None:
  2. if root.val == val → return root.
  3. else if root.val > val → root = root.left.
  4. else → root = root.right.
  5. After the loop → return None (not found).

6Code (Python)

Search in a BST, iterative
class Solution:
    def searchBST(self, root, val):
        while root is not None:
            if root.val == val:          # found it
                return root
            if root.val > val:           # val is smaller -> step left
                root = root.left
            else:                        # val is bigger -> step right
                root = root.right
        return None                      # fell off the tree: not found

7Code line by line

linewhat it means
while root is not None:Keep walking while we're standing on a real node. Becoming None means we left the tree.
if root.val == val: return rootMatch → done. Return this node (with its subtree).
if root.val > val: root = root.leftThe node is too big → the answer can only be on the left → step there.
else: root = root.rightThe node is too small → step right. The else guarantees only one step per round.
return NoneThe loop ended without a match → the value is not in the tree.

8Dry run (hand table)

Tree 4 → (2 → (1, 3), 7).

searchroundrootcheckaction
3144 ≠ 3, 4 > 3root = 2
222 ≠ 3, 2 > 3? noroot = 3
333 == 3return node 3
8144 ≠ 8, 4 > 8? noroot = 7
277 ≠ 8, 7 > 8? noroot = 7.right = None
3Noneloop condition falsereturn None

Only one variable was used the whole time, no stack of waiting calls.

9Complexity & remember

Remember (iterative) while root: match → return · too big → root = root.left · else → root = root.right · after loop → None. Same time, O(1) space.

Part C · Revision page

RecursiveIterative
stop whenroot is None or root.val == val → return rootloop ends when root is None; match → return inside
go left whenroot.val > valroot.val > val
movingreturn self.searchBST(child, val)root = child (with if / else)
not foundthe None base case returns Nonereturn None after the loop
timeO(h): log n balanced, n worstO(h)
spaceO(h) call stackO(1)
Search in a normal binary treeSearch in a BST
where can the value be?anywhereonly on one side, decided by comparing
visitspossibly every node, O(n)one node per level, O(h)
If you remember only 5 lines 1. BST: left subtree all smaller, right subtree all bigger, at every node.
2. Check None first, then match, then decide the side.
3. Node bigger than val → go left. Node smaller → go right. Never both.
4. Recursive: return the child call directly. Iterative: move the pointer with if / else.
5. Time O(h) (log n if balanced). Space O(h) recursive, O(1) iterative.
Mistakes to avoid ✗ reading root.val before checking root is None (crash)
✗ searching both sides (correct, but throws away the BST speed-up)
✗ mixing up the direction (node bigger → LEFT, not right)
✗ forgetting else in the loop (two moves in one round)
✗ assigning root.left = self.searchBST(...) (changes the tree while only reading)
✗ saying "always O(log n)" (a skewed BST is O(n))
test it yourself (paste under either solution above)
root = TreeNode(4, TreeNode(2, TreeNode(1), TreeNode(3)), TreeNode(7))
s = Solution()
n = s.searchBST(root, 2)
print(n.val, n.left.val, n.right.val)   # 2 1 3
print(s.searchBST(root, 3).val)         # 3
print(s.searchBST(root, 8))             # None
print(s.searchBST(TreeNode(5), 5).val)  # 5

Based on this video: Search in a Binary Search Tree