DFS와 BFS (Depth First Search, Breadth First Search)
한눈에 보기
그래프의 모든 정점을 방문하는 DFS와 BFS의 원리와 방문 순서 차이를 스택과 큐로 설명하고, O(V + E) 복잡도, 구현 코드, 문제 유형별 선택 기준을 정리한다.
최종 업데이트 2026.09.17
DFS(깊이 우선 탐색)와 BFS(너비 우선 탐색)는 그래프의 모든 정점을 빠짐없이 한 번씩 방문하는 두 가지 방법이다. 방문할 곳을 스택에 쌓느냐 큐에 담느냐, 차이는 이것 하나뿐인데 탐색 순서와 쓰임새가 완전히 달라진다.
코딩테스트에서 가장 출제 빈도가 높은 주제다. 미로 최단 거리, 섬 개수 세기, 네트워크 연결 판정, 바이러스 확산 같은 문제가 전부 이 둘 중 하나로 풀린다. "최단"이라는 단어가 보이면 BFS라는 감각만 잡아도 절반은 간다.
두 탐색의 차이
DFS는 한 길로 갈 수 있는 데까지 파고들었다가 막히면 돌아 나와 다른 길을 본다. BFS는 가까운 곳부터 한 겹씩 넓혀 나간다.
스택과 큐, 차이는 여기서 갈린다
두 알고리즘의 코드는 사실상 같다. "다음에 방문할 후보"를 담아두는 자료구조만 다르다.
BFS가 최단 거리를 주는 이유
BFS는 시작점에서 거리 1인 정점을 전부, 그다음 거리 2인 정점을 전부 방문한다. 그래서 어떤 정점에 처음 도착하는 순간이 곧 최단 경로로 도착한 순간이다. 나중에 다른 경로로 같은 정점에 닿더라도 그 경로가 더 짧을 수는 없다.
단, 이 성질은 모든 간선의 가중치가 같을 때만 성립한다. 간선마다 비용이 다르면 BFS는 최단 거리를 보장하지 못하고 다익스트라를 써야 한다. DFS는 어떤 경우에도 최단 거리를 주지 않는다. 운 좋게 맞을 수는 있어도 보장이 없다.
시간 복잡도
| 구분 | 인접 리스트 | 인접 행렬 |
|---|---|---|
| 시간 | O(V + E) | O(V²) |
| 공간 | O(V + E) | O(V²) |
DFS와 BFS의 복잡도는 같다. 모든 정점을 한 번씩, 모든 간선을 한 번씩(무방향이면 두 번) 훑기 때문이다. 정점 수에 비해 간선이 적은 희소 그래프에서는 인접 리스트가 압도적으로 유리하다. 코딩테스트에서는 거의 항상 인접 리스트를 쓴다.
추가 공간은 방문 체크 배열 O(V)에, DFS는 재귀 스택 O(V), BFS는 큐 O(V)가 붙는다.
구현 코드 예시
from collections import deque
def dfs_recursive(graph, v, visited, order):
visited[v] = True
order.append(v)
for nxt in graph[v]:
if not visited[nxt]:
dfs_recursive(graph, nxt, visited, order)
def dfs_stack(graph, start, n):
"""재귀 없이 스택으로 구현한 DFS."""
visited = [False] * (n + 1)
stack = [start]
order = []
while stack:
v = stack.pop()
if visited[v]: # 스택에는 중복이 들어올 수 있어 꺼낼 때 확인
continue
visited[v] = True
order.append(v)
for nxt in reversed(graph[v]): # 작은 번호부터 방문하려면 뒤집어 넣는다
if not visited[nxt]:
stack.append(nxt)
return order
def bfs(graph, start, n):
visited = [False] * (n + 1)
q = deque([start])
visited[start] = True # 큐에 넣는 순간 방문 표시
order = []
while q:
v = q.popleft()
order.append(v)
for nxt in graph[v]:
if not visited[nxt]:
visited[nxt] = True
q.append(nxt)
return order
최단 거리가 필요하면 방문 배열 대신 거리 배열을 쓴다. 코드가 거의 바뀌지 않는다.
def bfs_dist(graph, start, n):
dist = [-1] * (n + 1) # -1은 아직 도달하지 못했다는 뜻
dist[start] = 0
q = deque([start])
while q:
v = q.popleft()
for nxt in graph[v]:
if dist[nxt] == -1:
dist[nxt] = dist[v] + 1
q.append(nxt)
return dist
# 2차원 격자(미로) 문제의 관용구
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]
def bfs_grid(board, sr, sc):
h, w = len(board), len(board[0])
dist = [[-1] * w for _ in range(h)]
dist[sr][sc] = 0
q = deque([(sr, sc)])
while q:
r, c = q.popleft()
for d in range(4):
nr, nc = r + dx[d], c + dy[d]
if 0 <= nr < h and 0 <= nc < w and dist[nr][nc] == -1 and board[nr][nc] == 1:
dist[nr][nc] = dist[r][c] + 1
q.append((nr, nc))
return dist
무엇을 언제 쓰는가
| 상황 | 선택 | 이유 |
|---|---|---|
| 최단 거리 / 최소 이동 횟수 | BFS | 가까운 곳부터 방문하므로 첫 도달이 최단 |
| 모든 경로 탐색, 조합 생성 | DFS | 백트래킹과 자연스럽게 맞물린다 |
| 연결 요소 개수, 섬 세기 | 둘 다 | 전부 방문만 하면 되므로 아무거나 |
| 사이클 판정, 위상 정렬 | DFS | 되돌아오는 간선을 재귀 중에 잡아낼 수 있다 |
| 그래프가 아주 깊을 때 | BFS | DFS는 재귀 깊이 초과 위험 |
| 답이 얕은 곳에 있을 때 | BFS | DFS는 엉뚱한 깊은 가지를 다 파고든다 |
자주 하는 실수
- BFS에서 큐를 리스트로 만든다.
list.pop(0)은 O(n)이라 전체가 O(V²)로 느려진다. 반드시collections.deque를 쓴다. 이것 하나로 시간 초과가 나는 경우가 정말 많다. - BFS에서 꺼낼 때 방문 표시를 한다.
popleft()시점에 표시하면 같은 정점이 큐에 여러 번 들어가 중복 처리되고 최악에는 메모리가 터진다. 큐에 넣는 순간 표시해야 한다. - DFS 재귀 깊이를 넘긴다. 파이썬 기본 한도는 1000이다. 정점이 10만 개인 일자 그래프에서 바로
RecursionError가 난다.sys.setrecursionlimit(10**6)을 걸거나 스택 버전으로 바꾼다. - 무방향 그래프에서 간선을 한 방향만 넣는다.
graph[a].append(b)만 하고 반대쪽을 빼먹으면 절반이 탐색되지 않는다. - 격자 문제에서 경계 검사를 뒤에 한다.
board[nr][nc]를 먼저 읽고 범위를 나중에 확인하면 인덱스 오류가 난다. 파이썬은 음수 인덱스가 통해서 오류 없이 조용히 틀리기도 한다. - 시작점을 방문 표시하지 않는다. 시작 정점이 다시 큐에 들어가 무한 루프가 된다.
면접에서 이렇게 물어본다
- DFS와 BFS의 차이를 자료구조 관점에서 설명해 보라.
- BFS가 최단 거리를 보장하는 이유는 무엇이며, 그 전제 조건은 무엇인가.
- DFS를 재귀 없이 구현하려면 어떻게 하겠는가.
- BFS에서 방문 표시를 큐에 넣을 때 해야 하는 이유를 설명해 보라.
- 인접 리스트와 인접 행렬 중 어느 것을 언제 쓰겠는가.
- 정점이 매우 많고 깊은 그래프에서 DFS의 위험은 무엇인가.
- 가중치가 있는 그래프에서 BFS를 쓰면 왜 안 되는가.
알고리즘에서 같이 읽기
- 이분 탐색 (Binary Search)정렬된 배열에서 탐색 범위를 절반씩 줄여 O(log n)에 값을 찾는 이분 탐색의 원리와 동작 과정을 그림으로 정리했다. lower bound와 upper bound, 코딩테스트 단골인 파라메트릭 서치와 회전 정렬 배열, 구현 코드와 자주 하는 실수까지 다룬다.
- 비트마스크 (Bitmask)정수의 각 비트로 집합의 포함 여부를 표현하는 비트마스크 기법을 다룬다. 기본 비트 연산 정리, 부분집합 열거, 외판원 순회 동적 계획법 구현과 주의점을 정리한다.
- 다익스트라 알고리즘 (Dijkstra's Algorithm)음수 간선이 없는 그래프에서 한 정점으로부터의 최단 거리를 구하는 다익스트라 알고리즘의 그리디 원리, 우선순위 큐 구현, 복잡도 비교와 한계를 정리한다.
- 동적 계획법 (Dynamic Programming)겹치는 부분 문제와 최적 부분 구조를 전제로 부분 문제의 답을 저장해 재사용하는 동적 계획법을 다룬다. 하향식과 상향식 비교, 문제 풀이 단계, 배낭 문제 구현을 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기