DFS와 BFS — 그래프 탐색 알고리즘의 선택 기준·구현·코딩테스트 실전 가이드


“DFS와 BFS의 차이를 설명해 주세요.” 기술 면접과 코딩테스트에서 빠지지 않는 단골 질문입니다. 이름은 알지만 막상 언제 어느 것을 써야 하는지 헷갈리는 경우가 많습니다. DFS BFS 차이 선택 기준을 모르면 문제를 보는 순간 어떤 알고리즘으로 접근해야 할지 판단이 서지 않습니다. 이 글에서는 두 알고리즘의 작동 원리·구현 방법(Python & Java)·시간 복잡도부터, 실전 코딩테스트에서 어떤 상황에 무엇을 써야 하는지의 선택 기준, 그리고 자주 나오는 면접 질문 답변까지 한 번에 완전히 정리합니다.


목차

  1. 그래프란? — DFS·BFS를 이해하기 위한 전제 개념
  2. DFS(깊이 우선 탐색) — 원리·구현·언제 쓰는가
  3. BFS(너비 우선 탐색) — 원리·구현·언제 쓰는가
  4. DFS vs BFS 핵심 차이 완전 비교표
  5. 코딩테스트 유형별 DFS·BFS 선택 기준 — 실전 판단 공식
  6. 면접 단골 질문 Q&A와 심화 개념

1. 그래프란? — DFS·BFS를 이해하기 위한 전제 개념

그래프의 구성 요소

그래프(Graph)는 **노드(Node, 정점)**와 **간선(Edge)**으로 구성된 자료구조입니다. 소셜 네트워크에서 사람들의 친구 관계, 지도에서 도시와 도로, 웹 페이지 간의 링크 등이 모두 그래프로 표현됩니다. DFS와 BFS는 이 그래프의 모든 노드를 빠짐없이 방문하는 ‘그래프 탐색 알고리즘’입니다.

그래프 표현 방법 — 인접 행렬 vs 인접 리스트

그래프를 코드로 표현하는 방법은 두 가지입니다.

인접 행렬(Adjacency Matrix): 2차원 배열로 표현. 노드 수가 N개이면 N×N 배열을 사용합니다. graph[i][j] = 1이면 노드 i와 j가 연결됩니다.

python

# 인접 행렬 — 노드 수 적고 간선 많을 때 유리
graph = [
    [0, 1, 1, 0, 0],  # 노드 0
    [1, 0, 0, 1, 0],  # 노드 1
    [1, 0, 0, 1, 1],  # 노드 2
    [0, 1, 1, 0, 1],  # 노드 3
    [0, 0, 1, 1, 0],  # 노드 4
]

인접 리스트(Adjacency List): 각 노드에서 연결된 노드들의 리스트를 저장. 노드 수가 많고 간선이 적을 때(희소 그래프) 메모리 효율적입니다.

python

# 인접 리스트 — 코딩테스트에서 가장 많이 사용
graph = [
    [1, 2],     # 노드 0과 연결: 1, 2
    [0, 3],     # 노드 1과 연결: 0, 3
    [0, 3, 4],  # 노드 2와 연결: 0, 3, 4
    [1, 2, 4],  # 노드 3과 연결: 1, 2, 4
    [2, 3],     # 노드 4와 연결: 2, 3
]
구분인접 행렬인접 리스트
공간 복잡도O(V²)O(V + E)
특정 간선 확인O(1)O(V)
모든 간선 확인O(V²)O(V + E)
코딩테스트 활용노드 수 적을 때일반적으로 권장

2. DFS(깊이 우선 탐색) — 원리·구현·언제 쓰는가

DFS의 작동 원리

DFS(Depth First Search, 깊이 우선 탐색)는 그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘입니다. 스택 자료구조(또는 재귀함수)를 이용합니다. Judal

DFS는 길을 걸어가다가 막다른 곳에 다다를 때까지 계속 직진하고, 막히면 돌아와 다른 길로 가는 방식입니다. 미로를 탈출할 때 한 방향으로 끝까지 가보고 막히면 되돌아오는 것과 같습니다.

DFS의 탐색 단계를 요약하면 이렇습니다.

1. 시작 노드를 스택에 넣고 방문 표시
2. 스택 최상단 노드의 미방문 인접 노드가 있으면
   → 그 노드를 스택에 넣고 방문 표시
