Theme:

그래프를 코드로 표현하는 방법이 여러 가지인데, 어떤 상황에서 어떤 방식을 선택해야 할까요?

그래프 표현의 두 가지 방법

그래프는 정점(Vertex)과 간선(Edge)의 집합입니다. 이를 코드로 표현하는 대표적인 방법이 인접 행렬 과 인접 리스트 입니다.

PLAINTEXT
그래프 예시:
  0 --- 1
  |   / |
  |  /  |
  2 --- 3

인접 행렬 (Adjacency Matrix)

2차원 배열로 표현합니다. matrix[i][j] = 1이면 정점 i와 j 사이에 간선이 있습니다.

JAVA
// 위 그래프의 인접 행렬
int[][] matrix = {
    //  0  1  2  3
    {0, 1, 1, 0},  // 0번 정점
    {1, 0, 1, 1},  // 1번 정점
    {1, 1, 0, 1},  // 2번 정점
    {0, 1, 1, 0}   // 3번 정점
};

구현

JAVA
public class AdjacencyMatrix {
    private int[][] matrix;
    private int vertices;

    public AdjacencyMatrix(int vertices) {
        this.vertices = vertices;
        this.matrix = new int[vertices][vertices];
    }

    // 간선 추가: O(1)
    public void addEdge(int u, int v) {
        matrix[u][v] = 1;
        matrix[v][u] = 1; // 무방향 그래프
    }

    // 간선 존재 여부: O(1)
    public boolean hasEdge(int u, int v) {
        return matrix[u][v] == 1;
    }

    // u의 이웃 정점들: O(V)
    public List<Integer> neighbors(int u) {
        List<Integer> result = new ArrayList<>();
        for (int v = 0; v < vertices; v++) {
            if (matrix[u][v] == 1) {
                result.add(v);
            }
        }
        return result;
    }

    // 간선 삭제: O(1)
    public void removeEdge(int u, int v) {
        matrix[u][v] = 0;
        matrix[v][u] = 0;
    }
}

장단점

  • **장점 **: 간선 존재 여부를 O(1)에 확인, 구현 단순
  • ** 단점 **: 공간 O(V²), 희소 그래프에서 메모리 낭비 심각

인접 리스트 (Adjacency List)

각 정점마다 연결된 정점들의 리스트를 저장합니다.

JAVA
// 위 그래프의 인접 리스트
// 0: [1, 2]
// 1: [0, 2, 3]
// 2: [0, 1, 3]
// 3: [1, 2]

구현

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

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

    // 간선 추가: O(1)
    public void addEdge(int u, int v) {
        adjList.get(u).add(v);
        adjList.get(v).add(u); // 무방향 그래프
    }

    // 간선 존재 여부: O(degree(u))
    public boolean hasEdge(int u, int v) {
        return adjList.get(u).contains(v);
    }

    // u의 이웃 정점들: O(1) — 리스트 자체를 반환
    public List<Integer> neighbors(int u) {
        return adjList.get(u);
    }

    // 간선 삭제: O(degree)
    public void removeEdge(int u, int v) {
        adjList.get(u).remove(Integer.valueOf(v));
        adjList.get(v).remove(Integer.valueOf(u));
    }
}

장단점

  • ** 장점 **: 공간 O(V + E), 희소 그래프에서 효율적, 이웃 순회가 빠름
  • ** 단점 **: 간선 존재 여부 확인이 O(degree)

상세 비교

연산인접 행렬인접 리스트
공간O(V²)O(V + E)
간선 추가O(1)O(1)
간선 삭제O(1)O(degree)
간선 존재 확인O(1)O(degree)
이웃 순회O(V)O(degree)
모든 간선 순회O(V²)O(V + E)

밀집 그래프 vs 희소 그래프

  • ** 밀집 그래프 **: E ≈ V² → 인접 행렬이 적합
  • ** 희소 그래프 **: E ≪ V² → 인접 리스트가 적합

대부분의 실제 그래프(소셜 네트워크, 도로 네트워크 등)는 ** 희소 그래프 **입니다. 예를 들어 Facebook은 수십억 명의 사용자가 있지만, 각 사용자의 친구는 수백~수천 명에 불과합니다.

BFS/DFS에서의 차이

