LeetCode: Trees I BST

Binary Search Tree Intro
What is a Binary Search Tree
Trees are hierarchical data structures representing relationships between entities, often in a parent-child format. A binary search tree is a type of tree where:
235. Lowest Common Ancestor of a Binary Search Tree ::2:: - Medium
Topics: Tree, Depth First Search, Binary Search Tree, Binary Tree
Intro
Given a binary search tree (BST), find the lowest common ancestor (LCA) node of two given nodes in the BST. According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).”
| Example Input | Output |
|---|---|
| root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 | 6 |
| root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 | 2 |
| root = [2,1], p = 2, q = 1 | 2 |
Constraints:
The number of nodes in the root tree is in the range [1, 105].
-104 ≤ root.val ≤ 104
All Node.val are unique.
p != q
p and q will exist in the BST.
Abstraction
Find lowest node that has both p and q in their subtree.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [BST] Recursive BST Traversal - Tree/BST Guided Recursive Traversal
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
# Lowest Common Ancestor:
# Lowest node height/depth wise in a tree that has both p and q above it
# BST Traversal: (root -> left or right)
# BST property (left < root < right) guides us toward the LCA at each node
# - If both p and q are smaller, LCA must be in left subtree
# - If both p and q are larger, LCA must be in right subtree
# - Otherwise root is the split point — root is the LCA
# Both p and q are smaller: LCA in left subtree
if p.val < root.val and q.val < root.val:
return self.lowestCommonAncestor(root.left, p, q)
# Both p and q are larger: LCA in right subtree
elif p.val > root.val and q.val > root.val:
return self.lowestCommonAncestor(root.right, p, q)
# p and q are on different sides: root is the LCA
else:
return root
# Guaranteed to have the lowest point:
# The moment p and q are on different sides,
# any node deeper would only contain one of them, not both.
# So the split node is the lowest node that
# still has both p and q as descendants
# overall: tc O(log n) for balanced / O(n) for skewed trees
# overall: sc O(log n) for balanced / O(n) for skewed trees| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| BST Traversal | O(log n) / O(n) | O(log n) / O(n) | Guided by BST property, worst case height of tree | Guided by BST property, worst case height of tree |
| Per Node Work | O(1) | O(1) | Two Comparisons | No extra allocation |
| Overall | O(log n) / O(n) | O(log n) / O(n) | Balanced vs skewed trees | Balanced vs skewed trees |
Solution 2: [BST] Iterative BST Traversal - Tree/BST Guided Iterative Traversal
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
# BST Traversal: (root -> left or right) using Iteration
# - BST property guides us toward the LCA at each node
# - If both p and q are smaller, LCA must be in left subtree
# - If both p and q are larger, LCA must be in right subtree
# - Otherwise root is the split point — root is the LCA
# Tree Iterator
# sc: O(1)
node = root
# Traverse until LCA is found
while node:
# Both p and q are smaller — LCA in left subtree
if p.val < node.val and q.val < node.val:
node = node.left
# Both p and q are larger — LCA in right subtree
elif p.val > node.val and q.val > node.val:
node = node.right
# p and q are on different sides — node is the LCA
else:
return node
# Guaranteed to have the lowest point:
# The moment p and q are on different sides,
# any node deeper would only contain one of them, not both.
# So the split node is the lowest node that
# still has both p and q as descendants
# overall: tc O(log n) for balanced / O(n) for skewed trees
# overall: sc O(1)| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
| BST Traversal | O(log n) / O(n) | O(1) | Guided by BST property, worst case height of tree | Single pointer |
| Per Node Work | O(1) | O(1) | Two Comparisons | No extra allocation |
| Overall | O(log n) / O(n) | O(1) | Balanced vs skewed trees | No call stack |
701. Insert into a Binary Search Tree ::2:: - Medium
Topics: Tree, Binary Search Tree, Binary Tree
Intro
You are given the root node of a binary search tree (BST) and a value to insert into the tree. Return the root node of the BST after the insertion. It is guaranteed that the new value does not exist in the original BST. Notice that there may exist multiple valid ways for the insertion, as long as the tree remains a BST after insertion. You can return any of them.
| Example Input | Output |
|---|---|
| root = [4,2,7,1,3], val = 5 | [4,2,7,1,3,5] |
| root = [40,20,60,10,30,50,70], val = 25 | [40,20,60,10,30,50,70,null,null,25] |
| root = [4,2,7,1,3,null,null,null,null,null,null], val = 5 | [4,2,7,1,3,5] |
Constraints:
The number of nodes in the tree is in the range [1, 104].
-108 ≤ Node.val ≤ 108
All Node.val are unique.
-1010 ≤ val ≤ 108
It's guaranteed that val does not exist in the original BST.
Abstraction
Given a node, find its place in the binary tree
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [BST] Recursive Traversal - Tree/BST Guided Recursive Traversal
def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
# Binary Search Tree:
# BST property: For any node:
# left subtree nodes < node.val < right subtree nodes
# Recursive DFS:
# 1. Compare val with root.val
# 2. If val < root.val, recurse left
# 3. If val > root.val, recurse right
# 4. If reached None, insert node here
# Base case: reached a leaf, insert new node here
if root == None:
# tc O(1), sc O(1) for node creation
return TreeNode(val)
# Decide whether to insert left or right
if val < root.val:
# Insert into left subtree
root.left = self.insertIntoBST(root.left, val)
else:
# Insert into right subtree
root.right = self.insertIntoBST(root.right, val)
# Return root node to maintain BST structure upwards
# tc: O(h) for traversing height of tree
# sc: O(h) recursion stack (O(log n) balanced, O(n) skewed)
return root| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [BST] Iterative Traversal - Tree/BST Guided Iterative Traversal
def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]:
# Iterative BST insertion:
# Traverse tree from root iteratively to avoid recursive stack
# - At each node, compare node.val with val
# - Move left or right to maintain the BST property
# - When reached a 'None' child, have found insertion point for val
# Empty Case:
# If tree is empty, new node becomes root
if not root:
return TreeNode(val)
# Tree Traversal
# sc: O(1)
current = root
# Iterate until we have reached insertion point for val
while True:
# Check left
if val < current.val:
# Go left if left child exists
if current.left:
current = current.left
# Reached 'None', insert val here
else:
current.left = TreeNode(val)
break
# Check right
else:
# Go right if right child exists
if current.right:
current = current.right
# Reached 'None', insert val here
else:
current.right = TreeNode(val)
break
# Return original root to maintain BST structure
# overall: tc O(log n) for balanced / O(n) for skewed trees
# overall: sc: O(1)
return root| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
450. Delete Node in a BST ::2:: - Medium
Topics: Tree, Binary Search Tree, Binary Tree
Intro
Given a root node reference of a BST and a key, delete the node with the given key in the BST. Return the root node reference (possibly updated) of the BST. Basically, the deletion can be divided into two stages:
- Search for a node to remove.
- If the node is found, delete the node. Follow up: Could you solve it with time complexity O(height of tree)?
| Example Input | Output |
|---|---|
| root = [5,3,6,2,4,null,7], key = 3 | [5,4,6,2,null,null,7] |
| root = [5,3,6,2,4,null,7], key = 0 | [5,3,6,2,4,null,7] |
| root = [], key = 0 | [] |
Constraints:
The number of nodes in the tree is in the range [0, 104].
-105 ≤ Node.val ≤ 105
Each node has a unique value
root is a valid binary search tree
-105 ≤ key ≤ 105
Abstraction
Given a target node, remove it from a binary tree if it exists.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [BST] Recursive Traversal - Tree/BST Guided Recursive Traversal
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
# BST property: left < node.val < right
# Binary Search Tree Deletion: 3 main cases
# 1. Node to delete has no children:
# a. remove directly
# 2. Node has one child:
# a. replace node with child
# 3. Node has two children:
# a. find inorder successor (smallest node in right subtree)
# b. replace node value with successor
# c. delete successor node recursively
# Base case:
# Reached 'None' child, key was not found
if root == None:
return None
# Traverse left or right depending on BST property
if key < root.val:
# Key may be in left subtree
root.left = self.deleteNode(root.left, key)
elif key > root.val:
# Key may be in right subtree
root.right = self.deleteNode(root.right, key)
# Key matches current node,
# check 3 BST deletion cases
else:
# Case 1: Node has no children
# - remove by returning 'None' up the recursion calls
if root.left == None and root.right == None:
return None
# Case 2: Node only has 1 child
# - remove by returning the 1 child
elif root.left == None:
return root.right
elif root.right == None:
return root.left
# Case 3: Node has 2 children
# - find inorder successor (smallest node in right subtree)
# - replace node value with successor
# - delete successor node recursively
else:
# Grab right subtree
successor = root.right
# Find smallest node in right subtree
# tc O(h) worst case
while successor.left:
successor = successor.left
# Replace key successor's value
root.val = successor.val
# Delete successor node from right subtree recursively
# tc: O(h) for deletion path
root.right = self.deleteNode(root.right, successor.val)
# Return original root to maintain BST structure
# overall: tc O(log n) for balanced / O(n) for skewed trees
# overall: sc O(log n) for balanced / O(n) for skewed trees
return root| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [BST] Iterative Traversal - Tree/BST Guided Iterative Traversal
def deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
# Iterative BST Deletion
# Use pointers to track current node and parent to avoid recursive calls
# BST property: left < node.val < right
# Binary Search Tree Deletion: 3 main cases
# 1. Node to delete has no children:
# a. remove directly
# 2. Node has one child:
# a. replace node with child
# 3. Node has two children:
# a. find inorder successor (smallest node in right subtree)
# b. replace node value with successor
# c. delete successor node recursively
# Base case:
# Reached 'None' child, key was not found
if not root:
return None
# Dummyhead to keep track of original parent for convenience
# sc: O(1)
dummyHead = TreeNode(0)
dummyHead.left = root
parent = dummyHead
# BST Iterator
# sc: O(1)
current = root
# BST Info:
# Track whether current is left or right child of parent,
# to validate 3 edge cases
# sc: O(1)
is_left_child = True
# Step 1: Iterate over BST to find node to delete
while current and current.val != key:
parent = current
if key < current.val:
current = current.left
is_left_child = True
else:
current = current.right
is_left_child = False
# Early Exit:
# Key was not found, return original root
if not current:
return root
# Step 2: Node has two children
if current.left and current.right:
# Find inorder successor (leftmost node of right subtree)
successor_parent = current
successor = current.right
while successor.left:
successor_parent = successor
successor = successor.left # tc O(h)
# Replace current value with successor value
current.val = successor.val
# Prepare to delete successor node
parent = successor_parent
current = successor
is_left_child = (successor_parent.left == successor)
# Step 3: Node has at most one child
child = current.left if current.left else current.right # could be None
# Attach child to parent
if is_left_child:
parent.left = child
else:
parent.right = child
# Return root original root
# overall: tc O(h)
# overall: sc O(1)
return dummyHead.left| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
98. Validate Binary Search Tree ::2:: - Medium
Topics: Tree, Depth First Search, Binary Search Tree, Binary Tree
Intro
Given the root of a binary tree, determine if it is a valid binary search tree (BST). The left subtree of a node contains only nodes with keys strictly less than the node's key. The right subtree of a node contains only nodes with keys strictly greater than the node's key. Both the left and right subtrees must also be binary search trees.
| Example Input | Output |
|---|---|
| root = [2,1,3] | true |
| root = [5,1,4,null,null,3,6] | false |
Constraints:
The number of nodes in the root tree is in the range [1, 104].
-231 ≤ Node.val ≤ 231-1
Abstraction
Given a tree, determine if tree is a valid BST. Where value of nodes is always: left.val ≤ root.val ≤ right.val
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: DFS Pre Order Recursive Passing Range Limits - Tree/DFS Pre order Traversal
def isValidBST(self, root: Optional[TreeNode]) -> bool:
# Note:
# DFS pre order: root -> left -> right
# 1. Process root -> :
# validate order
# 2. Process -> left -> right :
# Result: validated BST
def dfs(node, low, high):
# Process root -> :
if not node:
return True
# Process root -> : validate order
if not (low < node.val < high):
return False
# Process -> left -> right :
left_valid = dfs(node.left, low, node.val)
right_valid = dfs(node.right, node.val, high)
# Process -> left -> right :
subtreeValid = left_valid and right_valid
return subtreeValid
# Start with infinite bounds
treeValid = dfs(root, float('-inf'), float('inf'))
# overall: time complexity
# overall: space complexity
return treeValid| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: BFS Pre Order Iterative Passing Queuing Range Limits - Tree/DFS Pre order Traversal
def isValidBST(self, root: Optional[TreeNode]) -> bool:
# Note:
# BFS pre order: process level: root -> left -> right
# 1. Process level:
# Process root -> :
# validate range limits
# 2. Process -> left -> right :
# Result: validated BST
# iterative queue
queue = deque([(root, float('-inf'), float('inf'))])
while queue:
# Process root -> :
node, low, high = queue.popleft()
# Process root -> :
if not node:
continue
# Process root -> : validate order
if not (low < node.val < high):
return False
# Process -> left -> right :
queue.append((node.left, low, node.val))
queue.append((node.right, node.val, high))
# overall: time complexity
# overall: space complexity
return True230. Kth Smallest Element in a BST ::2:: - Medium
Topics: Tree, Depth First Search, Binary Search Tree, Binary Tree
Intro
Given the root of a binary search tree, and an integer k, return the kth smallest value (1-indexed) of all the values of the nodes in the tree. Follow up: If the BST is modified often (i.e., we can do insert and delete operations) and you need to find the kth smallest frequently, how would you optimize?
| Example Input | Output |
|---|---|
| root = [3,1,4,null,2], k = 1 | 1 |
| root = [5,3,6,2,4,null,null,1], k = 3 | 3 |
Constraints:
The number of nodes in the root tree is n
1 ≤ k ≤ n ≤ 104
0 ≤ Node.val ≤ 104
Abstraction
Given a BST, find the kth smallest node.
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force: iterative
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: DFS In Order Recursive Early Stop with Global Element Counter - Tree/DFS Pre order Traversal
def isValidBST(self, root: Optional[TreeNode]) -> bool:
# Note:
# DFS in order: left -> root -> right
# 1. Process left -> :
# 2. Process -> root -> :
# if reached kth node, return
# 3. Process -> right :
# Result: kth smallest found
# global counter
k = k
# global pointer
result = None
def inorder(node):
nonlocal k
nonlocal result
# Process left -> :
if not node or result is not None:
return
# Process left -> :
inorder(node.left)
# Process root -> :
# if kth node, return
k -= 1
if k == 0:
result = node.val
return
# Process right -> :
inorder(node.right)
inorder(root)
# overall: time complexity
# overall: space complexity
return result| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: DFS In Order Iterative with Element Counter - Tree/DFS Pre order Traversal
def isValidBST(self, root: Optional[TreeNode]) -> bool:
# Note:
# DFS in order: left -> root -> right
# 1. Process left -> :
# 2. Process -> root -> :
# if reached kth node, return
# 3. Process -> right :
# Result: kth smallest found
# iterative stack
stack = []
while True:
# Process left -> :
while root:
stack.append(root)
root = root.left
# Process -> root -> :
root = stack.pop()
# Process -> root -> :
# if kth node, return
k -= 1
if k == 0:
return root.val
# Process -> right :
root = root.right
# overall: time complexity
# overall: space complexity| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
669. Trim a Binary Search Tree ::1:: - Medium
Topics: Tree, Depth First Search, Binary Search Tree, Binary Tree
Intro
Given the root of a binary search tree and the lowest and highest boundaries as low and high, trim the tree so that all its elements lies in [low, high]. Trimming the tree should not change the relative structure of the elements that will remain in the tree (i.e., any node's descendant should remain a descendant). It can be proven that there is a unique answer. Return the root of the trimmed binary search tree. Note that the root may change depending on the given bounds.
| Example Input | Output |
|---|---|
| root = [1,0,2], low = 1, high = 2 | [1,null,2] |
| root = [3,0,4,null,2,null,null,1], low = 1, high = 3 | [3,2,null,1] |
Constraints:
The number of nodes in the tree is in the range [1, 10^4]
0 ≤ Node.val ≤ 10^4
The value of each node in the tree is unique
root is guaranteed to be a valid binary search tree
0 ≤ low ≤ high ≤ 10^4
Abstraction
something!
Space & Time Complexity
| Solution | Time Complexity | Space Complexity | Time Remark | Space Remark |
|---|---|---|---|---|
| Bug | Error |
|---|---|
Brute Force:
| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Find the Bug:
Solution 1: [DFS] DFS And BFS Pruning Optimization - Tree/DFS Post Order Recursive Two Sided Bottom Up
def trimBST(self, root: Optional[TreeNode], low: int, high: int) -> Optional[TreeNode]:
# DFS + BST Optimization
#
# Idea:
# - Use BST property to prune entire subtrees.
#
# Rules:
# 1. If node.val < low:
# - left subtree is invalid
# - return trimmed right subtree
#
# 2. If node.val > high:
# - right subtree is invalid
# - return trimmed left subtree
# 3. Otherwise:
# - node is valid
# - recursively trim both children
#
# Complexity:
# - Time: O(n)
# - Space: O(h) (recursion stack)
if not root:
return None
# Node too small → discard left side
if root.val < low:
return self.trimBST(root.right, low, high)
# Node too large → discard right side
if root.val > high:
return self.trimBST(root.left, low, high)
# Node is valid → trim children
root.left = self.trimBST(root.left, low, high)
root.right = self.trimBST(root.right, low, high)
return root