3. 미방문 인접 노드가 없으면
   → 스택에서 현재 노드를 꺼냄 (백트래킹)
4. 스택이 빌 때까지 2~3 반복

DFS 구현 — Python 재귀 방식 (가장 간결)

스택을 이용하는 알고리즘이기 때문에 재귀 함수를 이용했을 때 매우 간결하게 구현할 수 있습니다. Judal

python

# Python — DFS 재귀 구현
def dfs(graph, v, visited):
    visited[v] = True           # 현재 노드 방문 표시
    print(v, end=' ')           # 방문 순서 출력

    for neighbor in graph[v]:   # 인접 노드 순회
        if not visited[neighbor]:
            dfs(graph, neighbor, visited)

# 사용 예시
graph = [
    [],           # 0번 노드 (인덱스 맞추기)
    [2, 3, 8],    # 1번 노드와 연결: 2, 3, 8
    [1, 7],       # 2번 노드와 연결: 1, 7
    [1, 4, 5],    # 3번 노드와 연결: 1, 4, 5
    [3, 5],       # 4번 노드와 연결: 3, 5
    [3, 4],       # 5번 노드와 연결: 3, 4
    [7],          # 6번 노드와 연결: 7
    [2, 6, 8],    # 7번 노드와 연결: 2, 6, 8
    [1, 7],       # 8번 노드와 연결: 1, 7
]
visited = [False] * 9
dfs(graph, 1, visited)
# 출력: 1 2 7 6 8 3 4 5

DFS 구현 — Python 스택 방식 (반복문)

재귀 깊이 제한(Python 기본 1000)이 문제가 될 수 있는 경우 스택으로 직접 구현합니다.

python

def dfs_stack(graph, start):
    visited = set()
    stack = [start]

    while stack:
        v = stack.pop()         # 스택에서 노드 꺼냄
        if v not in visited:
            visited.add(v)
            print(v, end=' ')
            # 인접 노드를 역순으로 넣어야 원래 순서로 탐색됨
            for neighbor in reversed(graph[v]):
                if neighbor not in visited:
                    stack.append(neighbor)

DFS 구현 — Java

java

import java.util.*;

public class DFS {
    static int[] visited;
    static List<List<Integer>> graph;

    public static void dfs(int v) {
        visited[v] = 1;
        System.out.print(v + " ");

        for (int neighbor : graph.get(v)) {
            if (visited[neighbor] == 0) {
                dfs(neighbor);
            }
        }
    }

    public static void main(String[] args) {
        int n = 9; // 노드 수
        graph = new ArrayList<>();
        visited = new int[n];

        for (int i = 0; i < n; i++) {
            graph.add(new ArrayList<>());
        }

        // 간선 추가
        graph.get(1).addAll(Arrays.asList(2, 3, 8));
        graph.get(2).addAll(Arrays.asList(1, 7));
        // ... 나머지 간선 추가

        dfs(1);
    }
}

3. BFS(너비 우선 탐색) — 원리·구현·언제 쓰는가

BFS의 작동 원리

BFS(Breadth First Search, 너비 우선 탐색)는 그래프에서 가까운 노드부터 우선적으로 탐색하는 알고리즘입니다. BFS는 큐(Queue) 자료구조를 이용합니다. Judal

BFS는 연못에 돌을 던졌을 때 물결이 퍼지는 방식과 같습니다. 출발점에서 1칸 거리에 있는 모든 노드를 먼저 방문하고, 그다음 2칸 거리 노드를 방문하는 식으로 레벨(층) 단위로 탐색이 이루어집니다.

BFS의 탐색 단계를 요약하면 이렇습니다.

1. 시작 노드를 큐에 넣고 방문 표시
2. 큐에서 노드를 꺼내 해당 노드의 미방문 인접 노드를
   모두 큐에 넣고 방문 표시
3. 큐가 빌 때까지 2 반복

BFS 구현 — Python

선입선출 방식인 큐 자료구조에 기반하여 구현할 수 있습니다. deque는 데이터를 넣고 빼는 속도가 리스트 자료형에 비해 효율적이며, 대부분의 코딩테스트에서 collections 모듈 같은 기본 라이브러리 사용을 허용합니다. Judal

python

from collections import deque