JAVA
// BFS — 인접 리스트 기반: O(V + E)
public void bfs(int start) {
    boolean[] visited = new boolean[vertices];
    Queue<Integer> queue = new LinkedList<>();
    visited[start] = true;
    queue.offer(start);

    while (!queue.isEmpty()) {
        int u = queue.poll();
        // 인접 리스트: 실제 이웃만 순회 → 효율적
        for (int v : adjList.get(u)) {
            if (!visited[v]) {
                visited[v] = true;
                queue.offer(v);
            }
        }
    }
}

인접 행렬로 BFS를 하면 매번 V개의 정점을 확인해야 하므로 O(V²)가 됩니다.

인접 집합 — 하이브리드 방식

간선 존재 확인을 O(1)로 하면서 공간도 절약하는 방법입니다.

JAVA
// 인접 집합: 각 정점의 이웃을 HashSet으로 저장
List<Set<Integer>> adjSet = new ArrayList<>();
for (int i = 0; i < vertices; i++) {
    adjSet.add(new HashSet<>());
}

// 간선 존재 확인: O(1) 평균
boolean hasEdge = adjSet.get(u).contains(v);

// 이웃 순회: O(degree)
for (int v : adjSet.get(u)) { ... }

간선 리스트 — 또 다른 표현

JAVA
// 간선을 (u, v, weight) 튜플의 리스트로 저장
List<int[]> edges = new ArrayList<>();
edges.add(new int[]{0, 1, 5});  // 0→1, 가중치 5
edges.add(new int[]{0, 2, 3});  // 0→2, 가중치 3

// Kruskal 알고리즘(MST) 등에서 유용
// 간선을 가중치 기준으로 정렬하기 편함
edges.sort(Comparator.comparingInt(e -> e[2]));

DB 관계 테이블과 인접 리스트

관계형 데이터베이스의 외래 키 관계는 인접 리스트와 구조적으로 유사합니다.

SQL
-- users 테이블 (정점)
CREATE TABLE users (
    id BIGINT PRIMARY KEY,
    name VARCHAR(100)
);

-- friendships 테이블 (간선 — 인접 리스트)
CREATE TABLE friendships (
    user_id BIGINT REFERENCES users(id),
    friend_id BIGINT REFERENCES users(id),
    PRIMARY KEY (user_id, friend_id)
);

-- 특정 사용자의 친구 목록 → 인접 리스트의 neighbors(u)
SELECT friend_id FROM friendships WHERE user_id = ?;

-- 두 사용자가 친구인지 확인 → hasEdge(u, v)
SELECT 1 FROM friendships WHERE user_id = ? AND friend_id = ?;

인덱스가 있으면 hasEdge도 O(log n)이 아니라 사실상 O(1)에 가깝습니다.

JPA에서의 그래프 관계

JAVA
@Entity
public class User {
    @Id @GeneratedValue
    private Long id;

    // 인접 리스트: 이 사용자의 친구 목록
    @ManyToMany
    @JoinTable(
        name = "friendships",
        joinColumns = @JoinColumn(name = "user_id"),
        inverseJoinColumns = @JoinColumn(name = "friend_id")
    )
    private Set<User> friends = new HashSet<>();
}

그래프 표현 선택 가이드

상황추천 표현
정점 수가 적고 간선이 많음인접 행렬
정점 수가 많고 간선이 적음** 인접 리스트**
간선 존재 여부 빈번하게 확인인접 행렬 또는 ** 인접 집합**
BFS/DFS 탐색** 인접 리스트**
MST (Kruskal)** 간선 리스트**
DB 기반 그래프인접 리스트 (관계 테이블)

정리

  • ** 인접 행렬 **은 간선 존재 확인이 O(1)이지만 공간이 O(V²)입니다. 밀집 그래프에 적합합니다.
  • ** 인접 리스트 **는 공간이 O(V+E)이고 이웃 순회가 효율적입니다. 대부분의 실제 그래프에 적합합니다.
  • BFS/DFS 같은 그래프 탐색에서는 ** 인접 리스트 **가 O(V+E)로 효율적입니다.
  • 관계형 DB의 외래 키 관계는 인접 리스트 구조와 유사합니다.
  • 상황에 따라 인접 집합, 간선 리스트 등 다른 표현도 고려할 수 있습니다.
댓글 로딩 중...