본 시리즈는 프로그래밍 자료구조의 개념을 정리하고 실습을 진행해보는 시리즈입니다.
지난 포스팅에서 그래프의 기본 개념과 그래프를 구성하는 요소들, 그리고 그래프의 다양한 분류에 대해 다뤘습니다.
이번 포스팅에서는 프로그래밍 언어(Java
, Python
)에서의 그래프 표현 방법과 구현 방법에 대해 다루고, 그래프를 활용하는 알고리즘 예시들을 간단하게 소개하는 시간을 가져보려합니다.
그래프는 주로 인접 행렬(Adjacency Matrix)과 인접 리스트(Adjacency List)를 사용하여 표현됩니다.
인접 행렬은 그래프의 정점들 간의 연결 관계를 2차원 배열로 표현하는 방법입니다.
i
에서 정점 j
로 간선이 존재하는 경우, 배열의 값을 1
(또는 가중치가 있을 경우 가중치 값
)로 설정합니다. 간선이 없으면 값은 0
입니다.n
개의 정점이 있을 때 n x n
크기의 2차원 배열이 필요하므로, 간선이 적은 희소 그래프(Sparse Graph)에는 비효율적입니다.인접 행렬 구현 (예시: Python)
# 4개의 정점을 가진 무향 그래프를 인접 행렬로 표현
graph = [[0, 1, 1, 0], # 1번 정점과 2번, 3번이 연결
[1, 0, 0, 1], # 2번 정점과 1번, 4번이 연결
[1, 0, 0, 1], # 3번 정점과 1번, 4번이 연결
[0, 1, 1, 0]] # 4번 정점과 2번, 3번이 연결
# 간선이 있는지 확인 (1번과 2번 정점)
print(graph[0][1]) # 출력: 1 (간선 존재)
인접 행렬 구현 (예시: Java)
int[][] graph = {
{0, 1, 1, 0}, // 1번 정점과 2번, 3번이 연결
{1, 0, 0, 1}, // 2번 정점과 1번, 4번이 연결
{1, 0, 0, 1}, // 3번 정점과 1번, 4번이 연결
{0, 1, 1, 0} // 4번 정점과 2번, 3번이 연결
};
// 간선이 있는지 확인 (1번과 2번 정점)
System.out.println(graph[0][1]); // 출력: 1 (간선 존재)
인접 리스트는 그래프의 각 정점에 연결된 정점의 리스트를 사용하는 표현 방법입니다.
O(k)
(k
는 해당 정점에 연결된 간선 수)입니다.인접 리스트 구현 (예시: Python)
# 4개의 정점을 가진 무향 그래프를 인접 리스트로 표현
graph = {
1: [2, 3], # 1번 정점이 2번, 3번과 연결
2: [1, 4], # 2번 정점이 1번, 4번과 연결
3: [1, 4], # 3번 정점이 1번, 4번과 연결
4: [2, 3] # 4번 정점이 2번, 3번과 연결
}
# 간선이 있는지 확인 (1번 정점과 2번 정점)
print(2 in graph[1]) # 출력: True (간선 존재)
인접 리스트 구현 (예시: Java)
import java.util.ArrayList;
import java.util.HashMap;
public class Graph {
public static void main(String[] args) {
HashMap<Integer, ArrayList<Integer>> graph = new HashMap<>();
graph.put(1, new ArrayList<>(List.of(2, 3))); // 1번 정점이 2번, 3번과 연결
graph.put(2, new ArrayList<>(List.of(1, 4))); // 2번 정점이 1번, 4번과 연결
graph.put(3, new ArrayList<>(List.of(1, 4))); // 3번 정점이 1번, 4번과 연결
graph.put(4, new ArrayList<>(List.of(2, 3))); // 4번 정점이 2번, 3번과 연결
// 간선이 있는지 확인 (1번 정점과 2번 정점)
System.out.println(graph.get(1).contains(2)); // 출력: true (간선 존재)
}
}
표현 방법 | 장점 | 단점 | 적용 예시 |
---|---|---|---|
인접 행렬 | 간선의 존재 여부를 O(1) 시간에 확인 | 메모리 사용량이 많음 | Dense Graph |
인접 리스트 | 메모리 효율적, Sparse Graph에 적합 | 간선의 존재 여부 확인에 O(k) 시간이 소요 | Sparse Graph |
그래프의 표현 방법은 그래프의 밀도와 크기 그리고 사용 목적에 따라 선택하는 것이 중요합니다.
다만 순회(traversal)를 하는 경우, 인접 리스트가 각 정점에 연결된 정점을 탐색하는 데 상대적으로 더 효율적입니다.
인접 행렬: 모든 정점에 대해 연결 여부를 빠르게 확인할 수 있으므로, 그래프의 연결성을 확인하는 경우에 유리합니다.
인접 리스트: DFS, BFS와 같은 그래프 탐색 알고리즘을 수행할 때 메모리 사용량이 적고, 인접한 노드를 빠르게 탐색할 수 있기 때문에 이 방식이 자주 사용됩니다.
이번 섹션에서는 대표적인 프로그래밍 언어인 Java와 Python에서 그래프를 어떻게 구현할 수 있는지 살펴보겠습니다.
Java에서는 주로 HashMap
과 ArrayList
를 사용하여 인접 리스트 방식으로 그래프를 구현합니다.
HashMap
의 키로, 해당 정점에 연결된 다른 정점들의 리스트를 값으로 저장하는 방식입니다.import java.util.*;
class Graph {
private Map<String, List<String>> adjList; // 인접 리스트
// 생성자
public Graph() {
adjList = new HashMap<>();
}
// 정점 추가
public void addVertex(String vertex) {
adjList.putIfAbsent(vertex, new ArrayList<>());
}
// 간선 추가 (무향 그래프)
public void addEdge(String vertex1, String vertex2) {
adjList.get(vertex1).add(vertex2);
adjList.get(vertex2).add(vertex1);
}
// 인접 리스트 출력
public void displayGraph() {
for (String vertex : adjList.keySet()) {
System.out.print(vertex + " -> ");
System.out.println(adjList.get(vertex));
}
}
}
public class Main {
public static void main(String[] args) {
Graph graph = new Graph();
graph.addVertex("A");
graph.addVertex("B");
graph.addVertex("C");
graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("B", "C");
graph.displayGraph();
}
}
addVertex(String vertex)
메서드는 주어진 정점을 그래프에 추가합니다.addEdge(String vertex1, String vertex2)
메서드는 두 정점 간에 간선을 추가합니다. 무향 그래프의 경우 두 정점 모두 연결해주어야 합니다.displayGraph()
메서드는 그래프의 인접 리스트를 출력합니다.Python에서는 주로 딕셔너리(Dictionary)를 사용하여 인접 리스트 방식으로 그래프를 구현합니다.
class Graph:
def __init__(self):
self.graph = {} # 인접 리스트
# 정점 추가
def add_vertex(self, vertex):
if vertex not in self.graph:
self.graph[vertex] = []
# 간선 추가 (무향 그래프)
def add_edge(self, vertex1, vertex2):
if vertex1 in self.graph and vertex2 in self.graph:
self.graph[vertex1].append(vertex2)
self.graph[vertex2].append(vertex1)
# 인접 리스트 출력
def display_graph(self):
for vertex in self.graph:
print(vertex, "->", self.graph[vertex])
# 그래프 생성 및 출력
g = Graph()
g.add_vertex("A")
g.add_vertex("B")
g.add_vertex("C")
g.add_edge("A", "B")
g.add_edge("A", "C")
g.add_edge("B", "C")
g.display_graph()
add_vertex(vertex)
메서드는 주어진 정점을 그래프에 추가합니다.add_edge(vertex1, vertex2)
메서드는 두 정점 간에 간선을 추가합니다. 무향 그래프의 경우 두 정점을 모두 연결해주어야 합니다.display_graph()
메서드는 그래프의 인접 리스트를 출력합니다.이번 섹션에서는 Graph와 관련된 주요 알고리즘들 중 탐색 알고리즘과 응용 알고리즘들에 대한 간단한 소개를 진행하겠습니다.
구체적인 알고리즘 구현과 자세한 설명은 이후의 알고리즘 시리즈에서 따로 다룰 예정입니다.
그래프에서의 탐색(Search)은 중요한 문제로, 대표적으로 DFS(Depth-First Search)와 BFS(Breadth-First Search)가 많이 사용됩니다.
DFS는 한 정점에서 출발해 더 깊이 있는 정점으로 먼저 탐색을 진행하는 방법입니다.
특징: 주로 모든 경로를 탐색하거나 사이클을 찾는 문제 등에 사용됩니다.
DFS 탐색 과정:
BFS는 한 정점에서 출발해 인접한 정점들을 먼저 탐색한 후, 다음 깊이로 넘어가는 방식입니다.
특징: 최단 경로를 찾는 문제나 탐색 깊이가 얕은 경우에 주로 사용됩니다.
BFS 탐색 과정:
(Python) DFS와 BFS 간단 구현 예시
class Graph:
def __init__(self):
self.graph = {}
def add_edge(self, vertex, neighbor):
if vertex not in self.graph:
self.graph[vertex] = []
self.graph[vertex].append(neighbor)
# DFS 구현 (재귀)
def dfs(self, vertex, visited=None):
if visited is None:
visited = set()
visited.add(vertex)
print(vertex, end=' ')
for neighbor in self.graph[vertex]:
if neighbor not in visited:
self.dfs(neighbor, visited)
# BFS 구현 (큐 사용)
def bfs(self, vertex):
visited = set()
queue = [vertex]
visited.add(vertex)
while queue:
current = queue.pop(0)
print(current, end=' ')
for neighbor in self.graph[current]:
if neighbor not in visited:
queue.append(neighbor)
visited.add(neighbor)
# 그래프 생성 및 탐색 예시
g = Graph()
g.add_edge('A', 'B')
g.add_edge('A', 'C')
g.add_edge('B', 'D')
g.add_edge('C', 'E')
print("DFS 탐색: ")
g.dfs('A') # 출력: A B D C E
print("\nBFS 탐색: ")
g.bfs('A') # 출력: A B C D E
(Java) DFS와 BFS 간단 구현 예시
import java.util.*;
class Graph {
private Map<String, List<String>> adjList;
// 생성자
public Graph() {
adjList = new HashMap<>();
}
// 간선 추가
public void addEdge(String vertex, String neighbor) {
adjList.putIfAbsent(vertex, new ArrayList<>());
adjList.get(vertex).add(neighbor);
adjList.putIfAbsent(neighbor, new ArrayList<>());
adjList.get(neighbor).add(vertex); // 무향 그래프의 경우 양방향으로 간선 추가
}
// DFS 구현 (재귀)
public void dfs(String vertex, Set<String> visited) {
visited.add(vertex);
System.out.print(vertex + " ");
for (String neighbor : adjList.get(vertex)) {
if (!visited.contains(neighbor)) {
dfs(neighbor, visited);
}
}
}
// BFS 구현 (큐 사용)
public void bfs(String startVertex) {
Set<String> visited = new HashSet<>();
Queue<String> queue = new LinkedList<>();
visited.add(startVertex);
queue.add(startVertex);
while (!queue.isEmpty()) {
String vertex = queue.poll();
System.out.print(vertex + " ");
for (String neighbor : adjList.get(vertex)) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.add(neighbor);
}
}
}
}
public static void main(String[] args) {
Graph graph = new Graph();
graph.addEdge("A", "B");
graph.addEdge("A", "C");
graph.addEdge("B", "D");
graph.addEdge("C", "E");
System.out.println("DFS 탐색:");
graph.dfs("A", new HashSet<>()); // 출력: A B D C E
System.out.println("\nBFS 탐색:");
graph.bfs("A"); // 출력: A B C D E
}
}
그래프는 여러 응용 문제에서 자주 사용되며, 이를 해결하기 위한 다양한 알고리즘이 존재합니다. 여기에서는 그중 일부를 간단하게 소개합니다.
최단 경로 알고리즘:
최소 신장 트리(MST) 알고리즘:
위상 정렬(Topological Sorting):
사이클 탐지(Cycle Detection):
위의 알고리즘에 대한 구체적인 구현과 설명은 이후 알고리즘 시리즈에서 자세히 다룰 예정입니다.
이번 포스팅에서는 그래프의 다양한 표현 방법과 구현 방법을 살펴보고, 탐색 알고리즘인 DFS와 BFS에 대해서도 간단히 구현 예시를 통해 알아보았습니다. 그리고 그래프와 관련된 응용 알고리즘들에 대해서도 간략히 소개해 드렸습니다.
그래프는 다양한 실제 문제를 해결하는 데 중요한 자료구조로, 최단 경로 탐색, 네트워크 흐름 분석, 트리 구조 응용 등 광범위하게 사용됩니다. 이를 이해하고 나면, 알고리즘 문제 풀이뿐만 아니라 현실 세계의 복잡한 문제들을 해결하는 데 큰 도움이 될 것입니다.
다음 포스팅부터는 비선형 자료구조의 또 다른 큰 축인 Tree 구조에 대해 다뤄보려 합니다.