그래프 예시:
A ─── B
│ │
│ │
C ─── D ─── E
정점: {A, B, C, D, E}
간선: {(A,B), (A,C), (B,D), (C,D), (D,E)}
무방향 그래프:
A ─── B
│ │
C ─── D
방향 그래프:
A ──→ B
│ │
↓ ↓
C ←── D
가중 그래프:
5 3
A ─────── B ─────── C
│ │ │
│2 │4 │1
│ │ │
D ─────── E ─────── F
7 6
장점:
단점:
무방향 그래프: 인접 행렬:
A ─── B A B C D
│ │ A [ 0 1 1 0 ]
C ─── D B [ 1 0 0 1 ]
C [ 1 0 0 1 ]
D [ 0 1 1 0 ]
가중 그래프: 인접 행렬:
A ─5─ B A B C
│ │ A [ 0 5 2 ]
2 3 B [ 5 0 3 ]
│ │ C [ 2 3 0 ]
C ─3─ ┘
class AdjacencyMatrix:
def __init__(self, num_vertices):
self.num_vertices = num_vertices
self.matrix = [[0] * num_vertices for _ in range(num_vertices)]
def add_edge(self, u, v, weight=1):
"""간선 추가"""
self.matrix[u][v] = weight
self.matrix[v][u] = weight # 무방향 그래프
def has_edge(self, u, v):
"""간선 존재 확인"""
return self.matrix[u][v] != 0
def get_neighbors(self, vertex):
"""인접 정점들 반환"""
neighbors = []
for i in range(self.num_vertices):
if self.matrix[vertex][i] != 0:
neighbors.append(i)
return neighbors
장점:
단점:
그래프: 인접 리스트:
A ─── B A: [B, C]
│ │ B: [A, D]
C ─── D C: [A, D]
D: [B, C]
가중 그래프: 인접 리스트:
A ─5─ B A: [(B,5), (C,2)]
│ │ B: [(A,5), (C,3)]
2 3 C: [(A,2), (B,3)]
│ │
C ─3─ ┘
from collections import defaultdict
class AdjacencyList:
def __init__(self):
self.graph = defaultdict(list)
def add_edge(self, u, v, weight=1):
"""간선 추가"""
self.graph[u].append((v, weight))
self.graph[v].append((u, weight)) # 무방향 그래프
def get_neighbors(self, vertex):
"""인접 정점들 반환"""
return self.graph[vertex]
def has_edge(self, u, v):
"""간선 존재 확인"""
return any(neighbor == v for neighbor, _ in self.graph[u])
| 연산 | 인접 행렬 | 인접 리스트 |
|---|---|---|
| 공간 복잡도 | O(V²) | O(V + E) |
| 간선 추가 | O(1) | O(1) |
| 간선 제거 | O(1) | O(degree) |
| 간선 확인 | O(1) | O(degree) |
| 정점의 차수 | O(V) | O(1) |
| 모든 간선 순회 | O(V²) | O(V + E) |
그래프: BFS 탐색 순서:
A 1. A (시작)
/ \ 2. B, C (A의 인접 정점)
B C 3. D, E (B의 인접 정점)
/| | 4. F (E의 인접 정점)
D E F
from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
result = []
while queue:
vertex = queue.popleft()
if vertex not in visited:
visited.add(vertex)
result.append(vertex)
# 인접 정점들을 큐에 추가
for neighbor, _ in graph[vertex]:
if neighbor not in visited:
queue.append(neighbor)
return result
그래프: DFS 탐색 순서:
A 1. A (시작)
/ \ 2. B (A의 첫 번째 인접 정점)
B C 3. D (B의 첫 번째 인접 정점)
/| | 4. E (B의 두 번째 인접 정점)
D E F 5. C, F (백트래킹 후 탐색)
def dfs_recursive(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
result = [start]
for neighbor, _ in graph[start]:
if neighbor not in visited:
result.extend(dfs_recursive(graph, neighbor, visited))
return result
def dfs_iterative(graph, start):
visited = set()
stack = [start]
result = []
while stack:
vertex = stack.pop()
if vertex not in visited:
visited.add(vertex)
result.append(vertex)
# 인접 정점들을 스택에 추가 (역순으로 추가)
for neighbor, _ in reversed(graph[vertex]):
if neighbor not in visited:
stack.append(neighbor)
return result
| 특성 | BFS | DFS |
|---|---|---|
| 자료구조 | 큐 (Queue) | 스택 (Stack) |
| 메모리 사용 | 많음 | 적음 |
| 최단 경로 | 보장 | 보장 안함 |
| 완전성 | 완전함 | 완전함 |
| 최적성 | 최적 | 비최적 |
수강 과목 의존성:
수학1 ──→ 수학2 ──→ 미적분
│ │
↓ ↓
물리1 ──→ 물리2
위상 정렬 결과: 수학1 → 물리1 → 수학2 → 물리2 → 미적분
또는: 수학1 → 수학2 → 물리1 → 물리2 → 미적분
from collections import deque, defaultdict
def topological_sort_kahn(graph):
# 진입 차수 계산
in_degree = defaultdict(int)
for vertex in graph:
for neighbor, _ in graph[vertex]:
in_degree[neighbor] += 1
# 진입 차수가 0인 정점들을 큐에 추가
queue = deque([v for v in graph if in_degree[v] == 0])
result = []
while queue:
vertex = queue.popleft()
result.append(vertex)
# 인접 정점들의 진입 차수 감소
for neighbor, _ in graph[vertex]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# 사이클 검사
if len(result) != len(graph):
return None # 사이클 존재
return result
def topological_sort_dfs(graph):
visited = set()
temp_visited = set()
result = []
def dfs(vertex):
if vertex in temp_visited:
return False # 사이클 발견
if vertex in visited:
return True
temp_visited.add(vertex)
for neighbor, _ in graph[vertex]:
if not dfs(neighbor):
return False
temp_visited.remove(vertex)
visited.add(vertex)
result.append(vertex)
return True
for vertex in graph:
if vertex not in visited:
if not dfs(vertex):
return None # 사이클 존재
return result[::-1] # 역순으로 반환
3차 B-Tree (m=3):
[10, 20]
/ | \
[5] [15] [25, 30]
/ \ / \ / | \
[1] [7] [12] [17] [23] [27] [35]
class BTreeNode:
def __init__(self, degree):
self.degree = degree
self.keys = []
self.children = []
self.is_leaf = True
def search(self, key):
"""키 검색"""
i = 0
while i < len(self.keys) and key > self.keys[i]:
i += 1
if i < len(self.keys) and key == self.keys[i]:
return True
if self.is_leaf:
return False
return self.children[i].search(key)
문자열: "CAR", "CARD", "CAT", "DOG"
Root
/ \
C D
| |
A O
| |
R G
/|\ |
$ T D $
| |
$ $
$ = 문자열 끝 표시
장점:
단점:
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
"""단어 삽입"""
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word):
"""단어 검색"""
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
def starts_with(self, prefix):
"""접두사로 시작하는 단어 존재 확인"""
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
def get_words_with_prefix(self, prefix):
"""접두사로 시작하는 모든 단어 반환"""
node = self.root
for char in prefix:
if char not in node.children:
return []
node = node.children[char]
words = []
self._collect_words(node, prefix, words)
return words
def _collect_words(self, node, current_word, words):
"""DFS로 모든 단어 수집"""
if node.is_end_of_word:
words.append(current_word)
for char, child_node in node.children.items():
self._collect_words(child_node, current_word + char, words)
그래프:
A ──2── B ──3── C
│ │ │
1 4 2
│ │ │
D ──5── E ──1── F
다익스트라 실행 (시작: A):
단계 1: A(0) 선택 → B(2), D(1) 갱신
단계 2: D(1) 선택 → E(6) 갱신
단계 3: B(2) 선택 → C(5), E(6) 확인
단계 4: C(5) 선택 → F(7) 갱신
...
import heapq
from collections import defaultdict
def dijkstra(graph, start):
distances = defaultdict(lambda: float('inf'))
distances[start] = 0
previous = {}
pq = [(0, start)]
while pq:
current_dist, current = heapq.heappop(pq)
if current_dist > distances[current]:
continue
for neighbor, weight in graph[current]:
distance = current_dist + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
previous[neighbor] = current
heapq.heappush(pq, (distance, neighbor))
return distances, previous
distance[i][j] = min(distance[i][j], distance[i][k] + distance[k][j])def floyd_warshall(graph):
vertices = list(graph.keys())
n = len(vertices)
# 거리 행렬 초기화
dist = [[float('inf')] * n for _ in range(n)]
# 자기 자신까지의 거리는 0
for i in range(n):
dist[i][i] = 0
# 직접 연결된 간선들의 가중치 설정
for i, vertex_i in enumerate(vertices):
for neighbor, weight in graph[vertex_i]:
j = vertices.index(neighbor)
dist[i][j] = weight
# 플로이드-워셜 알고리즘
for k in range(n):
for i in range(n):
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
return dist
그래프:
A ──1── B ──4── C
│ │ │
2 3 5
│ │ │
D ──6── E ──7── F
1. 간선 정렬: (A,B,1), (A,D,2), (B,E,3), (B,C,4), (C,F,5), (D,E,6), (E,F,7)
2. 간선 선택: (A,B), (A,D), (B,E), (B,C), (C,F)
3. MST 완성: 총 가중치 = 15
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x, y):
px, py = self.find(x), self.find(y)
if px == py:
return False
if self.rank[px] < self.rank[py]:
px, py = py, px
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
return True
def kruskal(edges, n):
edges.sort(key=lambda x: x[2]) # 가중치로 정렬
uf = UnionFind(n)
mst = []
total_weight = 0
for u, v, weight in edges:
if uf.union(u, v):
mst.append((u, v, weight))
total_weight += weight
return mst, total_weight
def prim(graph, start):
mst = []
visited = {start}
edges = [(weight, start, neighbor) for neighbor, weight in graph[start]]
heapq.heapify(edges)
while edges and len(visited) < len(graph):
weight, u, v = heapq.heappop(edges)
if v in visited:
continue
visited.add(v)
mst.append((u, v, weight))
for neighbor, weight in graph[v]:
if neighbor not in visited:
heapq.heappush(edges, (weight, v, neighbor))
return mst
| 문제 유형 | 추천 알고리즘 | 시간 복잡도 |
|---|---|---|
| 그래프 탐색 | BFS/DFS | O(V + E) |
| 최단 경로 (단일 출발점) | 다익스트라 | O((V + E) log V) |
| 최단 경로 (모든 쌍) | 플로이드-워셜 | O(V³) |
| 최소 신장 트리 | 크루스칼/프림 | O(E log E) |
| 의존성 순서 | 위상 정렬 | O(V + E) |
| 문자열 검색 | 트라이 | O(문자열 길이) |
| 그래프 특성 | 추천 표현 방식 | 이유 |
|---|---|---|
| 조밀한 그래프 | 인접 행렬 | 간선 확인이 빠름 |
| 희소한 그래프 | 인접 리스트 | 메모리 효율적 |
| 가중치 그래프 | 인접 리스트 | 가중치 저장 용이 |
| 자주 변경되는 그래프 | 인접 리스트 | 동적 변경 용이 |