Skip to content

DFS & BFS ​

DFS(깊이 우선 탐색)와 BFS(너비 우선 탐색)는 그래프의 정점을 빠짐없이 방문하는 가장 기본적인 두 가지 방법입니다.

한눈에 보기 ​

둘 다 그래프 알고리즘으로, 문제를 풀 때 아주 많이 쓰입니다. 경로를 찾는 문제에서는 상황에 맞게 DFS와 BFS 중 하나를 고릅니다.

  • 그래프: 점(정점, vertex)과 점을 잇는 선(간선, edge)으로 이루어진 구조입니다. 지하철 노선도를 떠올리면 됩니다. 역이 정점, 역 사이 선로가 간선입니다.
  • DFS: 미로에서 한 길을 골라 막다른 곳까지 쭉 들어가 보고, 막히면 되돌아와 다른 길로 가는 방식입니다.
  • BFS: 호수에 돌을 던졌을 때 물결이 퍼지듯, 가까운 곳부터 한 겹씩 넓혀 가는 방식입니다.
구분DFS (깊이 우선)BFS (너비 우선)
방문 순서한 가지(브랜치)를 끝까지 탐색한 뒤 다음 가지로시작점과 가까운(인접한) 정점부터
구현 도구스택 또는 재귀 함수큐
잘 맞는 경우모든 경로를 방문해야 할 때간선 수 기준 최단 경로(최소 이동 횟수)를 찾을 때

DFS (깊이 우선 탐색) ​

루트 노드 혹은 임의의 노드에서 출발해, 다음 브랜치로 넘어가기 전에 해당 브랜치를 모두 탐색하는 방법입니다.

스택 또는 재귀 함수로 구현합니다. 방금 들어온 길을 거꾸로 되돌아 나와야 하므로 "마지막에 넣은 것을 먼저 꺼내는" 스택이 딱 맞습니다. 재귀 함수도 내부적으로 호출 스택을 쓰므로 같은 원리입니다.

  • 모든 경로를 방문해야 할 경우에 적합합니다.

동작 과정 ​

  1. 시작 정점을 방문 표시합니다.
  2. 방문하지 않은 이웃 중 하나(보통 번호가 작은 것)로 들어갑니다.
  3. 들어간 정점에서 다시 1~2를 반복합니다.
  4. 더 갈 이웃이 없으면 직전 정점으로 되돌아가서 남은 이웃을 봅니다.
  5. 모든 정점에서 더 갈 곳이 없으면 끝납니다.

예시 그래프: 정점 4개, 간선 1-2, 1-3, 1-4, 2-4, 3-4, 시작 정점 1

단계현재 정점지나온 경로설명
1111 방문. 이웃 2, 3, 4 중 가장 작은 2로
221 → 22 방문. 이웃 1은 방문함, 4로
341 → 2 → 44 방문. 이웃 1, 2는 방문함, 3으로
431 → 2 → 4 → 33 방문. 이웃 1, 4 모두 방문함
5-되돌아감3 → 4 → 2 → 1로 되돌아가도 남은 이웃 없음, 종료

방문 순서: 1 → 2 → 4 → 3

그림으로 보기 ​

DFS 동작

그림 출처: Wikimedia Commons

Python 코드 (재귀) ​

원문 C 코드처럼 인접 행렬(graph[a][b] == 1이면 a와 b가 연결됨)을 쓰고, 번호가 작은 이웃부터 방문합니다.

python
def dfs(graph, v, visited, order):
    """재귀로 구현한 DFS. graph는 인접 행렬, 정점 번호는 1부터"""
    visited[v] = True                        # 1. 현재 정점 방문 표시
    order.append(v)
    for nxt in range(1, len(graph)):         # 2. 번호가 작은 이웃부터 확인
        if graph[v][nxt] == 1 and not visited[nxt]:
            dfs(graph, nxt, visited, order)  # 3. 그 이웃 쪽 가지를 끝까지 탐색


def build(n, edges):
    """간선 목록으로 무방향 인접 행렬 만들기"""
    graph = [[0] * (n + 1) for _ in range(n + 1)]
    for a, b in edges:
        graph[a][b] = graph[b][a] = 1
    return graph


n = 4
graph = build(n, [(1, 2), (1, 3), (1, 4), (2, 4), (3, 4)])
order = []
dfs(graph, 1, [False] * (n + 1), order)
print(*order)  # 출력: 1 2 4 3
  1. 들어오자마자 방문 표시를 해야 같은 정점을 두 번 방문하지 않습니다.
  2. 1번부터 차례로 보므로 이웃이 여러 개면 번호가 작은 쪽이 먼저입니다.
  3. 방문하지 않은 이웃을 찾으면 곧바로 그쪽으로 깊이 들어갑니다. 그 호출이 끝나야(막다른 곳까지 갔다 와야) 다음 이웃을 봅니다.

Python은 기본 재귀 깊이 제한이 약 1000입니다. 정점이 많으면 sys.setrecursionlimit(10000)처럼 늘리거나, 아래 스택 버전을 씁니다.

Python 코드 (스택) ​

