그래프 탐색 - DFS와 BFS
미로를 탈출한다고 상상해보세요. 두 가지 전략이 있습니다.
- 한 길을 끝까지 가보고, 막히면 되돌아오기 → DFS (깊이 우선 탐색)
- 현재 위치에서 갈 수 있는 모든 곳을 먼저 확인하기 → BFS (너비 우선 탐색)
저는 처음 배울 때 “둘 다 결국 모든 곳을 탐색하는데 왜 두 개나 있지?”라고 생각했습니다. 하지만 실제로 사용해보니, 각자 잘하는 것이 다르다는 걸 깨달았습니다.
왜 그래프 탐색을 배워야 할까요?
1. 실무에서 정말 많이 사용
- 소셜 네트워크: “친구의 친구” 찾기
- 파일 시스템: 폴더 구조 탐색
- 웹 크롤러: 링크 따라가기
- 게임 AI: 경로 찾기
- 추천 시스템: 연관 상품 찾기
2. 많은 문제의 기초
그래프 탐색을 이해하면 다음을 배우기 쉽습니다.
- 최단 경로 (다익스트라)
- 사이클 감지
- 위상 정렬
- 연결 요소 찾기
3. 코딩 테스트 단골 주제
LeetCode Medium 문제의 30% 이상이 그래프 탐색을 사용합니다.
그래프란?
실생활 예시
소셜 네트워크:
Alice ─── Bob
│ │
│ │
Charlie ─ David
도시 간 도로:
서울 ──200km── 부산
│ │
100km 150km
│ │
대전 ──100km── 대구
그래프 표현 방법
인접 리스트 (Adjacency List) - 가장 많이 사용
const graph = {
'A': ['B', 'C'],
'B': ['A', 'D'],
'C': ['A', 'D'],
'D': ['B', 'C']
};
인접 행렬 (Adjacency Matrix)
const graph = [
[0, 1, 1, 0], // A와 연결: B, C
[1, 0, 0, 1], // B와 연결: A, D
[1, 0, 0, 1], // C와 연결: A, D
[0, 1, 1, 0] // D와 연결: B, C
];
DFS (Depth First Search) - 깊이 우선 탐색
핵심 아이디어
“한 길을 끝까지 가보고, 막히면 되돌아온다”
미로 탐색 비유:
1. 오른쪽 길로 쭉 간다
2. 막히면? 왔던 길로 돌아가서
3. 다른 길을 시도
시각화
그래프:
A
/ \
B C
| |
D E
DFS 순서:
A → B → D (막힘, 되돌아감)
→ C → E (막힘, 되돌아감)
결과: A, B, D, C, E
스택(Stack)으로 동작
스택: [A]
pop A → 방문 ✅
push B, C
스택: [B, C]
pop B → 방문 ✅
push D
스택: [D, C]
pop D → 방문 ✅
스택: [C]
pop C → 방문 ✅
push E
스택: [E]
pop E → 방문 ✅
스택: [] → 끝!
JavaScript 구현 - 재귀 버전
function dfs(graph, start, visited = new Set()) {
// 1. 현재 노드 방문
console.log(start);
visited.add(start);
// 2. 인접 노드들을 재귀적으로 탐색
for (const neighbor of graph[start] || []) {
if (!visited.has(neighbor)) {
dfs(graph, neighbor, visited);
}
}
return visited;
}
// 사용 예시
const graph = {
'A': ['B', 'C'],
'B': ['A', 'D'],
'C': ['A', 'E'],
'D': ['B'],
'E': ['C']
};
dfs(graph, 'A');
// 출력: A, B, D, C, E
JavaScript 구현 - 스택 버전
function dfsIterative(graph, start) {
const visited = new Set();
const stack = [start];
while (stack.length > 0) {
const node = stack.pop();
if (visited.has(node)) continue;
console.log(node);
visited.add(node);
// 이웃들을 스택에 추가
const neighbors = graph[node] || [];
for (let i = neighbors.length - 1; i >= 0; i--) {
if (!visited.has(neighbors[i])) {
stack.push(neighbors[i]);
}
}
}
return visited;
}
DFS 실전 활용
1. 경로 존재 여부 확인
function hasPath(graph, start, target) {
if (start === target) return true;
const visited = new Set();
const stack = [start];
while (stack.length > 0) {
const node = stack.pop();
if (node === target) return true;
if (visited.has(node)) continue;
visited.add(node);
for (const neighbor of graph[node] || []) {
stack.push(neighbor);
}
}
return false;
}
console.log(hasPath(graph, 'A', 'E')); // true
console.log(hasPath(graph, 'A', 'Z')); // false
2. 모든 경로 찾기
function findAllPaths(graph, start, target, path = [], paths = []) {
path.push(start);
if (start === target) {
paths.push([...path]);
} else {
for (const neighbor of graph[start] || []) {
if (!path.includes(neighbor)) {
findAllPaths(graph, neighbor, target, path, paths);
}
}
}
path.pop();
return paths;
}
const allPaths = findAllPaths(graph, 'A', 'E');
console.log(allPaths); // [['A', 'C', 'E']]
BFS (Breadth First Search) - 너비 우선 탐색
핵심 아이디어
“가까운 곳부터 차례대로 탐색한다”
물결 퍼지기 비유:
시작점 (0)
/ | \
(1) (1) (1) ← 거리 1
| | |
(2) (2) (2) ← 거리 2
시각화
그래프:
A
/ \
B C
| |
D E
BFS 순서:
Level 0: A
Level 1: B, C
Level 2: D, E
결과: A, B, C, D, E
큐(Queue)로 동작
큐: [A]
dequeue A → 방문 ✅
enqueue B, C
큐: [B, C]
dequeue B → 방문 ✅
enqueue D
큐: [C, D]
dequeue C → 방문 ✅
enqueue E
큐: [D, E]
dequeue D → 방문 ✅
큐: [E]
dequeue E → 방문 ✅
큐: [] → 끝!
JavaScript 구현
function bfs(graph, start) {
const visited = new Set();
const queue = [start];
visited.add(start);
while (queue.length > 0) {
const node = queue.shift(); // 큐에서 꺼냄
console.log(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push(neighbor); // 큐에 추가
}
}
}
return visited;
}
bfs(graph, 'A');
// 출력: A, B, C, D, E
BFS 실전 활용
1. 최단 경로 찾기
function shortestPath(graph, start, target) {
const visited = new Set();
const queue = [[start, [start]]]; // [노드, 경로]
visited.add(start);
while (queue.length > 0) {
const [node, path] = queue.shift();
if (node === target) {
return path; // 최단 경로 발견!
}
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push([neighbor, [...path, neighbor]]);
}
}
}
return null; // 경로 없음
}
console.log(shortestPath(graph, 'A', 'E'));
// ['A', 'C', 'E']
2. 레벨별 순회 (층별 탐색)
function levelOrder(graph, start) {
const levels = [];
const visited = new Set();
const queue = [[start, 0]]; // [노드, 레벨]
visited.add(start);
while (queue.length > 0) {
const [node, level] = queue.shift();
if (!levels[level]) {
levels[level] = [];
}
levels[level].push(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.push([neighbor, level + 1]);
}
}
}
return levels;
}
console.log(levelOrder(graph, 'A'));
// [['A'], ['B', 'C'], ['D', 'E']]
3. 소셜 네트워크 - 2촌 친구 찾기
function friendsOfFriends(graph, person, maxDegree = 2) {
const visited = new Set();
const queue = [[person, 0]];
const result = [];
visited.add(person);
while (queue.length > 0) {
const [current, degree] = queue.shift();
if (degree > 0 && degree <= maxDegree) {
result.push(current);
}
if (degree < maxDegree) {
for (const friend of graph[current] || []) {
if (!visited.has(friend)) {
visited.add(friend);
queue.push([friend, degree + 1]);
}
}
}
}
return result;
}
const socialNetwork = {
'Alice': ['Bob', 'Charlie'],
'Bob': ['Alice', 'David'],
'Charlie': ['Alice', 'Eve'],
'David': ['Bob'],
'Eve': ['Charlie']
};
console.log(friendsOfFriends(socialNetwork, 'Alice', 2));
// ['Bob', 'Charlie', 'David', 'Eve']
DFS vs BFS 비교
언제 DFS를 사용할까?
✅ DFS가 좋은 경우:
1. 모든 경로를 찾아야 할 때
// 예: 미로의 모든 탈출 경로
findAllPaths(maze, start, exit);
2. 메모리가 제한적일 때
// DFS: O(깊이) 메모리
// BFS: O(너비) 메모리
// 깊은 트리에서는 DFS가 유리
3. 경로 자체가 중요할 때
// 백트래킹 문제
// N-Queens, Sudoku 등
언제 BFS를 사용할까?
✅ BFS가 좋은 경우:
1. 최단 경로를 찾을 때
// BFS는 항상 최단 경로를 보장!
shortestPath(graph, 'A', 'Z');
2. 가까운 노드부터 확인해야 할 때
// 예: 가장 가까운 병원 찾기
findNearestHospital(map, currentLocation);
3. 레벨/거리가 중요할 때
// 예: 6촌 이내의 친구 찾기
findFriendsWithinDegree(friends, 'Alice', 6);
비교표
| 항목 | DFS | BFS |
|---|---|---|
| 자료구조 | 스택 (Stack) | 큐 (Queue) |
| 메모리 | O(h) 높이 | O(w) 너비 |
| 최단 경로 | ❌ 보장 안 됨 | ✅ 보장됨 |
| 구현 | 재귀 or 스택 | 큐 |
| 속도 | 비슷 | 비슷 |
| 사용 예 | 백트래킹, 경로 찾기 | 최단 경로, 레벨 탐색 |
실수하기 쉬운 함정
함정 1: 방문 체크 빼먹기
// ❌ 무한 루프!
function dfsWrong(graph, start) {
console.log(start);
for (const neighbor of graph[start]) {
dfsWrong(graph, neighbor); // 방문 체크 없음!
}
}
// ✅ 올바른 코드
function dfsCorrect(graph, start, visited = new Set()) {
if (visited.has(start)) return;
console.log(start);
visited.add(start);
for (const neighbor of graph[start]) {
dfsCorrect(graph, neighbor, visited);
}
}
함정 2: BFS에서 queue.pop() 사용
// ❌ 잘못된 BFS (DFS가 됨!)
while (queue.length > 0) {
const node = queue.pop(); // 스택처럼 동작!
}
// ✅ 올바른 BFS
while (queue.length > 0) {
const node = queue.shift(); // 큐처럼 동작!
}
함정 3: 방문 체크 시점
// ❌ 중복 방문 가능
function bfsWrong(graph, start) {
const visited = new Set();
const queue = [start];
while (queue.length > 0) {
const node = queue.shift();
visited.add(node); // 너무 늦음!
for (const neighbor of graph[node]) {
if (!visited.has(neighbor)) {
queue.push(neighbor);
}
}
}
}
// ✅ 큐에 넣을 때 방문 체크
function bfsCorrect(graph, start) {
const visited = new Set([start]); // 시작 시 체크
const queue = [start];
while (queue.length > 0) {
const node = queue.shift();
for (const neighbor of graph[node]) {
if (!visited.has(neighbor)) {
visited.add(neighbor); // 큐에 넣을 때 체크!
queue.push(neighbor);
}
}
}
}
연습 문제
1. 섬의 개수 세기
// 2D 그리드에서 연결된 1의 개수 세기
const grid = [
[1, 1, 0, 0, 0],
[1, 1, 0, 0, 1],
[0, 0, 0, 1, 1],
[0, 0, 0, 0, 0]
];
function countIslands(grid) {
// DFS 또는 BFS 사용
// 힌트: 각 1을 만날 때마다 연결된 모든 1을 방문 처리
}
2. 최단 거리 계산
function shortestDistance(graph, start, target) {
// BFS 사용하여 거리 반환
// 힌트: 각 노드와 함께 거리를 저장
}
3. 사이클 감지
function hasCycle(graph) {
// DFS 사용하여 사이클 존재 여부 확인
// 힌트: 현재 경로에 있는 노드를 다시 방문하면 사이클
}
참고 자료
공식 문서 및 학술 자료
- Graph Theory - Wikipedia - 그래프 이론의 기초
- DFS - Wikipedia - DFS의 수학적 정의와 역사
- BFS - Wikipedia - BFS의 이론적 배경
- Introduction to Algorithms (CLRS) - Chapter 22 Elementary Graph Algorithms
- Graph Search - CMU Lecture - 카네기 멜론 대학 강의 자료
시각화 및 학습 도구
- VisuAlgo - Graph Traversal - DFS/BFS 인터랙티브 시각화
- Graph Visualizer - 그래프 그리기 및 탐색 도구
- Algorithm Visualizer - DFS/BFS - 단계별 실행 시각화
온라인 강의
- Khan Academy - Graph Representation - 그래프 표현 방법
- MIT OpenCourseWare - Graph Search - MIT 알고리즘 강의
- William Fiset - Graph Theory - 그래프 이론 플레이리스트 (영어)
실습 문제
- LeetCode - Graph - 400+ 그래프 문제
- Number of Islands (Medium) - DFS 연습
- Binary Tree Level Order (Medium) - BFS 연습
- Clone Graph (Medium) - 그래프 순회
- HackerRank - Graph Theory - 그래프 알고리즘 문제
- Codeforces - Graph - 경쟁 프로그래밍 문제
관련 문서
- 이진 탐색 - 탐색 알고리즘의 기초
- 재귀 개념 - DFS의 재귀 구현 이해
- 난이도 가이드 - 왜 DFS/BFS가 중급인지
- 그래프 알고리즘 구현 - 전체 TypeScript 코드
응용 분야
- 최단 경로 알고리즘 - BFS의 확장
- 위상 정렬 - DFS 응용
- 연결 요소 - DFS/BFS로 그룹 찾기
- 사이클 감지 - DFS로 사이클 찾기
실무 응용 사례
- Web Crawler Design - BFS 기반 크롤러
- Social Network Analysis - 그래프 탐색 응용
- Path Finding in Games - BFS와 A* 알고리즘
- File System Traversal - DFS로 파일 탐색
도서
- Grokking Algorithms - Aditya Bhargava (그래프 챕터)
- Algorithm Design Manual - Steven Skiena (Chapter 5: Graph Traversal)
- The Algorithm Design Manual - 그래프 알고리즘 실전 예제
마치며
DFS와 BFS는 같은 목적, 다른 전략입니다.
- DFS: 깊게 파고들기 → 백트래킹, 경로 찾기
- BFS: 넓게 퍼지기 → 최단 경로, 레벨 탐색
처음에는 “언제 뭘 써야 하지?”가 헷갈리지만, 문제를 많이 풀다 보면 자연스럽게 느낌이 옵니다.
핵심은 “방문 체크”와 “자료구조 선택”입니다! 🎯
다음 단계
같은 레벨 (중급):
다음 레벨 (고급):
- 다익스트라 알고리즘 - BFS + 가중치 그래프
- 벨만-포드 - 음수 가중치 처리
- 플로이드-워셜 - 모든 쌍 최단 경로
관련 자료구조:
🚀 추천 학습 순서: 이진 탐색 → 재귀 → DFS/BFS → 다익스트라 → 동적 프로그래밍
댓글