def bfs(graph, start, visited):
    queue = deque([start])      # 시작 노드를 큐에 삽입
    visited[start] = True       # 방문 처리

    while queue:
        v = queue.popleft()     # 큐에서 원소 꺼냄 (선입선출)
        print(v, end=' ')

        for neighbor in graph[v]:
            if not visited[neighbor]:
                queue.append(neighbor)      # 큐에 삽입
                visited[neighbor] = True    # 방문 표시

# 사용 예시 (위와 동일한 그래프)
visited = [False] * 9
bfs(graph, 1, visited)
# 출력: 1 2 3 8 7 4 5 6

BFS 구현 — 최단 거리 반환 (핵심 응용)

BFS는 단순 탐색 외에 최단 거리를 구하는 데 가장 많이 활용됩니다.

python

from collections import deque

def bfs_shortest(graph, start, end):
    queue = deque([(start, 0)])  # (현재 노드, 현재까지 거리)
    visited = {start}

    while queue:
        v, dist = queue.popleft()

        if v == end:
            return dist          # 목적지 도달 시 최단 거리 반환

        for neighbor in graph[v]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append((neighbor, dist + 1))

    return -1  # 경로 없음

BFS 구현 — Java

java

import java.util.*;

public class BFS {
    public static void bfs(List<List<Integer>> graph, int start) {
        boolean[] visited = new boolean[graph.size()];
        Queue<Integer> queue = new LinkedList<>();

        queue.offer(start);
        visited[start] = true;

        while (!queue.isEmpty()) {
            int v = queue.poll();
            System.out.print(v + " ");

            for (int neighbor : graph.get(v)) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.offer(neighbor);
                }
            }
        }
    }
}

4. DFS vs BFS 핵심 차이 완전 비교표

알고리즘·구조 차이

비교 항목DFS (깊이 우선 탐색)BFS (너비 우선 탐색)
기반 자료구조스택 (또는 재귀)
탐색 방향한 방향으로 깊이 파고듦출발점에서 레벨(층)별로 퍼짐
구현 방식재귀 or 명시적 스택반복문 + deque
시간 복잡도O(V + E)O(V + E)
공간 복잡도O(V) — 재귀 스택 깊이O(V) — 큐 최대 크기
최단 경로 보장❌ 보장 안 됨✅ 가중치 없는 그래프에서 보장
모든 해 탐색✅ 유리 (백트래킹 결합)△ 가능하나 DFS가 더 자연스러움
메모리 효율좁고 깊은 그래프에 유리시작점에서 거리 짧을수록 유리

실제 탐색 순서 비교

아래 그래프로 탐색 순서를 비교해 봅니다.

        1
       / \
      2   3
     / \   \
    4   5   6

DFS 탐색 순서: 1 → 2 → 4 → 5 → 3 → 6 (깊이 우선)

BFS 탐색 순서: 1 → 2 → 3 → 4 → 5 → 6 (레벨 우선)


5. 코딩테스트 유형별 DFS·BFS 선택 기준 — 실전 판단 공식

가장 중요한 원칙 하나

단순히 그래프에서 모든 정점을 탐색하는 용도라면 DFS, BFS 중 편하신 것을 사용하셔도 어지간한 문제들은 맞을 수 있습니다. 단 가중치 없는 그래프의 최단 경로 문제는 BFS로만 접근해야 합니다. Dividend-nomad

이 원칙 하나만 기억해도 절반 이상의 문제는 해결됩니다.

DFS를 선택해야 하는 경우

① 경우의 수·조합·순열을 구해야 할 때 (백트래킹)

DFS는 재귀 호출 구조 덕분에 백트래킹을 자연스럽게 구현할 수 있습니다. 가능한 모든 경우를 탐색하다가 조건에 맞지 않으면 돌아오는 패턴입니다.

python

# N개 중 M개를 고르는 조합 — DFS 백트래킹
def dfs_combination(start, path, n, m, result):
    if len(path) == m:
        result.append(path[:])
        return
    for i in range(start, n + 1):
        path.append(i)
        dfs_combination(i + 1, path, n, m, result)
        path.pop()  # 백트래킹 — 선택 취소

② 모든 경로를 탐색해야 할 때

시작점에서 도착점까지의 모든 경로를 찾거나, 특정 조건을 만족하는 경로가 존재하는지 확인하는 경우입니다.

③ 그래프의 사이클 감지

DFS를 이용하면 그래프 내 사이클(순환)이 존재하는지 효율적으로 탐지할 수 있습니다.

