Skip to content
Published on

FAANG Coding Interview Mastery: Trees & Graphs Core Patterns

Authors

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 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)

Google

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