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 · What you must know before starting (the BST rule)
- Part A · Search with recursion (DFS)
- Part B · Search with a loop (iterative, O(1) space)
- Part C · Revision page
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.
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 NoneThe BST rule (what makes searching fast)
• 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
val = 2→ return the node 2, i.e. the subtree2 → (1, 3).val = 3→ return the node 3 (a leaf, so just 3).val = 8→ not in the tree → returnNone.
2What the constraints tell us
- Number of nodes: 1 to 5000. The tree is never empty at the start. But as we move down we can still reach a
Nonechild, so the code must still handle None. - Node values and
val: up to 10⁷. Why does the teacher read this? If we had to add or multiply values, numbers like 10⁷ added many times could go past 10⁹, the int limit in Java/C++, and we'd need long. Here we only compare values. Nothing grows, so 10⁷ is completely safe. (Python never overflows anyway.) - The values are unique in a BST, so there is at most one match.
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:
- equal → found it, stop.
- the value you want is smaller than the node → it can only be in the left subtree. Forget the right side.
- the value you want is bigger → it can only be in the right subtree. Forget the left side.
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.
if 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.
if root.val == val: return rootBoth rules return root, so the teacher joins them into one line:
if root is None or root.val == val:
return rootroot 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.
if 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.
→ 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.
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
- If the node is None or its value equals
val→ return the node. - If the node's value is bigger than
val→ return the search result from the left child. - Otherwise → return the search result from the right child.
6Code (Python)
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 onlyThe last line doesn't need an else: if the if above was true, we already returned.
7Code line by line
| line | what it means |
|---|---|
| if root is None or root.val == val: return root | Base 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):
- Call (4, 2): 4 exists, 4 ≠ 2. Is 4 > 2? Yes → go left. (4,2) waits on the stack.
- Call (2, 2): 2 exists, 2 == 2 → found, return node 2.
- (4, 2) gets node 2 and simply returns it. Final answer: the subtree 2 → (1, 3) ✓
Search for 3:
- (4, 3): 4 > 3 → go left.
- (2, 3): 2 ≠ 3. Is 2 > 3? No → go right.
- (3, 3): match → return node 3.
- (2, 3) passes 3 up → (4, 3) passes 3 up. Final answer: node 3 ✓
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.
- Time O(h): one node per level. For a balanced tree h ≈ log n, so O(log n).
- Space O(h): the recursion stack holds one call per level on the path, so also O(log n) for a balanced tree.
→ 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."
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
- When does the loop stop? Take
val = 8: 4 → 7 → 7's right child, which is None. We've fallen off the tree, so stop. So we keep going whilerootis not None. - Inside the loop, check the match first. If
root.val == val, returnrootright away. - Otherwise move one step. If
root.val > val(e.g. 4 > 3), the value is on the left →root = root.left. Otherwise (e.g. looking for 7 from 4) →root = root.right. - After the loop: we only get here if we fell off the tree without a match → return
None.
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.→ 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.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
- While
rootis not None: - if
root.val == val→ returnroot. - else if
root.val > val→root = root.left. - else →
root = root.right. - After the loop → return None (not found).
6Code (Python)
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 found7Code line by line
| line | what 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 root | Match → done. Return this node (with its subtree). |
| if root.val > val: root = root.left | The node is too big → the answer can only be on the left → step there. |
| else: root = root.right | The node is too small → step right. The else guarantees only one step per round. |
| return None | The loop ended without a match → the value is not in the tree. |
8Dry run (hand table)
Tree 4 → (2 → (1, 3), 7).
| search | round | root | check | action |
|---|---|---|---|---|
| 3 | 1 | 4 | 4 ≠ 3, 4 > 3 | root = 2 |
| 2 | 2 | 2 ≠ 3, 2 > 3? no | root = 3 | |
| 3 | 3 | 3 == 3 | return node 3 | |
| 8 | 1 | 4 | 4 ≠ 8, 4 > 8? no | root = 7 |
| 2 | 7 | 7 ≠ 8, 7 > 8? no | root = 7.right = None | |
| 3 | None | loop condition false | return None |
Only one variable was used the whole time, no stack of waiting calls.
9Complexity & remember
- Time O(h), the same as recursion: we still visit one node per level and ignore the other half each time. O(log n) when balanced, O(n) for a straight-line tree.
- Space O(1): just the one pointer. This is the space-optimised version. On LeetCode both versions run in about the same time. The win is only memory.
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
| Recursive | Iterative | |
|---|---|---|
| stop when | root is None or root.val == val → return root | loop ends when root is None; match → return inside |
| go left when | root.val > val | root.val > val |
| moving | return self.searchBST(child, val) | root = child (with if / else) |
| not found | the None base case returns None | return None after the loop |
| time | O(h): log n balanced, n worst | O(h) |
| space | O(h) call stack | O(1) |
| Search in a normal binary tree | Search in a BST | |
|---|---|---|
| where can the value be? | anywhere | only on one side, decided by comparing |
| visits | possibly every node, O(n) | one node per level, O(h) |
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.
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))
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