인접 행렬 vs 인접 리스트 — 그래프 표현의 트레이드오프
그래프를 코드로 표현하는 방법이 여러 가지인데, 어떤 상황에서 어떤 방식을 선택해야 할까요?
그래프 표현의 두 가지 방법
그래프는 정점(Vertex)과 간선(Edge)의 집합입니다. 이를 코드로 표현하는 대표적인 방법이 인접 행렬 과 인접 리스트 입니다.
그래프 예시:
0 --- 1
| / |
| / |
2 --- 3
인접 행렬 (Adjacency Matrix)
2차원 배열로 표현합니다. matrix[i][j] = 1이면 정점 i와 j 사이에 간선이 있습니다.
// 위 그래프의 인접 행렬
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번 정점
};
구현
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)
각 정점마다 연결된 정점들의 리스트를 저장합니다.
// 위 그래프의 인접 리스트
// 0: [1, 2]
// 1: [0, 2, 3]
// 2: [0, 1, 3]
// 3: [1, 2]
구현
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에서의 차이
// 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)로 하면서 공간도 절약하는 방법입니다.
// 인접 집합: 각 정점의 이웃을 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)) { ... }
간선 리스트 — 또 다른 표현
// 간선을 (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 관계 테이블과 인접 리스트
관계형 데이터베이스의 외래 키 관계는 인접 리스트와 구조적으로 유사합니다.
-- 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에서의 그래프 관계
@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의 외래 키 관계는 인접 리스트 구조와 유사합니다.
- 상황에 따라 인접 집합, 간선 리스트 등 다른 표현도 고려할 수 있습니다.
댓글 로딩 중...