여러개의 점(노드)들이 서로 복잡하게 연결되어 있는 관계를 표현한 자료구조다(트리완 달리 양방향 데이터 흐름을 가질 수 있음)
즉, 그래프가 일정하고 단방향 구조를 띈다면 트리, 스택/큐와 비슷하다.
컴공에선 마치 거미줄처럼 여러 개의 점들이 선으로 이어져 있는 복잡한 네트워크망과 같은 모습을 띄는 구조를 뜻함.
아래 사진은 그래프를 이용한 SNS 친구 관계도를 그린 것임또 그 외 그래프 예시는 네비게이션 최적경로, 사이트 연결맵, 추천 엔진 등이 있다.
두 정점을 바로 이어주는 간선이 있다면 이 두 정점은 인접하다!
서로 다른 정점들이 인접한 상태인지를 표시한 행렬로 2차원 배열의 형태
각 정점이 어떤 정점과 인접하는지를 리스트의 형태로 표현, 각 정점마다 하나의 리스트를 가지고 있으며, 이 리스트는 자신과 인접한 다른 정점을 가짐
보통은 중요하지 않다. 그래프, 트리, 스택, 큐 등 모든 자료구조는 구현하는 사람의 편의와 목적에 따라 기능을 추가/삭제할 수 있다. 그래프를 인접 리스트로 구현할 때, 정점별로 살펴봐야 할 우선 순위를 고려해 구현할 수 있다. 이때, 리스트에 담겨진 정점들을 우선 순위별로 정렬할 수 있다. 우선 순위가 없다면, 연결된 정점들을 단순하게 나열한 리스트가 된다.
우선 순위를 다뤄야 한다면 더 적합한 자료구조(ex. queue, heap)를 사용하는 것이 합리적. 따라서 보통은 중요하지 않다. (언제나 예외는 존재.)
참고용임
서울에 사는 A는 부산에 사는 B와 오랜 친구 사이입니다. 이번 주말에 부산에서 열리는 B의 결혼식에 참석하기 위해 A는 차를 몰고 부산으로 가려고 합니다. 대전에 살고 있는 친구 C도 B의 결혼식에 참석한다고 하여, A는 서울에서 출발하여 대전에서 C를 태워 부산으로 이동하려고 합니다.
위의 예제를 분석해보면
이다. 즉, 이 간선은 내비게이션에서 이동할 수 있음을 나타냄,
실제 내비게이션은 간선에 거리를 표기한 가중치 그래프가 확장되어, 수백만 개의 정점(주소)과 간선이 추가되어야 비로소 내비게이션에서 쓰는 자료구조와 유사
그래프의 탐색은 하나의 정점에서 시작하여 그래프의 모든 정점들을 한 번씩 방문(탐색)하는 것이 목적, 최단 경로나 최소 환승 등 하나의 목적에도 여러 가지 방법, 정점 탐색 방법에도 여러 가지
가장 대표적인 두 가지 방법, BFS와 DFS를 알아보자!
이 둘은 데이터를 탐색하는 순서만 다를 뿐, 모든 자료를 하나씩 확인해 본다는 점은 같다!
⭐️사용할 때의 장단점은 분명하기 때문에 해당 상황에 맞는 탐색 기법을 사용해야한다!
너비 우선 탐색, 찾고자하는 목표에서 정점부터 탐색, ⭐️주로 두 정점 사이의 최단 경로를 찾을 때 사용
깊이 우선 탐색, 하나의 경로를 끝까지 탐색한 후, 목표가 아니라면 다음 경로로 넘어가 탐색, 한 정점에서 시작해서 다음 경로로 넘어가기 전에 해당 경로를 완벽하게 탐색할 때 사용
BFS보다 탐색 시간은 조금 오래 걸릴지라도 모든 노드를 완전히 탐색
BFS는 ⭐️최단 경로를 찾는다면 유용하게 쓰일 알고리즘이다. 일단 간단하게 BFS의 구조를 나타낸 코드를 보자.
BFS는 일반적으로 큐(Queue)를 사용하여 구현된다. 시작 정점을 큐에 삽입하고, 큐에서 정점을 하나씩 꺼내면서 해당 정점과 인접한 정점들을 모두 방문. 이때, 이미 방문한 정점을 다시 방문하지 않도록 체크하는 것이 중요!
const graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
};
function bfs(graph, start) {
const visited = new Set();
const queue = [start];
while (queue.length > 0) {
const vertex = queue.shift();
if (!visited.has(vertex)) {
visited.add(vertex);
console.log(vertex);
const neighbors = graph[vertex];
for (const neighbor of neighbors) {
queue.push(neighbor);
}
}
}
}
bfs(graph, 'A');
이 예시에서는 graph
객체를 인접 리스트로 표현했다. bfs
함수는 graph
와 시작 정점 start
를 입력으로 받아, BFS를 수행하여 그래프의 모든 정점을 탐색한다. 함수 내부에서는 visited
집합과 queue
배열을 초기화한 후, 시작 정점을 queue
에 삽입한다. 이후에는 queue
에서 정점을 하나씩 꺼내어 해당 정점이 방문된 적이 없다면 방문 처리하고, 해당 정점과 인접한 정점을 모두 queue
에 삽입. 이렇게 BFS를 반복하면서 그래프의 모든 정점을 탐색할 수 있다.
문제
다음은 정점과 간선의 정보가 주어졌을 때, 특정 노드에서 다른 모든 노드까지의 최단 경로의 길이를 구하는 문제입니다.
5 6
1 2
1 3
1 4
2 4
3 4
4 5
위 입력에서 첫 번째 줄은 정점과 간선의 개수를 나타냅니다. 이어지는 각 줄은 간선의 정보를 나타냅니다. 예를 들어, 1 2는 1번 노드와 2번 노드 사이에 간선이 있다는 것을 나타냅니다. 이 때, 1번 노드에서부터 다른 모든 노드까지의 최단 경로의 길이를 구하는 함수 shortestPath(n, edges, start)을 작성하세요.
function shortestPath(n, edges, start) {
// 인접 리스트 초기화
const adjList = new Array(n + 1).fill().map(() => []);
// 간선 정보를 바탕으로 인접 리스트 구성
for (const [u, v] of edges) {
adjList[u].push(v);
adjList[v].push(u);
}
// 방문 여부를 나타내는 배열과 거리 정보를 나타내는 배열 초기화
const visited = new Array(n + 1).fill(false);
const distances = new Array(n + 1).fill(Infinity);
// 시작 노드의 거리를 0으로 초기화
distances[start] = 0;
// BFS 구현
const queue = [start];
while (queue.length > 0) {
const node = queue.shift();
visited[node] = true;
for (const neighbor of adjList[node]) {
if (!visited[neighbor]) {
queue.push(neighbor);
distances[neighbor] = distances[node] + 1;
}
}
}
// 결과 반환
return distances.slice(1);
}
const n = 5;
const edges = [
[1, 2],
[1, 3],
[1, 4],
[2, 4],
[3, 4],
[4, 5],
];
const start = 1;
const result = shortestPath(n, edges, start);
console.log(result); // [0, 1, 1, 1, 2]
작 노드에서부터 하나의 경로를 따라 마지막 노드까지 탐색한 후, 다른 경로로 이동하여 탐색을 진행, DFS는 그래프에서 경로를 탐색하거나, 모든 정점을 탐색하는데 사용된다!
일반적으로 스택(Stack)이나 재귀(Recursion)를 사용하여 구현된다. 시작 정점을 스택에 삽입하고, 스택에서 정점을 하나씩 꺼내면서 해당 정점의 인접한 정점 중에서 방문하지 않은 정점을 방문한다. DFS도 이미 방문한 정점을 다시 방문하지 않도록 체크하는 것이 중요!!
const graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
};
function dfs(graph, start) {
const visited = new Set();
const stack = [start];
while (stack.length > 0) {
const vertex = stack.pop();
if (!visited.has(vertex)) {
visited.add(vertex);
console.log(vertex);
const neighbors = graph[vertex];
for (const neighbor of neighbors) {
stack.push(neighbor);
}
}
}
}
dfs(graph, 'A');
이 예시에서는 graph
객체를 인접 리스트로 표현했다. dfs
함수는 graph
와 시작 정점 start
를 입력으로 받아, DFS를 수행하여 그래프의 모든 정점을 탐색한다.
함수 내부에서는 visited
집합과 stack
! 배열을 초기화한 후, 시작 정점을 stack
!에 삽입한다.
이후에는 stack
에서 정점을 하나씩 꺼내어 해당 정점이 방문된 적이 없다면 방문 처리하고, 해당 정점의 인접한 정점을 모두 stack
에 삽입한다.
다음은 노드의 개수와 간선의 정보가 주어졌을 때, 연결된 노드들의 그룹 수를 구하는 문제입니다.
5 4
1 2
2 3
3 4
4 5
위 입력에서 첫 번째 줄은 노드의 개수와 간선의 개수를 나타냅니다. 이어지는 각 줄은 간선의 정보를 나타냅니다. 예를 들어, 1 2는 1번 노드와 2번 노드 사이에 간선이 있다는 것을 나타냅니다. 이 때, 연결된 노드들의 그룹 수를 구하는 함수 countGroups(n, edges)을 작성하세요.
function countGroups(n, edges) {
// 노드 간 연결 정보를 저장하는 그래프
const graph = new Array(n + 1).fill(null).map(() => []);
// 노드의 방문 여부를 저장하는 배열
const visited = new Array(n + 1).fill(false);
// 연결된 노드 그룹의 수
let count = 0;
// 그래프 생성: 간선 정보를 이용해 그래프를 만든다
for (const [u, v] of edges) {
graph[u].push(v);
graph[v].push(u);
}
// DFS 탐색 함수
function dfs(u) {
visited[u] = true;
for (const v of graph[u]) {
if (!visited[v]) {
dfs(v);
}
}
}
// 모든 노드를 방문하며 연결된 그룹의 수 계산
for (let i = 1; i <= n; i++) {
if (!visited[i]) {
count++;
dfs(i);
}
}
return count;
}
countGroups(5, [[1, 2], [2, 3], [3, 4], [4, 5]])
가 호출되면 함수가 실행된다.graph
배열과 visited
배열, count
변수가 초기화graph
배열을 채운다.graph[1] = [2]
, graph[2] = [1, 3]
, graph[3] = [2, 4]
, graph[4] = [3, 5]
, graph[5] = [4]
dfs
함수를 이용해 그룹을 찾는다.visited
배열에 표시dfs
를 수행한다.visited
배열의 값을 확인하여, 방문하지 않은 노드가 있다면 그룹의 개수를 1 증가시키고 dfs
를 호출한다.false
, 끊어진게 있다면 count
증가