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:
# Note:
# BST traversal: root -> either -> left or -> right
# 1. Process -> root :
# determine if LCA is in left or right
# 2. Process -> left or -> right :
# Result: find LCA node
# process root -> :
# p and q are smaller, LCA in left subtree
if p.val < root.val and q.val < root.val:
# process -> left :
return self.lowestCommonAncestor(root.left, p, q)
# process root -> :
# p and q are larger, LCA in right subtree
elif p.val > root.val and q.val > root.val:
# process -> right :
return self.lowestCommonAncestor(root.right, p, q)
# process root -> :
# p and q are in different subtrees, root is LCA
else:
return root
# overall: time complexity
# overall: space complexity| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
Solution 2: [BST] Iterative BST Traversal - Tree/BST Guided Iterative Traversal
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
# Note:
# BST traversal: root -> either -> left or -> right
# 1. Process -> root :
# determine if LCA is in left or right
# 2. Process -> left or -> right :
# Result: find LCA node
# iterative root
while root:
# process root -> :
# p and q are smaller, LCA in left subtree
if p.val < root.val and q.val < root.val:
# process -> left :
root = root.left
# process root ->
# p and q are larger, LCA in right subtree
elif p.val > curr.val and q.val > curr.val:
# process -> right :
curr = curr.right
# process root -> :
# p and q are in different subtrees, root is LCA
else:
return curr
# overall: time complexity
# overall: space complexity| Aspect | Time Complexity | Space Complexity | Time Remarks | Space Remarks |
|---|---|---|---|---|
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
#
# Example:
# 4
# / \
# 2 7
# / \
# 1 3
#
# Inserting val = 5:
# 4
# / \
# 2 7
# / \ /
# 1 3 5
# 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 not root:
# 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:
# Idea:
# 1. Traverse tree from root iteratively
# 2. At each node, compare val with node.val
# 3. Move left or right based on BST property
# 4. When a None child is found, insert the new node
if not root:
# If tree is empty, new node becomes root
# tc O(1), sc O(1)
return TreeNode(val)
current = root
while True:
if val < current.val:
# Go left if left child exists
if current.left:
current = current.left
else:
# Insert new node as left child
current.left = TreeNode(val)
# tc O(1), sc O(1) for node creation
break
else:
# Go right if right child exists
if current.right:
current = current.right
else:
# Insert new node as right child
current.right = TreeNode(val)
# tc O(1), sc O(1) for node creation
break
# Return root to maintain BST structure
# tc: O(h) traversal along height
# sc: O(1) iterative → no recursion stack
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]:
# Binary Search Tree Deletion:
# BST property: left < node.val < right
#
# Deletion has 3 main cases:
# 1. Node to delete has no children -> remove directly
# 2. Node has one child -> 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
if not root:
# Base case: key not found
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)
else:
# Found the node to delete
if not root.left and not root.right:
# Case 1: No children
return None # tc O(1), sc O(1)
elif not root.left:
# Case 2a: Only right child
return root.right # tc O(1), sc O(1)
elif not root.right:
# Case 2b: Only left child
return root.left # tc O(1), sc O(1)
else:
# Case 3: Two children
# Find inorder successor: leftmost node in right subtree
successor = root.right
while successor.left:
successor = successor.left # tc O(h) worst case
# Replace root value with successor's value
root.val = successor.val
# Delete successor node in right subtree recursively
root.right = self.deleteNode(root.right, successor.val)
# tc: O(h) for deletion path
# Return root to maintain BST structure
# overall: tc O(h), 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 deleteNode(self, root: Optional[TreeNode], key: int) -> Optional[TreeNode]:
# Iterative BST Deletion
# Idea:
# 1. Use pointers to track current node and parent
# 2. Traverse the tree to find the node with the key
# 3. Once found, handle three deletion cases:
# a. No children -> just remove
# b. One child -> replace node with child
# c. Two children -> find inorder successor (smallest in right subtree),
# replace value, then remove successor
if not root:
return None # tc O(1), sc O(1)
# Dummy parent for convenience
dummy = TreeNode(0)
dummy.left = root
parent = dummy
current = root
is_left_child = True # Track whether current is left or right child of parent
# Step 1: 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
# Key not found
if not current:
return root # tc O(h), sc O(1)
# 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 might not change)
# overall: tc O(h), sc O(1) iterative
return dummy.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 |
|---|---|---|---|---|