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 that follows these rules:
- each node contains a unique key
- all nodes in the left subtree of a node contain values strictly less than the nodes value
- all nodes in the right subtree of a node contain values strictly greater than the nodes value
BST Diagram (DFS In Order: Left -> Root -> Right)
4
/ \
2 5
/ \
1 3
left subtree of 4: {1, 2, 3} all < 4
right subtree of 4: {5} all > 4
DFS in-order visit: 1 → 2 → 3 → 4 → 5235. Lowest Common Ancestor of a Binary Search Tree ::2:: - Medium
Topics: Tree Structure Analysis, 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. Meaning, find a root node that contains p and q split between their left and right subtree.
Pseudocode
Sol 1: Recursive BST Traversal
1. if p.val < root.val and q.val < root.val:
Return lowestCommonAncestor(root.left, p, q)
2. elif p.val > root.val and q.val > root.val:
Return lowestCommonAncestor(root.right, p, q)
3. else:
Return root
Sol 2: Iterative BST Traversal
1. node = root
2. While node:
a. if p.val < node.val and q.val < node.val:
node = node.left
b. elif p.val > node.val and q.val > node.val:
node = node.right
c. else:
Return node
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 Structure Analysis, 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 correct spot in the binary tree.
Pseudocode
Sol 1: Recursive Traversal
1. if root == None:
Return TreeNode(val)
2. if val < root.val:
root.left = insertIntoBST(root.left, val)
3. else:
root.right = insertIntoBST(root.right, val)
4. Return root
Sol 2: Iterative Traversal
1. if not root: return TreeNode(val)
2. current = root
3. While True:
a. if val < current.val:
if current.left: current = current.left
else: current.left = TreeNode(val); break
b. else:
if current.right: current = current.right
else: current.right = TreeNode(val); break
4. Return root
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 Structure Analysis, 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 and reorganize binary tree to be valid again.
Utilize the BST properties to split delete into 3 cases: Removed node has either 0, 1, or 2 children.
Pseudocode
Sol 1: Recursive Traversal
1. if root == None: return None
2. if key < root.val:
root.left = deleteNode(root.left, key)
3. elif key > root.val:
root.right = deleteNode(root.right, key)
4. else:
a. if root.left == None and root.right == None:
Return None
b. elif root.left == None:
Return root.right
c. elif root.right == None:
Return root.left
d. else:
successor = root.right
while successor.left: successor = successor.left
root.val = successor.val
root.right = deleteNode(root.right, successor.val)
5. Return root
Sol 2: Iterative Traversal
1. if not root: return None
2. dummyHead = TreeNode(0), dummyHead.left = root
3. parent = dummyHead
4. current = root
5. (is_left_child = True)
6. While current and current.val != key:
a. parent = current
b. if key < current.val:
current = current.left, is_left_child = True
c. else:
current = current.right, is_left_child = False
7. if not current: return root
8. if current.left and current.right:
a. successor_parent = current, successor = current.right
b. while successor.left: successor_parent = successor, successor = successor.left
c. current.val = successor.val
d. parent = successor_parent, current = successor
e. is_left_child = (successor_parent.left == successor)
9. child = current.left if current.left else current.right
10. if is_left_child: parent.left = child
11. else: parent.right = child
12. Return dummyHead.left
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 Structure Analysis, 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.
A binary search tree is a type of tree that follows these rules:
- each node contains a unique key
- all nodes in the left subtree of a node contain values strictly less than the nodes value
- all nodes in the right subtree of a node contain values strictly greater than the nodes value
Pseudocode
Sol 1: DFS Pre Order Recursive Passing Range Limits
1. dfs(node, low, high):
a. if not node: return True
b. if not (low < node.val < high): return False
c. left_valid = dfs(node.left, low, node.val)
d. right_valid = dfs(node.right, node.val, high)
e. Return left_valid and right_valid
2. Return dfs(root, -inf, inf)
Sol 2: BFS Pre Order Iterative Passing Queuing Range Limits
1. queue = deque([(root, -inf, inf)])
2. While queue:
a. (node, low, high) = queue.popleft()
b. if not node: continue
c. if not (low < node.val < high): Return False
d. queue.append((node.left, low, node.val))
e. queue.append((node.right, node.val, high))
3. Return True
Solution 1: [DFS] 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] 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 Structure Analysis, 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.
Pseudocode
Sol 1: DFS In Order Recursive Early Stop with Global Element Counter
1. (result = None)
2. inorder(node):
a. if not node or result is not None: return
b. inorder(node.left)
c. k -= 1
d. if k == 0:
result = node.val
return
e. inorder(node.right)
3. inorder(root)
4. Return result
Sol 2: DFS In Order Iterative with Element Counter
1. stack = []
2. While True:
a. while root:
stack.append(root)
root = root.left
b. root = stack.pop()
c. k -= 1
d. if k == 0: Return root.val
e. root = root.right
Solution 1: [DFS] 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] 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 Structure Analysis, 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
Given a binary tree, and boundaries [low, high], remove all nodes that are not within the boundaries.
Utilize the BST property to invalidate entire left or right subtrees to optimize.
Pseudocode
Sol 1: DFS Pruning Optimization
1. if not root: return None
2. if root.val < low:
Return trimBST(root.right, low, high)
3. if root.val > high:
Return trimBST(root.left, low, high)
4. root.left = trimBST(root.left, low, high)
5. root.right = trimBST(root.right, low, high)
6. Return root
Solution 1: [DFS] DFS 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
#
# We can use the BST property to prune entire subtrees, instead of exploring invalid subtrees.
#
# 1. If node.val < low:
# - the entire left subtree is invalid
# - return trimmed right subtree
#
# 2. If node.val > high:
# - the entire right subtree is invalid
# - return trimmed left subtree
# 3. Otherwise:
# - node is valid
# - recursively into both left and right subtrees to trim
if not root:
return None
# Node too small:
# discard left + trim right
if root.val < low:
return self.trimBST(root.right, low, high)
# Node too large:
# discard right + trim left
if root.val > high:
return self.trimBST(root.left, low, high)
# Node is valid:
# trim left and right
root.left = self.trimBST(root.left, low, high)
root.right = self.trimBST(root.right, low, high)
# overall: O(n)
# overall: O(log(n)) for balanced / O(n) for skewed
return root