④ 트리의 깊이, 리프 노드 관련 문제

트리 구조에서 특정 깊이까지 탐색하거나 리프(말단) 노드를 처리하는 문제는 DFS가 자연스럽습니다.

⑤ 위상 정렬

방향 그래프에서 의존 관계를 순서대로 정렬하는 위상 정렬(Topological Sort)도 DFS 기반으로 구현합니다.


BFS를 선택해야 하는 경우

① 최단 거리·최소 횟수를 구해야 할 때 (가중치 없는 그래프)

BFS의 레벨 탐색 특성 덕분에 가중치가 없는 그래프(또는 모든 간선 비용이 같은 그래프)에서 최단 경로가 보장됩니다. 이것이 가장 중요한 BFS 사용 조건입니다.

python

# 미로 탈출 최단 경로 — BFS 전형 문제
from collections import deque

def maze_shortest(maze, start, end):
    rows, cols = len(maze), len(maze[0])
    queue = deque([(start[0], start[1], 0)])  # (행, 열, 거리)
    visited = [[False] * cols for _ in range(rows)]
    visited[start[0]][start[1]] = True

    dx = [-1, 1, 0, 0]  # 상하좌우
    dy = [0, 0, -1, 1]

    while queue:
        x, y, dist = queue.popleft()

        if (x, y) == end:
            return dist

        for i in range(4):
            nx, ny = x + dx[i], y + dy[i]
            if 0 <= nx < rows and 0 <= ny < cols:
                if not visited[nx][ny] and maze[nx][ny] == 1:
                    visited[nx][ny] = True
                    queue.append((nx, ny, dist + 1))
    return -1

② 레벨(층) 단위 처리가 필요할 때

트리의 레벨 순회(Level-order traversal), “N번 이동 후 도달할 수 있는 노드 수” 같은 문제에서 BFS의 레벨 탐색 구조가 직접적으로 활용됩니다.

③ 시작점에서 가장 가까운 것을 먼저 찾을 때

여러 출발점에서 동시에 퍼져나가는 멀티 소스 BFS도 BFS의 특기입니다. 예를 들어 여러 좀비가 동시에 퍼질 때 며칠 만에 전체가 감염되는지 구하는 문제 유형입니다.

python

# 멀티 소스 BFS — 여러 출발점에서 동시에 시작
def multi_source_bfs(grid, sources):
    queue = deque()
    for r, c in sources:
        queue.append((r, c, 0))
        grid[r][c] = 1  # 방문 표시
    # 이후 일반 BFS와 동일

DFS vs BFS 선택 기준 요약표

문제 유형선택이유
최단 거리·최소 이동 횟수BFS레벨 탐색으로 최단 경로 보장
경우의 수·조합·순열DFS백트래킹 구조에 적합
연결 요소(섬) 개수 세기둘 다 가능단순 탐색, 편한 것 선택
사이클 감지DFS재귀 호출 스택 활용
트리 경로 탐색DFS자연스러운 재귀 구조
레벨별 처리BFS층 단위 탐색 구조
멀티 소스 동시 확산BFS여러 출발점 동시 처리
위상 정렬DFS방향 그래프 의존 관계

6. 면접 단골 질문 Q&A와 심화 개념

면접 기본 답변 공식

“DFS와 BFS의 차이를 설명해 주세요”에 대한 구조적 답변입니다.

"DFS와 BFS는 모두 그래프의 모든 노드를 탐색하는 알고리즘이지만
탐색 순서와 사용 자료구조가 다릅니다.

DFS(깊이 우선 탐색)는 스택 또는 재귀를 사용해
한 방향으로 끝까지 탐색한 뒤 돌아오는 방식입니다.
경우의 수 탐색, 백트래킹, 사이클 감지에 적합합니다.

BFS(너비 우선 탐색)는 큐를 사용해
출발점에서 가까운 노드부터 레벨 단위로 탐색하는 방식입니다.
가중치 없는 그래프의 최단 경로 보장이 가장 큰 장점입니다.

시간 복잡도는 둘 다 O(V + E)로 동일하며,
최단 경로가 필요하면 BFS, 모든 경우를 탐색해야 하면 DFS를 선택합니다."

Q1. 왜 BFS가 최단 경로를 보장하는가?

