- Authors

- Name
- Youngju Kim
- @fjvbn20031
FAANG Coding Interview Mastery: Backtracking, Heap, and Advanced Patterns
This guide covers backtracking, heap/priority queue, monotonic stack, Trie, and bit manipulation patterns that appear frequently in FAANG interviews.
1. Backtracking Patterns
The Backtracking Template
Backtracking explores a decision tree to find all solutions satisfying a constraint.
def backtrack(state, choices):
# Base case: found a valid solution
if is_solution(state):
result.append(state[:]) # Save a copy, not a reference
return
for choice in choices:
# Choose
make_choice(state, choice)
# Explore (recurse)
backtrack(state, remaining_choices)
# Unchoose (undo — this is the key!)
undo_choice(state, choice)
Core Principles:
- Always save a copy of state to results (not a reference)
- State must be identical before and after each recursive call
- Use pruning to eliminate unnecessary branches early
Subsets (LeetCode 78)
def subsets(nums: list) -> list:
result = []
def backtrack(start, current):
result.append(current[:]) # Save current subset
for i in range(start, len(nums)):
current.append(nums[i]) # Choose
backtrack(i + 1, current) # Explore
current.pop() # Unchoose
backtrack(0, [])
return result
- Time: O(n * 2^n)
- Space: O(n) (call stack)
Subsets II — Handling Duplicates (LeetCode 90)
def subsetsWithDup(nums: list) -> list:
nums.sort() # Sort for duplicate detection
result = []
def backtrack(start, current):
result.append(current[:])
for i in range(start, len(nums)):
# Skip duplicates at the same recursion level
if i > start and nums[i] == nums[i-1]:
continue
current.append(nums[i])
backtrack(i + 1, current)
current.pop()
backtrack(0, [])
return result
Permutations (LeetCode 46)
def permute(nums: list) -> list:
result = []
def backtrack(current, remaining):
if not remaining:
result.append(current[:])
return
for i in range(len(remaining)):
current.append(remaining[i])
backtrack(current, remaining[:i] + remaining[i+1:])
current.pop()
backtrack([], nums)
return result
# Visited array approach (more efficient)
def permute_v2(nums: list) -> list:
result = []
visited = [False] * len(nums)
def backtrack(current):
if len(current) == len(nums):
result.append(current[:])
return
for i in range(len(nums)):
if visited[i]:
continue
visited[i] = True
current.append(nums[i])
backtrack(current)
current.pop()
visited[i] = False
backtrack([])
return result
Permutations II — Handling Duplicates (LeetCode 47)
def permuteUnique(nums: list) -> list:
nums.sort()
result = []
visited = [False] * len(nums)
def backtrack(current):
if len(current) == len(nums):
result.append(current[:])
return
for i in range(len(nums)):
if visited[i]:
continue
# Skip if same value and previous element not yet visited
if i > 0 and nums[i] == nums[i-1] and not visited[i-1]:
continue
visited[i] = True
current.append(nums[i])
backtrack(current)
current.pop()
visited[i] = False
backtrack([])
return result
Combination Sum (LeetCode 39)
def combinationSum(candidates: list, target: int) -> list:
result = []
def backtrack(start, current, remaining):
if remaining == 0:
result.append(current[:])
return
if remaining < 0:
return
for i in range(start, len(candidates)):
current.append(candidates[i])
backtrack(i, current, remaining - candidates[i]) # Can reuse same element
current.pop()
backtrack(0, [], target)
return result
Combination Sum II (LeetCode 40)
def combinationSum2(candidates: list, target: int) -> list:
candidates.sort()
result = []
def backtrack(start, current, remaining):
if remaining == 0:
result.append(current[:])
return
for i in range(start, len(candidates)):
if candidates[i] > remaining:
break
if i > start and candidates[i] == candidates[i-1]:
continue
current.append(candidates[i])
backtrack(i + 1, current, remaining - candidates[i])
current.pop()
backtrack(0, [], target)
return result
N-Queens (LeetCode 51)
def solveNQueens(n: int) -> list:
result = []
cols = set()
diag1 = set() # row - col (top-right to bottom-left)
diag2 = set() # row + col (top-left to bottom-right)
def backtrack(row, board):
if row == n:
result.append([''.join(r) for r in board])
return
for col in range(n):
if col in cols or (row - col) in diag1 or (row + col) in diag2:
continue
cols.add(col)
diag1.add(row - col)
diag2.add(row + col)
board[row][col] = 'Q'
backtrack(row + 1, board)
cols.remove(col)
diag1.remove(row - col)
diag2.remove(row + col)
board[row][col] = '.'
board = [['.' for _ in range(n)] for _ in range(n)]
backtrack(0, board)
return result
Word Search (LeetCode 79)
def exist(board: list, word: str) -> bool:
rows, cols = len(board), len(board[0])
def backtrack(r, c, idx):
if idx == len(word):
return True
if r < 0 or r >= rows or c < 0 or c >= cols:
return False
if board[r][c] != word[idx]:
return False
temp = board[r][c]
board[r][c] = '#' # Mark as visited
found = (
backtrack(r+1, c, idx+1) or
backtrack(r-1, c, idx+1) or
backtrack(r, c+1, idx+1) or
backtrack(r, c-1, idx+1)
)
board[r][c] = temp # Restore
return found
for r in range(rows):
for c in range(cols):
if backtrack(r, c, 0):
return True
return False
2. Heap / Priority Queue Patterns
Python heapq Complete Guide
import heapq
# Min-heap (default)
min_heap = []
heapq.heappush(min_heap, 3)
heapq.heappush(min_heap, 1)
heapq.heappush(min_heap, 2)
print(heapq.heappop(min_heap)) # 1 (minimum)
# Max-heap (negate values)
max_heap = []
heapq.heappush(max_heap, -3)
heapq.heappush(max_heap, -1)
heapq.heappush(max_heap, -2)
print(-heapq.heappop(max_heap)) # 3 (maximum)
# Build heap from list
nums = [3, 1, 4, 1, 5, 9]
heapq.heapify(nums) # O(n)
# n largest / n smallest
heapq.nlargest(3, nums) # Top 3 largest
heapq.nsmallest(3, nums) # Top 3 smallest
# Tuple heap: (priority, value)
heap = [(1, 'task_a'), (3, 'task_b'), (2, 'task_c')]
heapq.heapify(heap)
priority, task = heapq.heappop(heap) # (1, 'task_a')
Kth Largest Element (LeetCode 215)
def findKthLargest(nums: list, k: int) -> int:
# Maintain a min-heap of size k
min_heap = []
for num in nums:
heapq.heappush(min_heap, num)
if len(min_heap) > k:
heapq.heappop(min_heap)
return min_heap[0] # kth largest
# QuickSelect O(n) average
import random
def findKthLargest_quickselect(nums: list, k: int) -> int:
def quickselect(left, right, k_smallest):
pivot = nums[random.randint(left, right)]
lo, hi, mid = left, right, left
while mid <= hi:
if nums[mid] < pivot:
nums[lo], nums[mid] = nums[mid], nums[lo]
lo += 1
mid += 1
elif nums[mid] > pivot:
nums[hi], nums[mid] = nums[mid], nums[hi]
hi -= 1
else:
mid += 1
if k_smallest < lo:
return quickselect(left, lo - 1, k_smallest)
elif k_smallest >= mid:
return quickselect(mid, right, k_smallest)
else:
return nums[lo]
return quickselect(0, len(nums) - 1, len(nums) - k)
Top K Frequent Elements (LeetCode 347)
from collections import Counter
def topKFrequent(nums: list, k: int) -> list:
count = Counter(nums)
# Maintain a min-heap of size k using (freq, num) pairs
heap = []
for num, freq in count.items():
heapq.heappush(heap, (freq, num))
if len(heap) > k:
heapq.heappop(heap)
return [num for freq, num in heap]
# Bucket sort O(n)
def topKFrequent_bucket(nums: list, k: int) -> list:
count = Counter(nums)
buckets = [[] for _ in range(len(nums) + 1)]
for num, freq in count.items():
buckets[freq].append(num)
result = []
for i in range(len(buckets) - 1, -1, -1):
result.extend(buckets[i])
if len(result) >= k:
return result[:k]
return result
Merge K Sorted Lists (LeetCode 23)
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def mergeKLists(lists: list) -> ListNode:
heap = []
# Push the first node from each list
for i, node in enumerate(lists):
if node:
heapq.heappush(heap, (node.val, i, node))
dummy = ListNode(0)
curr = dummy
while heap:
val, i, node = heapq.heappop(heap)
curr.next = node
curr = curr.next
if node.next:
heapq.heappush(heap, (node.next.val, i, node.next))
return dummy.next
- Time: O(N log k), where N = total nodes, k = number of lists
Find Median from Data Stream (LeetCode 295)
The two-heap pattern is a key interview technique.
class MedianFinder:
def __init__(self):
self.small = [] # Max-heap (lower half, stored as negatives)
self.large = [] # Min-heap (upper half)
def addNum(self, num: int) -> None:
# Add to small heap, then rebalance
heapq.heappush(self.small, -num)
# If max of small > min of large, move element
if self.small and self.large and (-self.small[0] > self.large[0]):
heapq.heappush(self.large, -heapq.heappop(self.small))
# Balance sizes
if len(self.small) > len(self.large) + 1:
heapq.heappush(self.large, -heapq.heappop(self.small))
if len(self.large) > len(self.small):
heapq.heappush(self.small, -heapq.heappop(self.large))
def findMedian(self) -> float:
if len(self.small) > len(self.large):
return -self.small[0]
return (-self.small[0] + self.large[0]) / 2
Task Scheduler (LeetCode 621)
from collections import Counter
def leastInterval(tasks: list, n: int) -> int:
count = Counter(tasks)
max_heap = [-c for c in count.values()]
heapq.heapify(max_heap)
time = 0
queue = [] # (count, available_time)
while max_heap or queue:
time += 1
if max_heap:
cnt = 1 + heapq.heappop(max_heap)
if cnt:
queue.append((cnt, time + n))
if queue and queue[0][1] == time:
heapq.heappush(max_heap, queue.pop(0)[0])
return time
3. Monotonic Stack Patterns
The Monotonic Stack Concept
A monotonic stack maintains elements in monotonically increasing or decreasing order.
- Monotonically increasing stack: pop when a smaller or equal element arrives (finds next smaller element)
- Monotonically decreasing stack: pop when a larger or equal element arrives (finds next greater element)
Daily Temperatures (LeetCode 739)
def dailyTemperatures(temperatures: list) -> list:
n = len(temperatures)
result = [0] * n
stack = [] # stores indices
for i, temp in enumerate(temperatures):
# Pop while current temperature is warmer
while stack and temperatures[stack[-1]] < temp:
idx = stack.pop()
result[idx] = i - idx
stack.append(i)
return result
Next Greater Element (LeetCode 496)
def nextGreaterElement(nums1: list, nums2: list) -> list:
next_greater = {}
stack = []
for num in nums2:
while stack and stack[-1] < num:
next_greater[stack.pop()] = num
stack.append(num)
# Elements remaining in stack have no next greater element
while stack:
next_greater[stack.pop()] = -1
return [next_greater[n] for n in nums1]
Largest Rectangle in Histogram (LeetCode 84)
def largestRectangleArea(heights: list) -> int:
stack = [] # Monotonically increasing stack (indices)
max_area = 0
heights = heights + [0] # Sentinel to flush the stack at the end
for i, h in enumerate(heights):
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
Trapping Rain Water — Stack Approach (LeetCode 42)
def trap(height: list) -> int:
stack = [] # Monotonically decreasing stack
water = 0
for i, h in enumerate(height):
while stack and height[stack[-1]] < h:
bottom = stack.pop()
if not stack:
break
bounded_height = min(height[stack[-1]], h) - height[bottom]
width = i - stack[-1] - 1
water += bounded_height * width
stack.append(i)
return water
4. Trie (Prefix Tree) Patterns
Trie Implementation
class TrieNode:
def __init__(self):
self.children = {} # or [None] * 26 for lowercase letters
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def search(self, word: str) -> bool:
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end
def startsWith(self, prefix: str) -> bool:
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
Design Add and Search Words (LeetCode 211)
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def search(self, word: str) -> bool:
def dfs(node, i):
if i == len(word):
return node.is_end
char = word[i]
if char == '.':
# Wildcard: explore all children
for child in node.children.values():
if dfs(child, i + 1):
return True
return False
if char not in node.children:
return False
return dfs(node.children[char], i + 1)
return dfs(self.root, 0)
Word Search II (LeetCode 212)
def findWords(board: list, words: list) -> list:
# Build Trie from word list
root = TrieNode()
for word in words:
node = root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
rows, cols = len(board), len(board[0])
result = set()
def dfs(r, c, node, path):
if node.is_end:
result.add(path)
# Continue searching for longer words
if r < 0 or r >= rows or c < 0 or c >= cols:
return
char = board[r][c]
if char not in node.children or char == '#':
return
board[r][c] = '#' # Mark visited
for dr, dc in [(0,1), (0,-1), (1,0), (-1,0)]:
dfs(r+dr, c+dc, node.children[char], path+char)
board[r][c] = char # Restore
for r in range(rows):
for c in range(cols):
dfs(r, c, root, '')
return list(result)
5. Bit Manipulation Patterns
XOR Tricks and Bit Operation Fundamentals
# Basic bit operations
a = 0b1010 # 10
b = 0b1100 # 12
print(a & b) # AND: 0b1000 = 8
print(a | b) # OR: 0b1110 = 14
print(a ^ b) # XOR: 0b0110 = 6
print(~a) # NOT: -11 (two's complement)
print(a << 1) # Left shift: 0b10100 = 20
print(a >> 1) # Right shift: 0b0101 = 5
# Useful bit tricks
n = 12
print(n & (n-1)) # Remove lowest set bit: 8
print(n & (-n)) # Isolate lowest set bit: 4
print(n ^ n) # XOR with itself = 0
print(0 ^ n) # XOR with 0 = n
Single Number (LeetCode 136)
def singleNumber(nums: list) -> int:
# XOR all numbers: pairs cancel out to 0
result = 0
for num in nums:
result ^= num
return result
Counting Bits (LeetCode 338)
def countBits(n: int) -> list:
dp = [0] * (n + 1)
for i in range(1, n + 1):
# Number of 1s in i = number of 1s in (i >> 1) + lowest bit
dp[i] = dp[i >> 1] + (i & 1)
return dp
Reverse Bits (LeetCode 190)
def reverseBits(n: int) -> int:
result = 0
for _ in range(32):
result = (result << 1) | (n & 1)
n >>= 1
return result
Number of 1 Bits — Hamming Weight (LeetCode 191)
def hammingWeight(n: int) -> int:
count = 0
while n:
n &= (n - 1) # Remove the lowest set bit
count += 1
return count
# Python built-in
def hammingWeight_v2(n: int) -> int:
return bin(n).count('1')
6. Real Interview Strategies
How to Ask for Hints When Stuck
Staying silent when stuck is the worst thing you can do. Try phrases like these:
"I'm running into [specific issue] with my current approach.
Am I missing something obvious here?"
"I'm thinking of using [specific data structure / algorithm].
Does that seem like a reasonable direction?"
Hidden Rule: Even when a hint is given, how you incorporate it matters just as much as the answer.
How to Discuss Code Optimization
When asked to optimize:
"My current solution runs in O(n^2) time and O(n) space.
I think using [specific data structure / algorithm]
could bring this down to O(n log n). Should I implement that?"
How to Explain Time and Space Complexity
Always state the reasoning, not just the answer:
"The time complexity is O(n log n).
The outer loop runs n times, and each iteration performs
a heap operation costing O(log n).
The space complexity is O(k) because
we maintain a heap of at most k elements."
The Interview Execution Framework
1. Understand the Problem (5 min)
- Create your own examples
- Ask about edge cases
2. Explain Your Approach (5 min)
- Brute force first
- Then discuss optimizations
3. Code (15-20 min)
- Talk through your logic as you code
- Use meaningful variable names
4. Test (5 min)
- Trace through your own example
- Verify edge cases
7. Practice Quiz
Quiz 1: Why does Subsets II need the condition "i > start" instead of "i > 0"?
Answer: To skip duplicates only at the same level of recursion, not across different levels.
Explanation: Using "i > 0" would skip elements that were already used in a parent call, causing valid combinations to be missed. "i > start" only skips when the same value appears twice at the current recursion depth, which is exactly the condition that produces duplicate subsets.
Quiz 2: Why do we use two heaps in Find Median from Data Stream?
Answer: To achieve O(1) median retrieval and O(log n) insertion.
Explanation: Maintaining a sorted array gives O(n) insertion. Using only one heap gives O(n) retrieval. Two balanced heaps (a max-heap for the lower half and a min-heap for the upper half) let us read the median from the tops of the heaps in O(1), while heap push and pop are O(log n).
Quiz 3: Why do we append 0 to heights in Largest Rectangle in Histogram?
Answer: To force all remaining elements to be popped from the stack after the loop ends.
Explanation: The sentinel value 0 is smaller than any valid height, so it triggers the while-loop to pop every remaining index from the stack. This eliminates the need for a separate post-loop cleanup step. You could also handle the remaining elements manually after the loop — both approaches are correct.
Quiz 4: What mathematical property of XOR makes Single Number work?
Answer: Two key XOR properties: a XOR a = 0 (any number XORed with itself is 0), and a XOR 0 = a (any number XORed with 0 is itself).
Explanation: All numbers appearing twice cancel out to 0 when XORed together. The number appearing once is XORed with 0, leaving itself. XOR is commutative and associative, so the order does not matter.
Quiz 5: Why do we use a Trie instead of checking each word individually in Word Search II?
Answer: To share common prefix traversal across multiple words and prune branches that cannot lead to any word.
Explanation: Searching for each word individually costs O(words _ cells _ 4^L) where L is word length. A Trie allows multiple words sharing a common prefix to share the same DFS path. When the current board path is not a prefix of any word in the Trie, we can prune immediately, dramatically reducing the total number of DFS calls.
Summary
Complexity Reference:
| Pattern | Time | Space |
|---|---|---|
| Backtracking Subsets | O(n * 2^n) | O(n) |
| Backtracking Permutations | O(n * n!) | O(n) |
| Heap — Kth element | O(n log k) | O(k) |
| Merge K Lists | O(N log k) | O(k) |
| Monotonic Stack | O(n) | O(n) |
| Trie Insert/Search | O(L) | O(ALPHABET _ N _ L) |
| Bit XOR trick | O(n) | O(1) |
Interview Preparation Roadmap:
- Backtracking: Subsets → Permutations → Combination Sum → N-Queens
- Heap: Kth Largest → Top K → Merge K Lists → Median Finder
- Monotonic Stack: Daily Temperatures → Largest Rectangle
- Trie: Implement Trie → Word Search II
- Bit Manipulation: Single Number → Counting Bits → Power of Two