DFS와 BFS (Depth First Search, Breadth First Search)

한눈에 보기

그래프의 모든 정점을 방문하는 DFS와 BFS의 원리와 방문 순서 차이를 스택과 큐로 설명하고, O(V + E) 복잡도, 구현 코드, 문제 유형별 선택 기준을 정리한다.

최종 업데이트 2026.09.17

DFS(깊이 우선 탐색)와 BFS(너비 우선 탐색)는 그래프의 모든 정점을 빠짐없이 한 번씩 방문하는 두 가지 방법이다. 방문할 곳을 스택에 쌓느냐 큐에 담느냐, 차이는 이것 하나뿐인데 탐색 순서와 쓰임새가 완전히 달라진다.

코딩테스트에서 가장 출제 빈도가 높은 주제다. 미로 최단 거리, 섬 개수 세기, 네트워크 연결 판정, 바이러스 확산 같은 문제가 전부 이 둘 중 하나로 풀린다. "최단"이라는 단어가 보이면 BFS라는 감각만 잡아도 절반은 간다.

두 탐색의 차이

DFS는 한 길로 갈 수 있는 데까지 파고들었다가 막히면 돌아 나와 다른 길을 본다. BFS는 가까운 곳부터 한 겹씩 넓혀 나간다.

DFS와 BFS: 같은 그래프를 DFS와 BFS로 탐색했을 때의 방문 순서를 나란히 비교한 그림
큰 원 안의 숫자가 정점 번호, 작은 배지가 방문한 순서다. DFS는 2번으로 내려가 4·5를 다 훑고 나서야 3번을 보고, BFS는 깊이가 같은 정점을 모두 방문한 뒤 다음 층으로 내려간다.

스택과 큐, 차이는 여기서 갈린다

두 알고리즘의 코드는 사실상 같다. "다음에 방문할 후보"를 담아두는 자료구조만 다르다.

DFS와 BFS: DFS가 쓰는 스택과 BFS가 쓰는 큐의 넣고 빼는 방향 차이
스택은 넣는 곳과 빼는 곳이 같아서 최근 것이 먼저 나오고(LIFO), 큐는 반대편으로 빠져나가서 먼저 온 것이 먼저 나온다(FIFO). 이 한 가지가 깊이 우선과 너비 우선을 만든다.

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되돌아오는 간선을 재귀 중에 잡아낼 수 있다
그래프가 아주 깊을 때BFSDFS는 재귀 깊이 초과 위험
답이 얕은 곳에 있을 때BFSDFS는 엉뚱한 깊은 가지를 다 파고든다

자주 하는 실수

  • 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를 쓰면 왜 안 되는가.
알고리즘 문서 전체 보기

이 개념, 내 코드에서는 어떻게 물어볼까요

데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.

공고 넣고 시작하기