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 · What you must know before starting (the BST rule)
- Part A · Insert with recursion (DFS)
- Part B · Insert 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. A leaf is a node with no children.
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
• 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.
4
/ \
2 7
/ \
1 3 4
/ \
2 7
/ \ /
1 3 5 4
/ \
2 7
/ \ \
1 3 8LeetCode 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
- Number of nodes: 0 to 10⁴ → 0 is allowed, so the tree can be empty (
rootis None). Then the answer is a tree with one node:val. E.g. empty tree + insert 5 → just5. We need this case in the code. - n up to 10⁴ → an O(n²) idea would be about 10⁸ steps. Around 10⁸ is the edge, and beyond it you risk TLE (Time Limit Exceeded). Our solution will be much faster anyway.
- Values from −10⁸ to 10⁸ → close to the int limit (about 10⁹) in Java/C++, but still safe, because we only compare values. Overflow is only a worry when you add or multiply big numbers.
- "It is guaranteed that val does not exist in the original BST." → at every node,
valis either bigger or smaller, never equal. So we always have a direction to go, and we never have to decide what to do with duplicates.
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?
- At 4: is 4 bigger than 5? No → 5 must go to the right of 4. Move to 7.
- At 7: is 7 bigger than 5? Yes → 5 must go to the left of 7. Move to 7's left… which is None.
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.
if 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
root.val < val(node smaller than val, like 4 < 5) → val belongs on the right → recurse onroot.right.- otherwise (node bigger, like 7 > 5) → val belongs on the left → recurse on
root.left.
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.
if root.val < val: root.right = self.insertIntoBST(root.right, val)else: root.left = self.insertIntoBST(root.left, val)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.
- Should 4.right change? No. 7 was already there and is still the right child of 4. So the call for 7 must return 7 itself, which is
root. - If we forget
return root, Python returnsNoneby default. Then 4.right becomes None, and the whole 7-subtree (with the new 5) disappears.
forgot "return root": 4
/ \
2 None ← 7 and 5 are lost
/ \
1 3
return 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.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
- If
rootis None → return a new node withval. - If
root.val < val→root.right = insert(root.right, val). - Else →
root.left = insert(root.left, val). - Return
root.
6Code (Python)
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 back7Code line by line
| line | what 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 root | This 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.
- ins(4): not None. 4 < 5 → go right: it will run
4.right = ins(7). (ins(4) waits.) - ins(7): not None. 7 < 5? No → else → it will run
7.left = ins(None). (ins(7) waits.) - ins(None): empty spot → create node 5 and return it.
- Back in ins(7):
7.left = 5→ 5 is now connected. ins(7) returns 7. - Back in ins(4):
4.right = 7(the same as before, nothing lost). ins(4) returns 4, the root. Done ✓
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
- Time O(h): at every level we look at just one node and pick a side, exactly like search. The teacher says O(log n), which is true for a balanced tree (log n levels).
- Space O(h): one waiting call per level on the stack, so O(log n) when balanced.
→ 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.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.
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
- If
val > cur.val, we want to go right. Before moving, checkcur.right:- it's None → this is the empty spot. Do
cur.right = TreeNode(val)and break out of the loop (the job is done). - it's a node → step there:
cur = cur.right.
- it's None → this is the empty spot. Do
- Else (val is smaller) → the same thing on the left: if
cur.leftis None, attach there and break, otherwisecur = cur.left.
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.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
- If
rootis None → returnTreeNode(val). cur = root.- Loop: if
val > cur.val→ ifcur.rightis None, attach there and stop. Otherwise move right. - Else → if
cur.leftis None, attach there and stop. Otherwise move left. - Return
root.
6Code (Python)
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 root7Code line by line
| line | what it means |
|---|---|
| if root is None: return TreeNode(val) | No tree yet (allowed by the constraints) → the new node is the whole answer. |
| cur = root | A 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) break | The right spot is empty → this is the place. Create, connect, stop. |
| cur = cur.right | The right spot is taken → step down and compare again. |
| else: … cur.left … | The mirror case for a smaller val. |
| return root | The top of the tree, now with one extra leaf. |
8Dry run (hand table)
Insert 5 into 4 → (2 → (1, 3), 7).
| round | cur | check | child on that side | action |
|---|---|---|---|---|
| start | — | root is 4, not None | — | cur = 4 |
| 1 | 4 | 5 > 4 → right | 4.right = 7 (taken) | cur = 7 |
| 2 | 7 | 5 > 7? no → left | 7.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
- Time O(h): still one node per level. O(log n) balanced, O(n) for a straight-line tree. Creating the node itself is O(1).
- Space O(1): just
cur. No extra data structure, no call stack. (The one new node is the answer, not extra space.)
cur = root · look at the child before stepping · None child → attach + break · return root, not cur.Part C · Revision page
| Recursive | Iterative | |
|---|---|---|
| empty tree | handled by the base case | separate check before the loop |
| reaching the spot | step onto None, create there, return it | stop on the parent, check the child is None, attach |
| connecting | root.left/right = call(...) + return root | cur.left/right = TreeNode(val) |
| returns | root of each subtree (unchanged) | the original root |
| time | O(h) | O(h) |
| space | O(h) stack | O(1) |
| Search in BST | Insert into BST | |
|---|---|---|
| reaching None means | not found → return None | found the spot → create the node |
| child call result | passed straight up (return call) | attached (root.x = call), then return root |
| iterative pointer | can move root itself | needs cur, root must be returned |
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.
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)
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