너비 우선 탐색 (BFS)
마지막 업데이트
너비 우선 탐색은 그래프를 레벨 단위로 탐색합니다. 시작 노드에서 출발해 먼저 그 노드의 모든 인접 이웃을 방문하고, 그다음 그 이웃들의 방문하지 않은 이웃을 모두 방문하며, 이런 식으로 진행하여 - 거리가 커지는 고리 형태로 바깥쪽으로 확장합니다. 위의 재생을 누르면 시작 노드에서 한 층씩 퍼져 나가는 모습을 볼 수 있습니다.
BFS는 선입선출 큐를 사용하며, 이것이 레벨 단위 순서를 강제합니다. 홉 거리 순서로 노드에 도달하기 때문에, BFS는 가중치가 없는 그래프에서 최단 경로(가장 적은 간선)를 찾습니다. 각 노드와 간선을 한 번씩 방문하므로 O(V + E) 시간에 동작합니다.
시간 및 공간 복잡도
| 측정 항목 | 복잡도 | 비고 |
|---|---|---|
| 시간 | O(V + E) | 각 정점과 간선을 한 번씩 방문 |
| 공간 | O(V) | 큐 + 방문 집합, 최악의 경우 모든 노드 |
| 순회 | 레벨 단위 | 가장 가까운 노드부터, 고리 형태로 |
| 최단 경로 | 예 (가중치 없음) | 가장 적은 간선으로 각 노드에 도달 |
단계별 진행
| 단계 | 무슨 일이 일어나는가 |
|---|---|
| 1 | 시작 노드를 큐에 넣는다. |
| 2 | 맨 앞 노드를 큐에서 꺼내고 방문했다고 표시한다. |
| 3 | 그 노드의 각 이웃을 살펴본다. |
| 4 | 아직 방문하지 않았고 큐에도 없는 이웃을 큐에 넣는다. |
| 5 | 큐가 빌 때까지 반복한다. |
풀이 예제
노드 0에서 이 그래프를 순회하며, 여기서 0-1, 0-2, 1-3, 2-3, 2-4는 간선이다:
| 단계 | 방문 | 큐 (프런티어) |
|---|---|---|
| 시작 | {} | [0] |
0 꺼냄 | {0} | [1, 2] |
1 꺼냄 | {0, 1} | [2, 3] |
2 꺼냄 | {0, 1, 2} | [3, 4] |
3 꺼냄 | {0, 1, 2, 3} | [4] |
4 꺼냄 | {0, 1, 2, 3, 4} | [] (완료) |
BFS를 사용해야 할 때
| 사용할 때 | 피해야 할 때 |
|---|---|
| 가중치가 없는 그래프에서 최단 경로가 필요할 때 | 간선에 가중치가 있을 때 - 대신 Dijkstra를 사용 |
| 목표가 시작점 근처에 있을 가능성이 높을 때 | 그래프가 매우 넓을 때 - 큐가 거대한 프런티어를 담을 수 있음 |
| 그래프를 레벨 순서로 탐색하고 싶을 때 | 임의의 노드에 도달하기만 하면 될 때, DFS가 메모리를 덜 사용 |
| 연결 요소나 최소 홉 경로를 찾을 때 | 위상 정렬이나 사이클 탐지가 필요할 때 - DFS가 더 적합 |
Breadth-First Search 코드
Python, JavaScript, Java, C++, C로 작성된 깔끔하고 실행 가능한 Breadth-First Search 구현입니다. 언어를 선택해 코드를 복사하거나 Coddy 플레이그라운드에서 바로 열어보세요.
Python로 구현한 Breadth-First Search 코드
Python
1from collections import deque2
3
4def bfs(graph, start):5 visited = {start}6 queue = deque([start])7 order = []8 while queue:9 node = queue.popleft()10 order.append(node)11 for neighbor in graph[node]:12 if neighbor not in visited:13 visited.add(neighbor) # mark on enqueue, not dequeue14 queue.append(neighbor)15 return order16
17
18graph = {19 "A": ["B", "C"],20 "B": ["D", "E"],21 "C": ["F"],22 "D": [],23 "E": ["F"],24 "F": [],25}26
27print("BFS order:", " -> ".join(bfs(graph, "A")))JavaScript로 구현한 Breadth-First Search 코드
JavaScript
1const graph = {2 A: ["B", "C"],3 B: ["D", "E"],4 C: ["F"],5 D: [],6 E: ["F"],7 F: [],8};9
10function bfs(start) {11 const visited = new Set([start]);12 const queue = [start];13 const order = [];14 while (queue.length > 0) {15 const node = queue.shift(); // Dequeue the oldest node16 order.push(node);17 for (const next of graph[node]) {18 if (!visited.has(next)) {19 visited.add(next);20 queue.push(next);21 }22 }23 }24 return order;25}26
27console.log("BFS from A:", bfs("A").join(" -> "));Java로 구현한 Breadth-First Search 코드
Java
1import java.util.ArrayDeque;2import java.util.List;3import java.util.Queue;4
5public class Main {6 public static void main(String[] args) {7 List<List<Integer>> adj = List.of(8 List.of(1, 2), // neighbors of 09 List.of(0, 3, 4), // neighbors of 110 List.of(0, 5),11 List.of(1),12 List.of(1, 5),13 List.of(2, 4)14 );15 boolean[] visited = new boolean[adj.size()];16 Queue<Integer> queue = new ArrayDeque<>();17 visited[0] = true;18 queue.add(0);19
20 StringBuilder order = new StringBuilder("BFS from 0:");21 while (!queue.isEmpty()) {22 int node = queue.poll();23 order.append(" ").append(node);24 // Mark neighbors when enqueued so nothing enters twice25 for (int next : adj.get(node)) {26 if (!visited[next]) {27 visited[next] = true;28 queue.add(next);29 }30 }31 }32 System.out.println(order);33 }34}C++로 구현한 Breadth-First Search 코드
C++
1#include <iostream>2#include <queue>3#include <vector>4
5void bfs(int start, const std::vector<std::vector<int>>& adj) {6 std::vector<bool> visited(adj.size(), false);7 std::queue<int> frontier;8 visited[start] = true;9 frontier.push(start);10 // Visit nodes level by level11 while (!frontier.empty()) {12 int node = frontier.front();13 frontier.pop();14 std::cout << node << " ";15 for (int next : adj[node]) {16 if (!visited[next]) {17 visited[next] = true;18 frontier.push(next);19 }20 }21 }22}23
24int main() {25 // 6-node undirected graph as an adjacency list26 std::vector<std::vector<int>> adj = {27 {1, 2}, // 028 {0, 3, 4}, // 129 {0, 5}, // 230 {1}, // 331 {1, 5}, // 432 {2, 4}, // 533 };34 std::cout << "BFS from node 0: ";35 bfs(0, adj);36 std::cout << "\n";37 return 0;38}C로 구현한 Breadth-First Search 코드
C
1#include <stdbool.h>2#include <stdio.h>3
4#define N 65
6void bfs(int start, const int adj[N][N]) {7 bool visited[N] = {false};8 int queue[N]; // fixed-size queue: head chases tail9 int head = 0, tail = 0;10 visited[start] = true;11 queue[tail++] = start;12 // Visit nodes level by level13 while (head < tail) {14 int node = queue[head++];15 printf("%d ", node);16 for (int next = 0; next < N; next++) {17 if (adj[node][next] && !visited[next]) {18 visited[next] = true;19 queue[tail++] = next;20 }21 }22 }23}24
25int main(void) {26 // 6-node undirected graph as an adjacency matrix27 int adj[N][N] = {28 {0, 1, 1, 0, 0, 0},29 {1, 0, 0, 1, 1, 0},30 {1, 0, 0, 0, 0, 1},31 {0, 1, 0, 0, 0, 0},32 {0, 1, 0, 0, 0, 1},33 {0, 0, 1, 0, 1, 0},34 };35 printf("BFS from node 0: ");36 bfs(0, adj);37 printf("\n");38 return 0;39}너비 우선 탐색 자주 묻는 질문
BFS의 시간 복잡도는 얼마인가요?
BFS는
O(V + E) 시간에 동작하며, 여기서 V는 정점의 수, E는 간선의 수입니다. 각 정점을 한 번 방문하고 각 간선을 한 번 살펴보기 때문입니다. 큐와 방문 집합을 위해 O(V)의 공간을 사용합니다.BFS는 최단 경로를 찾나요?
예, 가중치가 없는 그래프에서 찾습니다. BFS는 시작점으로부터 홉 거리 순서로 노드에 도달하므로, 어떤 노드에 처음 도달하는 것은 간선 수가 가장 적은 경로를 통해서입니다. 가중치가 있는 그래프에서는 대신 Dijkstra 알고리즘이 필요합니다.
BFS와 DFS의 차이는 무엇인가요?
BFS는 큐를 사용해 레벨 단위(가까운 것부터)로 탐색하고, DFS는 스택을 사용해 한 가지를 따라 깊이 들어간 뒤 되돌아옵니다. BFS는 가중치 없는 최단 경로를 찾고, DFS는 넓은 그래프에서 메모리를 덜 사용하며 사이클 탐지와 위상 정렬에 적합합니다.
Dijkstra 알고리즘 대신 BFS를 언제 사용해야 하나요?
모든 간선의 비용이 같을 때 BFS를 사용하세요. 우선순위 큐 없이
O(V + E) 시간에 간선 수가 가장 적은 경로를 찾기 때문입니다. 간선에 서로 다른 가중치가 있을 때는 Dijkstra 알고리즘이 필요하며, 가중치 그래프에 순수한 BFS를 실행하면 최소 비용이 아니라 최소 홉 경로가 나옵니다.BFS에는 왜 방문 집합이 필요한가요?
그래프에는 사이클이 있을 수 있어서, 방문 집합이 없으면 BFS가 같은 노드를 반복해서 큐에 넣어 무한히 반복하게 됩니다. 노드를 처음 큐에 넣을 때(꺼낼 때가 아니라) 표시하면, 같은 노드가 서로 다른 이웃에 의해 큐에 두 번 추가되는 것도 방지됩니다.
노드를 큐에 넣을 때와 꺼낼 때 중 언제 방문으로 표시해야 하나요?
큐에 넣을 때 표시하세요. 꺼낼 때까지 기다리면, 처리되기 전에 같은 노드가 서로 다른 이웃에 의해 큐에 여러 번 추가되어 메모리와 시간을 낭비합니다. 넣을 때 표시하면 각 노드가 정확히 한 번만 큐에 들어가는 것이 보장됩니다.