DSA sheet · Trees · BST pattern

Insert into a Binary Search Tree

This problem joins the two before it. From Search in a BST we take the walk: compare, then go left or right, one path only. From Sorted Array to BST we take the building habit: assign what a call returns to root.left / root.right, and always return the node. The key insight: a new value always goes into the empty (None) spot where a search for it would end. The teacher shows a recursive solution and then an iterative one with O(1) extra space.

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. A leaf is a node with no children.

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

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

The rule is about the whole subtree, not just the children. A node deep down must respect every ancestor above it. Example: in the tree below, 3 is the right child of 2, so 3 > 2. But 3 also sits in 4's left subtree, so it must be < 4 as well. It is, so the tree is fine.

          4
        /   \
       2     7
      / \
     1   3

After we insert, the tree must still follow this rule everywhere. That's the whole challenge.

Inorder of a BST is sorted

Inorder = left subtree, node, right subtree. Since the left side is all smaller and the right side all bigger at every node, inorder lists the values in increasing order: the tree above gives 1 2 3 4 7. An easy way to check our insert worked: the new inorder should be the old sorted list with the new number slotted into its place.

Height

The height h is the number of levels (nodes on the longest root-to-leaf path). We use it for the complexity.

Part A · Insert with recursion (DFS)

LeetCode 701 · Insert into a Binary Search Tree

1The question in simple words

You get the root of a BST and a number val. Put a new node with value val into the tree so that it is still a BST, and return the root.

before
        4
      /   \
     2     7
    / \
   1   3
insert 5
        4
      /   \
     2     7
    / \   /
   1   3 5
insert 8 (instead)
        4
      /   \
     2     7
    / \     \
   1   3     8

LeetCode accepts any valid BST. The simple answer, and the one the teacher builds, adds the new value as a new leaf and doesn't move any old node.

2What the constraints tell us

3Intuition: how to think about it

Pretend you are searching for val. Since it's not in the tree, the search must fail: it keeps choosing left or right until it steps onto an empty spot (None). That empty spot is exactly where val belongs. Every comparison on the way down made sure the spot is on the correct side of every ancestor, so placing it there keeps the BST rule true everywhere.

So: walk down like search → when you hit None, put the new node there → make sure the parent gets linked to it.

4Building the conditions from examples

Example: insert 5. Where do we end up?

We landed on None. Is that an error? Try another value. Insert 8: 4 → right → 7 → right → None again. Since val is never in the tree, every insert ends at a None. So None is not a failure, it's the target.

Rule 1 (base case): reached None → create the node and return it

In Search, reaching None meant "return None, not found". Here it means the opposite: build the new node right here and return it. The teacher's tip: for recursion, always think of the base case first.

Rule 1if root is None: return TreeNode(val)
This one line also handles the empty-tree case from the constraints: the new node becomes the whole tree.

Rule 2: decide the side, and attach what comes back

But this time we must store the result: root.left = self.insertIntoBST(root.left, val). Why? When the call on 7's left reaches None, it creates node 5 and returns it. Somebody has to hook 5 under 7. The call for 7 does that by writing the result into root.left.

Rule 2if root.val < val: root.right = self.insertIntoBST(root.right, val)
else: root.left = self.insertIntoBST(root.left, val)
Doubt: in Search we just did return self.searchBST(...). Why assign here?
→ The teacher's rule: this problem is about constructing (changing the tree's shape), even if it's just one node. When you construct, whatever a call returns gets attached to root.left / root.right, not stored in a plain variable and not just passed up. Search only read the tree, so it passed the answer up.

Rule 3: return root (the extra line that saves the tree)

After 5 is attached under 7, the call for 7 ends. But remember how 7 was reached: the call for 4 did root.right = self.insertIntoBST(7, 5). Whatever the call for 7 returns will be written into 4.right.

  forgot "return root":     4
                          /   \
                         2    None   ← 7 and 5 are lost
                        / \
                       1   3
Rule 3return root at the end, so every parent re-attaches the same child it already had. Only the bottom-most call (the None spot) returns something new.
Doubt: doesn't re-assigning 4.right = 7 do extra work?
→ It writes the same link that was already there, a tiny O(1) step per level. In return, the code stays simple: the same line works for every level, including the one where the new node is attached.

5Approach steps

  1. If root is None → return a new node with val.
  2. If root.val < val → root.right = insert(root.right, val).
  3. Else → root.left = insert(root.left, val).
  4. Return root.

