- Authors

- Name
- Youngju Kim
- @fjvbn20031
FAANG Coding Interview Mastery: Trees & Graphs Core Patterns
Trees and graphs rank second only to arrays and strings in FAANG interview frequency. Google and Meta in particular place heavy emphasis on graph and tree traversal. This guide covers 5 essential patterns with complete, working Python implementations.
1. Binary Tree Core Patterns
DFS (Depth-First Search)
DFS explores a tree by going as deep as possible before backtracking. It can be implemented recursively or iteratively with a stack.
Tree Node Definition
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
Three DFS Traversals (Recursive)
# Inorder: Left → Current → Right (sorted order in BST)
def inorder(root):
if not root:
return []
return inorder(root.left) + [root.val] + inorder(root.right)
# Preorder: Current → Left → Right (tree copying, serialization)
def preorder(root):
if not root:
return []
return [root.val] + preorder(root.left) + preorder(root.right)
# Postorder: Left → Right → Current (directory size calculation)
def postorder(root):
if not root:
return []
return postorder(root.left) + postorder(root.right) + [root.val]
Iterative DFS (with explicit stack)
def inorderIterative(root):
result = []
stack = []
current = root
while current or stack:
# Go as far left as possible
while current:
stack.append(current)
current = current.left
current = stack.pop()
result.append(current.val)
current = current.right
return result
def preorderIterative(root):
if not root:
return []
result = []
stack = [root]
while stack:
node = stack.pop()
result.append(node.val)
# Push right first so left is processed first
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return result
BFS (Breadth-First Search) — Level Order Traversal
from collections import deque
def levelOrder(root):
if not root:
return []
result = []
queue = deque([root])
while queue:
level_size = len(queue)
current_level = []
for _ in range(level_size):
node = queue.popleft()
current_level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(current_level)
return result
# Example:
# Tree: 3
# / \
# 9 20
# / \
# 15 7
# Output: [[3], [9,20], [15,7]]
Problem 1: Maximum Depth of Binary Tree (LeetCode 104)
def maxDepth(root) -> int:
if not root:
return 0
return 1 + max(maxDepth(root.left), maxDepth(root.right))
# BFS approach
def maxDepthBFS(root) -> int:
if not root:
return 0
depth = 0
queue = deque([root])
while queue:
depth += 1
for _ in range(len(queue)):
node = queue.popleft()
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return depth
Time Complexity: O(n) — every node visited once Space Complexity: O(h) — h is tree height; O(n) worst case for skewed trees
Problem 2: Invert Binary Tree (LeetCode 226)
def invertTree(root):
if not root:
return None
root.left, root.right = invertTree(root.right), invertTree(root.left)
return root
# Iterative approach (BFS)
def invertTreeBFS(root):
if not root:
return None
queue = deque([root])
while queue:
node = queue.popleft()
node.left, node.right = node.right, node.left
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return root
Time Complexity: O(n) Space Complexity: O(n)
Problem 3: Symmetric Tree (LeetCode 101)
Check whether a binary tree is a mirror of itself.
def isSymmetric(root) -> bool:
def isMirror(left, right):
if not left and not right:
return True
if not left or not right:
return False
return (left.val == right.val and
isMirror(left.left, right.right) and
isMirror(left.right, right.left))
return isMirror(root.left, root.right)
# Iterative approach
def isSymmetricIterative(root) -> bool:
queue = deque([(root.left, root.right)])
while queue:
left, right = queue.popleft()
if not left and not right:
continue
if not left or not right:
return False
if left.val != right.val:
return False
queue.append((left.left, right.right))
queue.append((left.right, right.left))
return True
Problem 4: Path Sum II (LeetCode 113)
Find all root-to-leaf paths where the sum equals targetSum.
def pathSum(root, targetSum: int):
result = []
def dfs(node, current_path, remaining):
if not node:
return
current_path.append(node.val)
remaining -= node.val
# Leaf node with correct sum
if not node.left and not node.right and remaining == 0:
result.append(list(current_path))
else:
dfs(node.left, current_path, remaining)
dfs(node.right, current_path, remaining)
# Backtrack
current_path.pop()
dfs(root, [], targetSum)
return result
Problem 5: Lowest Common Ancestor (LeetCode 236)
Find the LCA of two nodes p and q in a binary tree.
def lowestCommonAncestor(root, p, q):
if not root or root == p or root == q:
return root
left = lowestCommonAncestor(root.left, p, q)
right = lowestCommonAncestor(root.right, p, q)
# Found on both sides → current node is LCA
if left and right:
return root
# Found on one side only → that side is the LCA
return left if left else right
# Example:
# 3
# / \
# 5 1
# / \ / \
# 6 2 0 8
# / \
# 7 4
# p=5, q=1 → LCA = 3
# p=5, q=4 → LCA = 5
Problem 6: Serialize and Deserialize Binary Tree (LeetCode 297) — Hard
class Codec:
def serialize(self, root) -> str:
"""Serialize tree to string using BFS"""
if not root:
return ""
result = []
queue = deque([root])
while queue:
node = queue.popleft()
if node:
result.append(str(node.val))
queue.append(node.left)
queue.append(node.right)
else:
result.append("null")
return ",".join(result)
def deserialize(self, data: str):
"""Reconstruct tree from serialized string"""
if not data:
return None
values = data.split(",")
root = TreeNode(int(values[0]))
queue = deque([root])
i = 1
while queue:
node = queue.popleft()
if i < len(values) and values[i] != "null":
node.left = TreeNode(int(values[i]))
queue.append(node.left)
i += 1
if i < len(values) and values[i] != "null":
node.right = TreeNode(int(values[i]))
queue.append(node.right)
i += 1
return root
Time Complexity: O(n) for both operations Space Complexity: O(n)
2. Binary Search Tree (BST) Patterns
BST Properties
- All values in the left subtree are less than the current node
- All values in the right subtree are greater than the current node
- Inorder traversal visits nodes in sorted order
Problem 7: Validate Binary Search Tree (LeetCode 98)
def isValidBST(root) -> bool:
def validate(node, min_val, max_val):
if not node:
return True
if node.val <= min_val or node.val >= max_val:
return False
return (validate(node.left, min_val, node.val) and
validate(node.right, node.val, max_val))
return validate(root, float('-inf'), float('inf'))
# Common pitfall: simply checking left.val < root.val < right.val is WRONG
# Example of invalid BST that passes the naive check:
# 5
# / \
# 1 4
# / \
# 3 6
# root.right.left = 3 (violates BST: 3 < 5 should not appear in right subtree)
Problem 8: Kth Smallest Element in BST (LeetCode 230)
def kthSmallest(root, k: int) -> int:
count = [0]
result = [0]
def inorder(node):
if not node or count[0] >= k:
return
inorder(node.left)
count[0] += 1
if count[0] == k:
result[0] = node.val
return
inorder(node.right)
inorder(root)
return result[0]
# Iterative approach (more explicit)
def kthSmallestIterative(root, k: int) -> int:
stack = []
current = root
while current or stack:
while current:
stack.append(current)
current = current.left
current = stack.pop()
k -= 1
if k == 0:
return current.val
current = current.right
return -1
Problem 9: Convert Sorted Array to BST (LeetCode 108)
Convert a sorted array to a height-balanced BST.
def sortedArrayToBST(nums: list[int]):
def buildBST(left, right):
if left > right:
return None
# Always pick the middle element as root to ensure balance
mid = left + (right - left) // 2
root = TreeNode(nums[mid])
root.left = buildBST(left, mid - 1)
root.right = buildBST(mid + 1, right)
return root
return buildBST(0, len(nums) - 1)
3. Graph BFS/DFS Patterns
Graph Representations
# Adjacency list (preferred)
graph = {
0: [1, 2],
1: [0, 3],
2: [0, 4],
3: [1],
4: [2]
}
# Adjacency matrix
matrix = [
[0, 1, 1, 0, 0],
[1, 0, 0, 1, 0],
[1, 0, 0, 0, 1],
[0, 1, 0, 0, 0],
[0, 0, 1, 0, 0]
]
Standard Templates
# DFS (recursive)
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
# BFS
def bfs(graph, start):
visited = {start}
queue = deque([start])
while queue:
node = queue.popleft()
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
Problem 10: Number of Islands (LeetCode 200)
def numIslands(grid: list[list[str]]) -> int:
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == '0':
return
grid[r][c] = '0' # Mark as visited
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
for r in range(rows):
for c in range(cols):
if grid[r][c] == '1':
count += 1
dfs(r, c)
return count
# Example:
# grid = [
# ["1","1","0","0","0"],
# ["1","1","0","0","0"],
# ["0","0","1","0","0"],
# ["0","0","0","1","1"]
# ]
# Output: 3
Time Complexity: O(mn) Space Complexity: O(mn) for recursion stack
Problem 11: Clone Graph (LeetCode 133)
class Node:
def __init__(self, val=0, neighbors=None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
def cloneGraph(node):
if not node:
return None
cloned = {} # Maps original node to its clone
def dfs(original):
if original in cloned:
return cloned[original]
copy = Node(original.val)
cloned[original] = copy
for neighbor in original.neighbors:
copy.neighbors.append(dfs(neighbor))
return copy
return dfs(node)
Time Complexity: O(V + E) Space Complexity: O(V)
Problem 12: Course Schedule (LeetCode 207) — Topological Sort
Determine if you can finish all courses given prerequisite relationships.
from collections import defaultdict, deque
def canFinish(numCourses: int, prerequisites: list[list[int]]) -> bool:
graph = defaultdict(list)
in_degree = [0] * numCourses
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
# Start with courses that have no prerequisites
queue = deque([i for i in range(numCourses) if in_degree[i] == 0])
completed = 0
while queue:
course = queue.popleft()
completed += 1
for next_course in graph[course]:
in_degree[next_course] -= 1
if in_degree[next_course] == 0:
queue.append(next_course)
return completed == numCourses
# DFS cycle detection
def canFinishDFS(numCourses: int, prerequisites: list[list[int]]) -> bool:
graph = defaultdict(list)
for course, prereq in prerequisites:
graph[prereq].append(course)
# States: 0 = unvisited, 1 = visiting, 2 = done
state = [0] * numCourses
def hasCycle(node):
if state[node] == 1: # Cycle detected
return True
if state[node] == 2: # Already processed
return False
state[node] = 1
for neighbor in graph[node]:
if hasCycle(neighbor):
return True
state[node] = 2
return False
for i in range(numCourses):
if hasCycle(i):
return False
return True
Problem 13: Pacific Atlantic Water Flow (LeetCode 417)
def pacificAtlantic(heights: list[list[int]]) -> list[list[int]]:
if not heights:
return []
rows, cols = len(heights), len(heights[0])
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
def bfs(starts):
visited = set(starts)
queue = deque(starts)
while queue:
r, c = queue.popleft()
for dr, dc in directions:
nr, nc = r + dr, c + dc
if (0 <= nr < rows and 0 <= nc < cols and
(nr, nc) not in visited and
heights[nr][nc] >= heights[r][c]): # Reverse flow
visited.add((nr, nc))
queue.append((nr, nc))
return visited
# Pacific: top row + left column
pacific_starts = [(0, c) for c in range(cols)] + [(r, 0) for r in range(1, rows)]
# Atlantic: bottom row + right column
atlantic_starts = [(rows-1, c) for c in range(cols)] + [(r, cols-1) for r in range(rows-1)]
pacific = bfs(pacific_starts)
atlantic = bfs(atlantic_starts)
return [[r, c] for r, c in pacific & atlantic]
Problem 14: Word Ladder (LeetCode 127)
Find the shortest transformation sequence from beginWord to endWord.
def ladderLength(beginWord: str, endWord: str, wordList: list[str]) -> int:
word_set = set(wordList)
if endWord not in word_set:
return 0
queue = deque([(beginWord, 1)])
visited = {beginWord}
while queue:
word, steps = queue.popleft()
for i in range(len(word)):
for c in 'abcdefghijklmnopqrstuvwxyz':
new_word = word[:i] + c + word[i+1:]
if new_word == endWord:
return steps + 1
if new_word in word_set and new_word not in visited:
visited.add(new_word)
queue.append((new_word, steps + 1))
return 0
# Example:
# beginWord = "hit", endWord = "cog"
# wordList = ["hot","dot","dog","lot","log","cog"]
# Output: 5 (hit -> hot -> dot -> dog -> cog)
Time Complexity: O(M^2 _ N) — M is word length, N is dictionary size Space Complexity: O(M^2 _ N)
4. Union Find (Disjoint Set) Pattern
Implementation
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
# Path Compression
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False # Already in same set
# Union by Rank
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True
def connected(self, x, y):
return self.find(x) == self.find(y)
Problem 15: Number of Connected Components (LeetCode 323)
def countComponents(n: int, edges: list[list[int]]) -> int:
uf = UnionFind(n)
components = n
for u, v in edges:
if uf.union(u, v):
components -= 1 # Two components merged
return components
Problem 16: Redundant Connection (LeetCode 684)
Find the edge that, when removed, results in a tree.
def findRedundantConnection(edges: list[list[int]]) -> list[int]:
n = len(edges)
uf = UnionFind(n + 1)
for u, v in edges:
if not uf.union(u, v):
return [u, v] # Already connected → redundant edge
return []
Problem 17: Accounts Merge (LeetCode 721)
def accountsMerge(accounts: list[list[str]]) -> list[list[str]]:
email_to_id = {}
email_to_name = {}
for i, account in enumerate(accounts):
name = account[0]
for email in account[1:]:
if email not in email_to_id:
email_to_id[email] = len(email_to_id)
email_to_name[email] = name
uf = UnionFind(len(email_to_id))
for account in accounts:
first_email = account[1]
first_id = email_to_id[first_email]
for email in account[2:]:
uf.union(first_id, email_to_id[email])
from collections import defaultdict
groups = defaultdict(list)
for email, eid in email_to_id.items():
root = uf.find(eid)
groups[root].append(email)
result = []
for root, emails in groups.items():
name = email_to_name[emails[0]]
result.append([name] + sorted(emails))
return result
5. Shortest Path Algorithms
Dijkstra's Algorithm
Finds single-source shortest paths in graphs with non-negative edge weights.
import heapq
from collections import defaultdict
def dijkstra(graph: dict, start: int) -> dict:
distances = {node: float('inf') for node in graph}
distances[start] = 0
pq = [(0, start)] # (distance, node)
while pq:
current_dist, current_node = heapq.heappop(pq)
# Skip if a shorter path has already been found
if current_dist > distances[current_node]:
continue
for neighbor, weight in graph[current_node]:
distance = current_dist + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(pq, (distance, neighbor))
return distances
Time Complexity: O((V + E) log V) Space Complexity: O(V)
Problem 18: Network Delay Time (LeetCode 743)
def networkDelayTime(times: list[list[int]], n: int, k: int) -> int:
graph = defaultdict(list)
for u, v, w in times:
graph[u].append((v, w))
distances = {i: float('inf') for i in range(1, n + 1)}
distances[k] = 0
pq = [(0, k)]
while pq:
dist, node = heapq.heappop(pq)
if dist > distances[node]:
continue
for neighbor, weight in graph[node]:
new_dist = dist + weight
if new_dist < distances[neighbor]:
distances[neighbor] = new_dist
heapq.heappush(pq, (new_dist, neighbor))
max_dist = max(distances.values())
return max_dist if max_dist != float('inf') else -1
# Example:
# times = [[2,1,1],[2,3,1],[3,4,1]], n=4, k=2
# Output: 2
Problem 19: Cheapest Flights Within K Stops (LeetCode 787)
Bellman-Ford variant with at most k intermediate stops.
def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
# Bellman-Ford: relax k+1 times
prices = [float('inf')] * n
prices[src] = 0
for i in range(k + 1):
temp = prices.copy() # Isolate each iteration's updates
for u, v, w in flights:
if prices[u] != float('inf') and prices[u] + w < temp[v]:
temp[v] = prices[u] + w
prices = temp
return prices[dst] if prices[dst] != float('inf') else -1
# Example:
# n=3, flights=[[0,1,100],[1,2,100],[0,2,500]], src=0, dst=2, k=1
# Output: 200 (0 -> 1 -> 2)
6. FAANG Company-Specific Trends
Google is particularly heavy on graph problems.
- Frequent: Graph traversal, topological sort, shortest path
- Key skills: Choosing the right algorithm and justifying complexity
- Example problems: Course Schedule II, Word Ladder, Alien Dictionary
Meta (Facebook)
Meta favors Tree Traversal and LCA problems.
- Frequent: Binary Tree, LCA, Serialize/Deserialize
- Key skills: Comfortable with both recursive and iterative implementations
- Example problems: Binary Tree Right Side View, Vertical Order Traversal
Amazon
Amazon leans toward BFS and shortest path problems.
- Frequent: Number of Islands, BFS variants, Union Find
- Key skills: Practical and efficient solutions
- Example problems: Rotting Oranges, Find if Path Exists in Graph
7. Practice Quiz
Quiz 1: Diameter of Binary Tree
Find the length of the longest path between any two nodes in a binary tree. The path does not need to pass through the root.
Answer: Use DFS; at each node compute the sum of left and right max depths and update a global maximum.
Explanation:
def diameterOfBinaryTree(root) -> int:
max_diameter = [0]
def depth(node):
if not node:
return 0
left_depth = depth(node.left)
right_depth = depth(node.right)
# Path through current node
max_diameter[0] = max(max_diameter[0], left_depth + right_depth)
return 1 + max(left_depth, right_depth)
depth(root)
return max_diameter[0]
Quiz 2: Detect Cycle in a Directed Graph
Graph {0: [1], 1: [2], 2: [0], 3: [4], 4: []} — does it contain a cycle?
Answer: Yes (0 -> 1 -> 2 -> 0)
Explanation: Use DFS with three-color marking (0=unvisited, 1=visiting, 2=done). If you encounter a node currently being visited, a cycle exists.
def hasCycle(graph):
state = {}
def dfs(node):
if state.get(node) == 1:
return True
if state.get(node) == 2:
return False
state[node] = 1
for neighbor in graph.get(node, []):
if dfs(neighbor):
return True
state[node] = 2
return False
return any(dfs(node) for node in graph if node not in state)
Quiz 3: Two Sum in a BST
Given a BST and target k, determine if there exist two nodes whose values sum to k.
Answer: Perform an inorder traversal to get a sorted list, then apply Two Pointers.
Explanation:
def findTarget(root, k: int) -> bool:
def inorder(node):
if not node:
return []
return inorder(node.left) + [node.val] + inorder(node.right)
nums = inorder(root)
left, right = 0, len(nums) - 1
while left < right:
total = nums[left] + nums[right]
if total == k:
return True
elif total < k:
left += 1
else:
right -= 1
return False
Quiz 4: Max Area of Island
In a binary grid, find the maximum area of a connected island of 1s.
Answer: DFS/BFS each island and count cells; return the maximum.
Explanation:
def maxAreaOfIsland(grid):
rows, cols = len(grid), len(grid[0])
def dfs(r, c):
if r < 0 or r >= rows or c < 0 or c >= cols or grid[r][c] == 0:
return 0
grid[r][c] = 0 # Mark visited
return 1 + dfs(r+1,c) + dfs(r-1,c) + dfs(r,c+1) + dfs(r,c-1)
return max(dfs(r, c) for r in range(rows) for c in range(cols) if grid[r][c])
Quiz 5: Minimum Spanning Tree — Kruskal's Algorithm
Use Union Find to connect n cities with minimum total cost. (Min Cost to Connect All Points, LeetCode 1584)
Answer: Sort all edges by cost, then greedily add edges using Union Find to avoid cycles.
Explanation:
def minCostConnectPoints(points):
n = len(points)
edges = []
for i in range(n):
for j in range(i + 1, n):
dist = abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1])
edges.append((dist, i, j))
edges.sort()
uf = UnionFind(n)
total_cost = 0
edges_used = 0
for cost, u, v in edges:
if uf.union(u, v):
total_cost += cost
edges_used += 1
if edges_used == n - 1:
break
return total_cost
Conclusion
Trees and graphs offer the widest variety of patterns in FAANG interviews. Key decision rules:
- BFS vs DFS: Use BFS for shortest path, DFS for all-path exploration
- Tree problems: Recursion is often elegant; prepare iterative versions too
- Graph problems: Always manage the visited set carefully
- Union Find: A powerful tool for any connectivity problem
Recommended LeetCode problems:
- Easy: Maximum Depth, Invert Tree, Symmetric Tree
- Medium: Course Schedule, Number of Islands, LCA, BFS/DFS variants
- Hard: Serialize/Deserialize, Word Ladder II, Alien Dictionary