python
def dfs_stack(graph, start):
    """스택으로 구현한 DFS. 재귀 버전과 같은 순서로 방문"""
    n = len(graph) - 1
    visited = [False] * (n + 1)
    order = []
    stack = [start]                          # 1. 시작 정점을 스택에 넣기
    while stack:
        v = stack.pop()                      # 2. 가장 최근에 넣은 정점 꺼내기
        if visited[v]:
            continue
        visited[v] = True
        order.append(v)
        for nxt in range(n, 0, -1):          # 3. 큰 번호부터 넣어야 작은 번호가 먼저 나옴
            if graph[v][nxt] == 1 and not visited[nxt]:
                stack.append(nxt)
    return order


print(*dfs_stack(graph, 1))  # 출력: 1 2 4 3
  1. 스택은 "나중에 넣은 것을 먼저 꺼내는" 통입니다. Python에서는 리스트의 append/pop으로 충분합니다.
  2. 꺼낸 정점이 이미 방문한 것이면 건너뛰고, 아니면 방문 표시합니다. 같은 정점이 스택에 여러 번 들어갈 수 있어서 꺼낼 때 확인합니다.
  3. 스택은 거꾸로 나오므로, 작은 번호를 먼저 방문하려면 큰 번호부터 넣습니다.

BFS (너비 우선 탐색) ​

루트 노드 또는 임의의 노드에서 출발해, 인접한 노드부터 먼저 탐색하는 방법입니다.

큐로 구현합니다. 해당 노드의 주변부터 탐색해야 하므로, "먼저 넣은 것을 먼저 꺼내는" 큐가 맞습니다. 은행 번호표처럼 먼저 온 순서대로 처리됩니다.

  • 최소 비용이 우선일 때, 즉 모든 곳을 다 탐색하는 것보다 가장 적은 이동 횟수로 도착하는 길을 찾을 때 적합합니다. BFS는 처음 도달한 순간이 곧 최단 거리(간선 수 기준)입니다. 간선마다 비용이 다르면 BFS가 아니라 다익스트라를 씁니다.

동작 과정 ​

  1. 시작 정점을 방문 표시하고 큐에 넣습니다.
  2. 큐 맨 앞의 정점을 꺼냅니다.
  3. 그 정점의 이웃 중 방문하지 않은 것을 모두 방문 표시하고 큐 뒤에 넣습니다.
  4. 큐가 빌 때까지 2~3을 반복합니다.

같은 예시 그래프, 시작 정점 1

단계꺼낸 정점새로 넣은 정점큐 상태방문 순서
시작-1[1]1
112, 3, 4[2, 3, 4]1, 2, 3, 4
22없음 (1, 4 방문함)[3, 4]1, 2, 3, 4
33없음[4]1, 2, 3, 4
44없음[]1, 2, 3, 4

방문 순서: 1 → 2 → 3 → 4 (시작점에서 거리 1인 2, 3, 4가 한꺼번에 먼저 방문됨)

그림으로 보기 ​

BFS 동작

그림 출처: Wikimedia Commons

Python 코드 ​

python
from collections import deque


def bfs(graph, start):
    """큐로 구현한 BFS. graph는 인접 행렬, 정점 번호는 1부터"""
    n = len(graph) - 1
    visited = [False] * (n + 1)
    order = [start]
    queue = deque([start])                   # 1. 시작 정점을 큐에 넣고 방문 표시
    visited[start] = True
    while queue:
        v = queue.popleft()                  # 2. 먼저 들어온 정점부터 꺼내기
        for nxt in range(1, n + 1):
            if graph[v][nxt] == 1 and not visited[nxt]:
                visited[nxt] = True          # 3. 큐에 넣는 순간 방문 표시
                order.append(nxt)
                queue.append(nxt)
    return order


print(*bfs(graph, 1))  # 출력: 1 2 3 4
  1. deque는 앞뒤에서 넣고 빼기가 빠른 Python 표준 큐입니다. 리스트의 pop(0)은 매번 나머지를 한 칸씩 당기느라 느립니다(O(n)).
  2. popleft()로 가장 먼저 들어온 정점을 꺼내므로 가까운 정점부터 처리됩니다.
  3. 꺼낼 때가 아니라 넣을 때 방문 표시를 해야, 같은 정점이 큐에 여러 번 들어가지 않습니다.

DFS와 BFS 한눈에 비교 ​

구분DFS (Depth-First Search)BFS (Breadth-First Search)
이름깊이 우선 탐색너비 우선 탐색
방식한 방향으로 끝까지 내려가 탐색가까운 노드부터 층별로 탐색
자료구조스택 (LIFO / 후입선출)큐 (FIFO / 선입선출)

예시 1 (A의 자식 B, C / B의 자식 D)

  • DFS: A → B → D → C (D까지 내려갔다가 B, A로 되돌아온 뒤 C)
  • BFS: A → B → C → D

예시 2 (A의 자식 B, C / B의 자식 D, E / C의 자식 F, G)

  • DFS: A → B → D → E → C → F → G
  • BFS: A → B → C → D → E → F → G

큐 (BFS): 먼저 들어간 것이 먼저 나옵니다. 예) A → B → C 순서로 넣으면 꺼낼 때도 A → B → C.