6Code (Python)

Insert into a BST, recursive
class Solution:
    def insertIntoBST(self, root, val):
        if root is None:                     # empty spot found -> new node lives here
            return TreeNode(val)
        if root.val < val:                   # val is bigger -> belongs on the right
            root.right = self.insertIntoBST(root.right, val)
        else:                                # val is smaller -> belongs on the left
            root.left = self.insertIntoBST(root.left, val)
        return root                          # give the parent its same child back

7Code line by line

linewhat it means
if root is None: return TreeNode(val)We stepped onto an empty spot. This is where val goes, so create it and hand it to the parent. It also covers an empty tree.
if root.val < val: root.right = self.insertIntoBST(root.right, val)The node is smaller than val, so val goes somewhere in the right subtree. Insert it there, then store whatever comes back as the right child.
else: root.left = self.insertIntoBST(root.left, val)The node is bigger (never equal, guaranteed), so insert into the left subtree and store the result as the left child.
return rootThis subtree's top is unchanged. Return it so the parent keeps the right link. Without this line the parent's link becomes None.

8Dry run using the call stack

Insert 5 into 4 → (2 → (1, 3), 7). ins(x) means the call with root = x.

  1. ins(4): not None. 4 < 5 → go right: it will run 4.right = ins(7). (ins(4) waits.)
  2. ins(7): not None. 7 < 5? No → else → it will run 7.left = ins(None). (ins(7) waits.)
  3. ins(None): empty spot → create node 5 and return it.
  4. Back in ins(7): 7.left = 5 → 5 is now connected. ins(7) returns 7.
  5. Back in ins(4): 4.right = 7 (the same as before, nothing lost). ins(4) returns 4, the root. Done ✓
step 3 (deepest)
ins(4) → rightins(7) → leftins(None) → new 5
step 4
ins(4) → rightins(7): 7.left = 5, return 7
step 5
ins(4): 4.right = 7, return 4

Insert 8 instead: ins(4) → right → ins(7): 7 < 8 → right → ins(None) creates 8 → 7.right = 8 → return 7 → 4.right = 7 → return 4 ✓

Check: the inorder after inserting 5 is 1 2 3 4 5 7, still sorted ✓

9Complexity & remember

Doubt: is it really always log n?
→ Only for a balanced BST. A BST built by inserting 1, 2, 3, 4… in order becomes a straight line, h = n, so the worst case is O(n) time and O(n) stack. Say "O(h): log n balanced, n worst case". Python detail: its default recursion limit is about 1000 calls, so on a very deep skewed tree (the constraints allow 10⁴ nodes) the recursive version can raise RecursionError. That's one more reason to know the loop version below.
Remember (recursive) None → return TreeNode(val) · smaller node → root.right = … · bigger node → root.left = … · always return root.

Part B · Insert with a loop (iterative)

1The question

Same question. The teacher's follow-up: the recursive code is already fast, but what if an interviewer wants it without the O(h) stack space, in constant space? Then we drop recursion and walk down with a pointer, as in the iterative Search.

2Constraints

Same as Part A. The 0-node case now needs its own check before the loop, because there's no base case that handles it automatically.

3Intuition

Walk down the tree with one finger, choosing left or right at each node. But don't step onto the None. Stop one step before it, while you're still on the parent, because that's the only way to attach the new node: parent.left = new or parent.right = new.

4Building the conditions

Empty tree first

If root is None, there's nothing to walk. Return TreeNode(val) straight away.

Why a separate pointer cur?

At the end we must return the top of the tree (4). If we moved root itself down the tree, we'd lose track of the top. So we keep root fixed and walk with a copy: cur = root.

Doubt: in iterative Search we moved root directly. Why not here?
→ Search returned the found node, so it never needed the top again. Insert must return the root of the whole tree, so the top has to be kept safe.

Look before you step

Doubt: what goes wrong if I just step to cur.right and then check for None?
→ Once cur becomes None, you're holding nothing. You no longer know which node was the parent or which side was empty, so you can't attach the new node. Checking the child first keeps us on the parent.
Doubt: why a while True loop? Is there a risk it never ends?
→ Each round either steps one level down or attaches and breaks. The tree has a bottom, and val isn't in the tree, so we must reach an empty child within h rounds. The loop always ends with a break. We don't need to check for None at the top of the loop either: the root was checked before the loop, and we only ever step onto real nodes.