BFS는 출발점에서 거리 1인 모든 노드를 방문한 뒤 거리 2인 노드를 방문합니다. 어떤 노드를 처음 방문했을 때 그것이 해당 노드까지의 최단 거리임이 보장됩니다. 반면 DFS는 하나의 경로를 끝까지 따라가기 때문에 처음 도달한 경로가 최단 경로가 아닐 수 있습니다.

Q2. DFS의 재귀 방식에서 스택 오버플로가 발생하는 경우는?

Python의 기본 재귀 깊이 제한은 1000입니다. 노드 수가 수십만 개인 깊은 그래프에서 DFS를 재귀로 구현하면 RecursionError가 발생합니다.

해결 방법은 두 가지입니다.

python

# 방법 1: 재귀 깊이 제한 늘리기
import sys
sys.setrecursionlimit(100000)

# 방법 2: 재귀 대신 명시적 스택으로 구현
def dfs_iterative(graph, start):
    visited = set()
    stack = [start]
    while stack:
        v = stack.pop()
        if v not in visited:
            visited.add(v)
            for neighbor in reversed(graph[v]):
                if neighbor not in visited:
                    stack.append(neighbor)

Q3. BFS에서 deque를 써야 하는 이유는?

Python의 list에서 pop(0)(왼쪽 제거)은 O(n) 시간이 걸립니다. 모든 원소를 한 칸씩 앞으로 당겨야 하기 때문입니다. collections.dequepopleft()는 O(1)입니다. 노드 수가 많아질수록 list와 deque의 성능 차이가 극적으로 벌어집니다.

Q4. 2차원 배열(격자 맵) 탐색에서의 구현 패턴

코딩테스트에서 가장 많이 나오는 BFS/DFS 유형이 2차원 격자 맵 탐색입니다. 상하좌우 4방향 이동 패턴을 반드시 익혀두어야 합니다.

python

# 2D 격자 탐색 공통 패턴
dx = [-1, 1, 0, 0]   # 상, 하, 좌, 우 (행 방향)
dy = [0, 0, -1, 1]   # 상, 하, 좌, 우 (열 방향)

for i in range(4):
    nx = x + dx[i]
    ny = y + dy[i]
    if 0 <= nx < rows and 0 <= ny < cols:  # 경계 확인
        # 탐색 조건 처리

Q5. DFS와 백트래킹의 관계는?

백트래킹(Backtracking)은 DFS의 응용입니다. 탐색 중 현재 경로가 해답이 될 수 없다고 판단되면 이전 단계로 돌아가(백트래킹) 다른 경로를 탐색합니다. N-Queen 문제, 스도쿠 풀기, 부분집합 합 문제 등이 대표적입니다.

python

# N-Queen 백트래킹 핵심 구조
def solve(row, n, queens, result):
    if row == n:                    # 모든 퀸 배치 완료
        result.append(queens[:])
        return
    for col in range(n):
        if is_safe(queens, row, col):
            queens.append(col)      # 선택
            solve(row + 1, n, queens, result)
            queens.pop()            # 백트래킹 — 선택 취소

결론

DFS BFS 차이 선택 기준을 한 문장으로 압축하면 이렇습니다. “최단 거리가 필요하면 BFS, 모든 경우를 탐색해야 하면 DFS, 그 외 단순 탐색은 둘 다 가능하다.” 시간 복잡도는 둘 다 O(V + E)로 동일하지만, 사용 목적에 따라 선택이 갈립니다. 코딩테스트에서 문제를 보자마자 “최단 거리/최소 횟수”가 보이면 BFS, “경우의 수/모든 경로/조건 탐색”이 보이면 DFS를 먼저 떠올리는 반사적 판단 능력이 핵심입니다. 오늘 정리한 구현 코드를 직접 타이핑해보고, 백준 2606(바이러스, DFS), 2178(미로 탐색, BFS) 문제로 실전 감각을 익혀보세요.


ℹ️ 참고 안내 본 글의 코드 예시는 Python 3.10+와 Java 17 기준으로 작성되었습니다. 코딩테스트 플랫폼마다 허용 라이브러리와 언어 버전이 다를 수 있으니 제출 전 확인하시기 바랍니다. 연습 문제는 백준(acmicpc.net)의 DFS/BFS 태그, 프로그래머스의 깊이/너비 우선 탐색 카테고리를 활용하시면 됩니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다