데이터 들어감 → [ C | B | A ] → 데이터 나감 (오른쪽 A가 맨 앞)

스택 (DFS): 가장 최근에 들어온 것이 맨 위에 있고, 가장 먼저 들어온 것이 맨 아래에 있습니다.

스택 상태 (맨 위 → 아래)꺼내는 것
[B, C]B 꺼냄 (B의 자식 D를 넣음)
[D, C]D 꺼냄
[C]C 꺼냄

방문 순서: A → B → D → C

큐(BFS)와 스택(DFS) 손글씨 노트

직접 손으로 정리한 노트: 큐(BFS)와 스택(DFS)

시간복잡도와 공간복잡도 ​

V는 정점(vertex) 수, E는 간선(edge) 수입니다. DFS와 BFS 모두 같습니다.

그래프 표현시간복잡도이유
인접 행렬O(V²)정점마다 모든 정점(V개)과의 연결 여부를 확인
인접 리스트O(V + E)정점마다 실제 연결된 이웃만 확인, 간선은 합쳐서 한두 번씩만 봄
  • 공간복잡도: 방문 표시와 스택/큐에 O(V). 그래프 저장은 인접 행렬 O(V²), 인접 리스트 O(V + E).
  • 간선이 적은 그래프라면 인접 리스트가 훨씬 빠르고 메모리도 적게 씁니다. 복잡도 표기는 시간 복잡도 글을 참고하세요.

연습문제 ​

[BOJ] DFS와 BFS: 그래프와 시작 정점이 주어지면 DFS, BFS 방문 순서를 출력하는 문제입니다. 방문할 수 있는 정점이 여러 개면 번호가 작은 것부터 방문합니다. 위 코드가 바로 이 규칙대로 동작합니다.

예) 정점 5개, 시작 정점 3, 간선 5-4, 4-3, 5-2, 1-2, 3-4, 3-1 → DFS 3 1 2 5 4, BFS 3 1 4 2 5

Mini Git에서 써 보기 ​

커밋 그래프를 탐색하는 두 가지 방법, 최단 경로를 찾는 BFS와 조상을 모으는 DFS를 다룹니다.

BFS(너비 우선 탐색) - 최단 경로 찾기 ​

PATH <c1> <c2>: 두 커밋 사이의 가장 짧은 연결(간선 수 최소)을 찾습니다. 단, 커밋-부모 연결을 무방향(양방향으로 오갈 수 있는) 간선으로 봅니다.

BFS = 시작점에서 출발해서 가까운 이웃부터 차례차례 넓혀가며 탐색. 성질: BFS는 처음 도달했을 때가 곧 최단 거리입니다.

필요한 도구:

  • 큐(queue): 다음에 방문할 곳 목록. 먼저 넣은 것부터 꺼냄 (collections 금지지만 list의 append/pop(0) 또는 인덱스 포인터로 구현 가능)
  • visited 집합: 이미 방문한 커밋 재방문 방지 (사이클 대비)

경로 복원: 큐에 넣을 때 "어디서 왔는지(came_from)"를 기록해두면, 도착점에서 시작점까지 거꾸로 따라가며 경로를 만들 수 있습니다.

동률 처리(요구사항): 최단 경로가 여러 개면 hash1->hash2->... 문자열 기준 사전순 최소 선택. 구현 팁: 이웃을 방문 예약할 때 해시 문자열 순으로 정렬하거나, 같은 거리의 후보들 중 사전순으로 앞선 것을 우선 선택하도록 처리.

DFS/집합 추적 - 조상 찾기 ​

ANCESTORS <hash>: 특정 커밋에서 도달 가능한 모든 부모 쪽 커밋을 전부 출력.

방법: hash에서 출발해 parents를 따라 쭉쭉 방문하면서, 방문한 적 없는 커밋마다 결과에 추가.

python
def find_ancestors(start):
    """시작 커밋의 모든 조상을 모으는 함수 (BFS/DFS 아무거나)"""
    result = []
    visited = set()
    stack = [start]               # DFS는 줄 대신 스택(또는 재귀)
    while stack:
        cur = stack.pop()
        for p in cur.parents:
            if p.hash not in visited:
                visited.add(p.hash)
                result.append(p)
                stack.append(p)   # 부모의 부모도 계속 탐색
    return result

핵심은 visited로 중복 방문 막기. 브랜치가 합쳐진 그래프에서는 같은 조상에 여러 경로로 도달하기 때문입니다.

정리 ​

개념비전공자 버전 요약
DFS한 길을 막다른 곳까지 쭉 가 보고, 막히면 되돌아와 다른 길로
BFS가까운 곳부터 넓혀가며 찾는 최단 경로 탐색
스택 / 큐스택은 나중에 넣은 것 먼저, 큐는 먼저 넣은 것 먼저 꺼냄
visited이미 간 곳에 표시해 두어 두 번 가지 않기
조상 탐색부모 줄기를 끝까지 따라가 전부 모으기

참고: developer-mac.tistory.com/64

출처: Codyssey-B1/B5-2 (학습 노트 포함)