After the loop

Return root, the untouched top. We've only added one leaf somewhere below.

5Approach steps

  1. If root is None → return TreeNode(val).
  2. cur = root.
  3. Loop: if val > cur.val → if cur.right is None, attach there and stop. Otherwise move right.
  4. Else → if cur.left is None, attach there and stop. Otherwise move left.
  5. Return root.

6Code (Python)

Insert into a BST, iterative
class Solution:
    def insertIntoBST(self, root, val):
        if root is None:                      # empty tree -> new node is the tree
            return TreeNode(val)
        cur = root                            # walk with a copy, keep root safe
        while True:
            if val > cur.val:                 # belongs on the right
                if cur.right is None:         # empty spot -> attach and stop
                    cur.right = TreeNode(val)
                    break
                cur = cur.right
            else:                             # belongs on the left
                if cur.left is None:
                    cur.left = TreeNode(val)
                    break
                cur = cur.left
        return root

7Code line by line

linewhat it means
if root is None: return TreeNode(val)No tree yet (allowed by the constraints) → the new node is the whole answer.
cur = rootA walking pointer. root stays on the top so we can return it.
while True:Keep walking until we attach. Always ends with a break.
if val > cur.val:val is bigger than this node → it belongs in the right subtree.
if cur.right is None: cur.right = TreeNode(val) breakThe right spot is empty → this is the place. Create, connect, stop.
cur = cur.rightThe right spot is taken → step down and compare again.
else: … cur.left …The mirror case for a smaller val.
return rootThe top of the tree, now with one extra leaf.

8Dry run (hand table)

Insert 5 into 4 → (2 → (1, 3), 7).

roundcurcheckchild on that sideaction
start—root is 4, not None—cur = 4
145 > 4 → right4.right = 7 (taken)cur = 7
275 > 7? no → left7.left = None (empty)7.left = new 5, break
end———return root (4)

Insert 8: round 1: 8 > 4 → 4.right is 7 → cur = 7. Round 2: 8 > 7 → 7.right is None → attach 8 there, break → return 4 ✓

Empty tree, insert 5: the first if returns a single node 5 ✓

9Complexity & remember

Remember (iterative) empty → new node · cur = root · look at the child before stepping · None child → attach + break · return root, not cur.

Part C · Revision page

RecursiveIterative
empty treehandled by the base caseseparate check before the loop
reaching the spotstep onto None, create there, return itstop on the parent, check the child is None, attach
connectingroot.left/right = call(...) + return rootcur.left/right = TreeNode(val)
returnsroot of each subtree (unchanged)the original root
timeO(h)O(h)
spaceO(h) stackO(1)
Search in BSTInsert into BST
reaching None meansnot found → return Nonefound the spot → create the node
child call resultpassed straight up (return call)attached (root.x = call), then return root
iterative pointercan move root itselfneeds cur, root must be returned
If you remember only 5 lines 1. A new value always becomes a new leaf, in the None spot where its search ends.
2. Node smaller than val → go right. Bigger → go left (never equal, guaranteed).
3. Recursive: None → TreeNode(val), attach with root.left/right = …, return root.
4. Iterative: check the child before stepping, attach and break, return the original root.
5. Time O(h) (log n if balanced). Space O(h) recursive, O(1) iterative.
Mistakes to avoid ✗ forgetting return root (the parent's link becomes None and a subtree vanishes)
✗ calling the recursion without assigning to root.left/root.right (the new node is never connected)
✗ not handling the empty tree (0 nodes is allowed)
✗ moving root instead of cur in the loop (you return the wrong node)
✗ stepping onto None before attaching (the parent is lost)
✗ mixing up the direction (val bigger → RIGHT)
test it yourself (paste under either solution above)
def inorder(n):
    return inorder(n.left) + [n.val] + inorder(n.right) if n else []

s = Solution()
root = TreeNode(4, TreeNode(2, TreeNode(1), TreeNode(3)), TreeNode(7))
root = s.insertIntoBST(root, 5)
print(root.right.left.val)           # 5  (left child of 7)
root = s.insertIntoBST(root, 8)
print(root.right.right.val)          # 8  (right child of 7)
print(inorder(root))                 # [1, 2, 3, 4, 5, 7, 8]
print(s.insertIntoBST(None, 5).val)  # 5  (empty tree)

Based on this video: Insert into a Binary Search Tree