목록으로

Java로 DFS, BFS 알고리즘 정리

1

개요

그래프 탐색은 코딩 테스트와 실무 모두에서 핵심적인 알고리즘입니다. 이 글에서는 DFS(깊이 우선 탐색)BFS(너비 우선 탐색) 의 개념을 정리하고, Java로 직접 구현해 봅니다.


그래프 표현

모든 예제에서 사용할 인접 리스트 기반 그래프입니다.

import java.util.*;

public class Graph {
    private int vertices;
    private List<List<Integer>> adjList;

    public Graph(int vertices) {
        this.vertices = vertices;
        this.adjList = new ArrayList<>();
        for (int i = 0; i < vertices; i++) {
            adjList.add(new ArrayList<>());
        }
    }

    public void addEdge(int src, int dest) {
        adjList.get(src).add(dest);
        adjList.get(dest).add(src); // 무방향 그래프
    }

    public List<Integer> getNeighbors(int vertex) {
        return adjList.get(vertex);
    }

    public int getVertices() {
        return vertices;
    }
}

1. DFS (깊이 우선 탐색)

개념

DFS는 한 경로를 끝까지 탐색한 뒤, 더 이상 갈 곳이 없으면 이전 분기점으로 되돌아가(backtrack) 다른 경로를 탐색하는 방식입니다.

  • 스택(Stack) 또는 재귀(Recursion)를 사용합니다.
  • 트리의 전위 순회(pre-order traversal)와 동일한 원리입니다.

재귀 구현

public class DFSRecursive {

    public static void dfs(Graph graph, int start) {
        boolean[] visited = new boolean[graph.getVertices()];
        dfsHelper(graph, start, visited);
    }

    private static void dfsHelper(Graph graph, int vertex, boolean[] visited) {
        visited[vertex] = true;
        System.out.print(vertex + " ");

        for (int neighbor : graph.getNeighbors(vertex)) {
            if (!visited[neighbor]) {
                dfsHelper(graph, neighbor, visited);
            }
        }
    }

    public static void main(String[] args) {
        Graph graph = new Graph(6);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 3);
        graph.addEdge(1, 4);
        graph.addEdge(2, 5);

        System.out.print("DFS (재귀): ");
        dfs(graph, 0);
        // 출력: 0 1 3 4 2 5
    }
}

스택 구현

재귀 대신 명시적 스택을 사용하면 스택 오버플로를 방지할 수 있습니다.

public class DFSIterative {

    public static void dfs(Graph graph, int start) {
        boolean[] visited = new boolean[graph.getVertices()];
        Deque<Integer> stack = new ArrayDeque<>();

        stack.push(start);

        System.out.print("DFS (스택): ");
        while (!stack.isEmpty()) {
            int vertex = stack.pop();

            if (visited[vertex]) {
                continue;
            }

            visited[vertex] = true;
            System.out.print(vertex + " ");

            // 인접 노드를 역순으로 push하면 재귀 버전과 같은 순서가 됩니다
            List<Integer> neighbors = graph.getNeighbors(vertex);
            for (int i = neighbors.size() - 1; i >= 0; i--) {
                int neighbor = neighbors.get(i);
                if (!visited[neighbor]) {
                    stack.push(neighbor);
                }
            }
        }
    }

    public static void main(String[] args) {
        Graph graph = new Graph(6);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 3);
        graph.addEdge(1, 4);
        graph.addEdge(2, 5);

        dfs(graph, 0);
        // 출력: 0 1 3 4 2 5
    }
}

2. BFS (너비 우선 탐색)

개념

BFS는 시작 노드에서 가까운 노드부터 레벨(level) 순서로 탐색하는 방식입니다.

  • 큐(Queue)를 사용합니다.
  • 최단 경로 문제(가중치 없는 그래프)에 적합합니다.

큐 구현

public class BFS {

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

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

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

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

    public static void main(String[] args) {
        Graph graph = new Graph(6);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 3);
        graph.addEdge(1, 4);
        graph.addEdge(2, 5);

        bfs(graph, 0);
        // 출력: 0 1 2 3 4 5
    }
}

BFS로 최단 거리 구하기

public class BFSShortestPath {

    public static int[] shortestPath(Graph graph, int start) {
        int[] distance = new int[graph.getVertices()];
        Arrays.fill(distance, -1);

        Queue<Integer> queue = new LinkedList<>();
        distance[start] = 0;
        queue.offer(start);

        while (!queue.isEmpty()) {
            int vertex = queue.poll();

            for (int neighbor : graph.getNeighbors(vertex)) {
                if (distance[neighbor] == -1) {
                    distance[neighbor] = distance[vertex] + 1;
                    queue.offer(neighbor);
                }
            }
        }

        return distance;
    }

    public static void main(String[] args) {
        Graph graph = new Graph(6);
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 3);
        graph.addEdge(1, 4);
        graph.addEdge(2, 5);

        int[] dist = shortestPath(graph, 0);
        for (int i = 0; i < dist.length; i++) {
            System.out.println("0 -> " + i + " : " + dist[i]);
        }
        // 0 -> 0 : 0
        // 0 -> 1 : 1
        // 0 -> 2 : 1
        // 0 -> 3 : 2
        // 0 -> 4 : 2
        // 0 -> 5 : 2
    }
}

3. 시간/공간 복잡도 비교

항목DFSBFS
시간 복잡도O(V + E)O(V + E)
공간 복잡도O(V) — 재귀 스택 또는 명시적 스택O(V) — 큐
최악 공간O(V) — 경로 깊이만큼O(V) — 가장 넓은 레벨만큼

V = 정점(Vertex) 수, E = 간선(Edge) 수

시간 복잡도는 동일하지만, 공간 사용 패턴이 다릅니다.

  • DFS: 깊고 좁은 그래프에서 메모리 효율적
  • BFS: 얕고 넓은 그래프에서 메모리 효율적

4. 언제 DFS vs BFS를 사용하는가?

DFS를 선택하는 경우

  • 모든 경로 탐색: 가능한 모든 경우의 수를 탐색해야 할 때 (백트래킹)
  • 사이클 탐지: 그래프에 사이클이 존재하는지 확인할 때
  • 위상 정렬: DAG에서 작업 순서를 결정할 때
  • 연결 요소 탐색: 그래프의 연결된 컴포넌트를 찾을 때
  • 미로 탈출: 한 방향으로 끝까지 가 보는 전략이 자연스러울 때

BFS를 선택하는 경우

  • 최단 경로: 가중치가 없는 그래프에서 최단 거리를 구할 때
  • 레벨별 탐색: 트리의 레벨 순서 순회가 필요할 때
  • 최소 횟수 문제: "최소 몇 번 만에 도달할 수 있는가" 유형 문제
  • 네트워크 전파: 바이러스 확산, 소셜 네트워크 친구 추천 등

요약 표

상황추천 알고리즘
최단 경로 (비가중 그래프)BFS
모든 경로 탐색 / 백트래킹DFS
사이클 탐지DFS
위상 정렬DFS
레벨별 순회BFS
연결 요소DFS 또는 BFS

마무리

DFS와 BFS는 그래프 탐색의 가장 기본이 되는 알고리즘입니다. 두 알고리즘의 동작 원리를 확실히 이해하고, 문제 유형에 따라 적절한 탐색 방법을 선택하는 것이 중요합니다. 코딩 테스트에서 자주 등장하므로 위 코드를 직접 타이핑하며 손에 익혀 두시길 권장합니다.