Split View: FAANG 코딩 인터뷰 완전 정복: 트리와 그래프 핵심 패턴
FAANG 코딩 인터뷰 완전 정복: 트리와 그래프 핵심 패턴
FAANG 코딩 인터뷰 완전 정복: 트리와 그래프 핵심 패턴
트리와 그래프 문제는 FAANG 인터뷰에서 배열/문자열 다음으로 출제 빈도가 높습니다. 특히 Google, Meta에서 그래프와 트리 탐색 문제를 매우 중요하게 봅니다. 이 글에서는 인터뷰 필수 패턴 5가지를 완전한 Python 코드와 함께 정복합니다.
1. Binary Tree 핵심 패턴
DFS (Depth-First Search)
DFS는 깊이 우선으로 트리를 탐색합니다. 재귀와 반복(스택) 두 가지 방식으로 구현할 수 있습니다.
트리 노드 정의
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
DFS 순회 3가지 (재귀)
# Inorder: 왼쪽 → 현재 → 오른쪽 (BST에서 정렬된 순서)
def inorder(root):
if not root:
return []
return inorder(root.left) + [root.val] + inorder(root.right)
# Preorder: 현재 → 왼쪽 → 오른쪽 (트리 복사, 직렬화)
def preorder(root):
if not root:
return []
return [root.val] + preorder(root.left) + preorder(root.right)
# Postorder: 왼쪽 → 오른쪽 → 현재 (디렉토리 크기 계산)
def postorder(root):
if not root:
return []
return postorder(root.left) + postorder(root.right) + [root.val]
DFS 반복 구현 (스택 사용)
def inorderIterative(root):
result = []
stack = []
current = root
while current or stack:
# 가장 왼쪽으로 이동
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)
# 오른쪽을 먼저 스택에 넣어야 왼쪽이 먼저 처리됨
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
# 예시
# 트리: 3
# / \
# 9 20
# / \
# 15 7
# Output: [[3], [9,20], [15,7]]
문제 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 방식
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
시간 복잡도: O(n) - 모든 노드 방문 공간 복잡도: O(h) - h는 트리 높이 (최악 O(n) for skewed tree)
문제 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
# 반복 방식 (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
시간 복잡도: O(n) 공간 복잡도: O(n) for balanced tree, O(n) worst case
문제 3: Symmetric Tree (LeetCode 101)
트리가 대칭인지 확인하라.
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)
# 반복 방식
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
문제 4: Path Sum II (LeetCode 113)
루트에서 리프까지 합이 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
# 리프 노드이고 합이 맞으면 경로 추가
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)
# 백트래킹: 현재 노드 제거
current_path.pop()
dfs(root, [], targetSum)
return result
문제 5: Lowest Common Ancestor (LeetCode 236)
두 노드 p, q의 LCA(최소 공통 조상)를 찾아라.
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)
# 양쪽에서 발견되면 현재 노드가 LCA
if left and right:
return root
# 한쪽에서만 발견되면 그쪽이 LCA
return left if left else right
# 예시
# 트리: 3
# / \
# 5 1
# / \ / \
# 6 2 0 8
# / \
# 7 4
# p=5, q=1 → LCA = 3
# p=5, q=4 → LCA = 5
문제 6: Serialize and Deserialize Binary Tree (LeetCode 297) - 고난도
class Codec:
def serialize(self, root) -> str:
"""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):
"""직렬화된 문자열에서 트리 복원"""
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
시간 복잡도: O(n) - 모든 노드 방문 공간 복잡도: O(n) - 직렬화 결과 저장
2. Binary Search Tree (BST) 패턴
BST 특성
- 왼쪽 서브트리의 모든 값 < 현재 노드 값
- 오른쪽 서브트리의 모든 값 > 현재 노드 값
- Inorder 순회 시 정렬된 순서로 방문
문제 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'))
# 함정: 단순히 left.val < root.val < right.val 만 체크하는 것은 틀림
# 예시:
# 5
# / \
# 1 4
# / \
# 3 6
# root.right.left = 3 (3 < 5 이므로 BST 위반)
문제 8: Kth Smallest Element in BST (LeetCode 230)
def kthSmallest(root, k: int) -> int:
# Inorder 순회가 정렬된 순서이므로 k번째가 답
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]
# 반복 방식 (더 직관적)
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
문제 9: Convert Sorted Array to BST (LeetCode 108)
정렬된 배열을 높이 균형 BST로 변환하라.
def sortedArrayToBST(nums: list[int]):
def buildBST(left, right):
if left > right:
return None
# 중간 값을 루트로 선택 (균형 보장)
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 패턴
그래프 표현
# 인접 리스트 (권장)
graph = {
0: [1, 2],
1: [0, 3],
2: [0, 4],
3: [1],
4: [2]
}
# 인접 행렬
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]
]
방문 처리
# DFS (재귀)
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
print(start, end=' ')
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
# BFS
def bfs(graph, start):
visited = set([start])
queue = deque([start])
while queue:
node = queue.popleft()
print(node, end=' ')
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
문제 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'
# 4방향 탐색
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
# 예시
# Input: grid = [
# ["1","1","0","0","0"],
# ["1","1","0","0","0"],
# ["0","0","1","0","0"],
# ["0","0","0","1","1"]
# ]
# Output: 3
시간 복잡도: O(mn) 공간 복잡도: O(mn) - 재귀 스택
문제 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 = {} # 원본 노드 → 복제 노드 매핑
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)
시간 복잡도: O(V + E) - 모든 정점과 간선 방문 공간 복잡도: O(V) - 복제 매핑 저장
문제 12: Course Schedule (LeetCode 207) - 위상 정렬
선수과목 관계가 주어질 때 모든 과목을 수강할 수 있는지 판별하라.
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
# 진입 차수가 0인 노드에서 시작 (선수과목 없는 과목)
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로 사이클 감지
def canFinishDFS(numCourses: int, prerequisites: list[list[int]]) -> bool:
graph = defaultdict(list)
for course, prereq in prerequisites:
graph[prereq].append(course)
# 상태: 0=미방문, 1=방문중, 2=완료
state = [0] * numCourses
def hasCycle(node):
if state[node] == 1: # 사이클 발견
return True
if state[node] == 2: # 이미 처리됨
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
문제 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]): # 역방향 탐색
visited.add((nr, nc))
queue.append((nr, nc))
return visited
# 태평양 (상단 + 좌측 경계)
pacific_starts = [(0, c) for c in range(cols)] + [(r, 0) for r in range(1, rows)]
# 대서양 (하단 + 우측 경계)
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]
문제 14: Word Ladder (LeetCode 127)
BFS로 단어 변환 최단 경로를 구하라.
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
# 예시
# beginWord = "hit", endWord = "cog"
# wordList = ["hot","dot","dog","lot","log","cog"]
# Output: 5 (hit→hot→dot→dog→cog)
시간 복잡도: O(M^2 _ N) - M은 단어 길이, N은 단어 수 공간 복잡도: O(M^2 _ N)
4. Union Find (Disjoint Set) 패턴
구현
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 # 이미 같은 집합
# 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)
문제 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 # 두 컴포넌트가 합쳐지면 감소
return components
문제 16: Redundant Connection (LeetCode 684)
트리에 하나의 중복 간선이 추가되었을 때, 그 간선을 찾아라.
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] # 이미 연결된 경우 → 중복 간선
return []
문제 17: Accounts Merge (LeetCode 721)
def accountsMerge(accounts: list[list[str]]) -> list[list[str]]:
email_to_id = {} # 이메일 → ID
email_to_name = {} # 이메일 → 이름
# 모든 이메일에 고유 ID 부여
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))
# 같은 계정의 이메일들을 Union
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 알고리즘
Dijkstra's Algorithm
음수 가중치가 없는 그래프에서 단일 출발점 최단 경로를 구합니다.
import heapq
from collections import defaultdict
def dijkstra(graph: dict, start: int) -> dict:
distances = {node: float('inf') for node in graph}
distances[start] = 0
priority_queue = [(0, start)] # (거리, 노드)
while priority_queue:
current_dist, current_node = heapq.heappop(priority_queue)
# 더 짧은 경로가 이미 있으면 스킵
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(priority_queue, (distance, neighbor))
return distances
# 예시
graph = {
0: [(1, 4), (2, 1)],
1: [(3, 1)],
2: [(1, 2), (3, 5)],
3: []
}
# distances from 0: {0: 0, 1: 3, 2: 1, 3: 4}
시간 복잡도: O((V + E) log V) 공간 복잡도: O(V)
문제 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
# 예시
# times = [[2,1,1],[2,3,1],[3,4,1]], n=4, k=2
# Output: 2
문제 19: Cheapest Flights Within K Stops (LeetCode 787)
Bellman-Ford 변형으로 k번 이하 경유지로 최저 비용을 구합니다.
def findCheapestPrice(n: int, flights: list[list[int]], src: int, dst: int, k: int) -> int:
# Bellman-Ford: k+1번 릴랙세이션
prices = [float('inf')] * n
prices[src] = 0
for i in range(k + 1):
temp = prices.copy() # 현재 단계 결과를 분리
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
# 예시
# 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 기업별 트리/그래프 출제 경향
Google은 그래프 문제를 매우 자주 출제합니다.
- 자주 출제: 그래프 탐색, 위상 정렬, 최단 경로
- 핵심: 효율적인 알고리즘 선택과 복잡도 분석
- 예시 문제: Course Schedule II, Word Ladder, Alien Dictionary
Meta (Facebook)
Meta는 Tree Traversal과 LCA 문제를 선호합니다.
- 자주 출제: Binary Tree, LCA, Serialize/Deserialize
- 핵심: 재귀와 반복 두 방식 모두 구현 가능
- 예시 문제: Binary Tree Right Side View, Vertical Order Traversal
Amazon
Amazon은 BFS/최단 경로 문제를 많이 출제합니다.
- 자주 출제: Number of Islands, BFS 변형, Union Find
- 핵심: 실용적이고 효율적인 해법
- 예시 문제: Rotting Oranges, Find if Path Exists in Graph
7. 연습 문제 퀴즈
퀴즈 1: 이진 트리의 지름 (Diameter)
이진 트리에서 임의의 두 노드 사이의 최장 경로 길이(간선 수)를 구하라. 경로가 루트를 통과하지 않아도 됩니다.
정답: DFS로 각 노드에서 왼쪽/오른쪽 최대 깊이의 합을 계산하고 전역 최대값을 업데이트합니다.
설명:
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)
# 현재 노드를 지나는 경로의 지름
max_diameter[0] = max(max_diameter[0], left_depth + right_depth)
return 1 + max(left_depth, right_depth)
depth(root)
return max_diameter[0]
퀴즈 2: 그래프에서 사이클 감지
방향 그래프 graph = {0: [1], 1: [2], 2: [0], 3: [4], 4: []}에서 사이클이 있는가?
정답: Yes (0 → 1 → 2 → 0)
설명: DFS와 색상 마킹(0=미방문, 1=방문중, 2=완료)을 사용합니다. 방문 중인 노드를 다시 만나면 사이클입니다.
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)
퀴즈 3: BST에서 두 노드 값의 합 찾기
BST와 타겟 k가 주어질 때 두 노드 값의 합이 k가 되는 경우가 있는지 판별하라.
정답: Inorder 순회로 정렬된 값들을 얻은 후 Two Pointers를 적용합니다.
설명:
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
퀴즈 4: 섬의 최대 넓이
이진 그리드에서 1로 연결된 섬 중 가장 큰 섬의 넓이를 구하라.
정답: DFS/BFS로 각 섬의 넓이를 계산하고 최대값을 반환합니다.
설명:
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 # 방문 표시
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])
퀴즈 5: 최소 스패닝 트리 - Kruskal 알고리즘
Union Find를 이용해 n개의 도시를 최소 비용으로 연결하는 알고리즘을 구현하라. (Min Cost to Connect All Points, LeetCode 1584)
정답: 모든 간선을 비용 오름차순으로 정렬 후, Union Find로 사이클 없이 간선을 추가합니다.
설명:
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
마무리
트리와 그래프는 FAANG 인터뷰에서 가장 다양한 패턴이 등장하는 분야입니다. 특히:
- DFS/BFS 선택: 최단 경로 → BFS, 모든 경로 탐색 → DFS
- 트리 문제: 재귀가 자연스러운 경우가 많음, 반복도 준비
- 그래프 문제: visited 처리를 항상 명확히 하기
- Union Find: 연결성 문제에서 강력한 도구
추천 LeetCode 목록:
- Easy: Maximum Depth, Invert Tree, Symmetric Tree
- Medium: Course Schedule, Number of Islands, LCA, BFS/DFS 변형
- Hard: Serialize/Deserialize, Word Ladder II, Alien Dictionary
FAANG Coding Interview Mastery: Trees & Graphs Core Patterns
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