봄봄.devlog
그래프(Graph) 본문
💡Goal
- 그래프의 기본 개념 이해
- 그래프의 종류 구분
- 그래프의 표현
- 그래프의 탐색
그래프의 개념
노드(node)와 그 노드를 연결하는 간선(edge)을 하나로 모아놓은 자료 구조
- 즉, 연결되어 있는 객체 간의 관계를 표현할 수 있는 자료 구조이다.
ex) 지도, 지하철 노선도의 최단 경로, 전기 회로의 소자들, 도로(교차점과 일방 통행길) 등
그래프의 종류
1. 무방향 그래프(undirected graph) vs 방향 그래프(directed graph)
- 무방향 그래프(Undirected Graph)
- 무방향 그래프의 간선은 간선을 통해서 양 방향으로 갈 수 있다.
- 정점 A와 정점 B를 연결하는 간선은 (A, B)와 같이 정점의 쌍으로 표현한다
- (A,B)는 (B,A) 동일
- ex) 양방향 통행 도로
- 방향 그래프(Directed Graph)
- 간선에 방향성이 존재하는 그래프
- A -> B로만 갈 수 있는 간선은 <A, B>로 표시한다.
- <A,B>는 <B,A>는 다름
- ex) 일방 통행
2. 가중치 그래프(Weighted Graph)
- 가중치 그래프(Weighted Graph)
- 간선에 비용이나 가중치가 할당된 그래프
- "네트워크"라고도 한다.
- ex) 도시-도시의 연결, 도로의 길이, 회로 소자의 용량, 통신망의 사용료 등
3. 연결 그래프 vs 비연결 그래프
- 연결 그래프(Connected Graph)
- 무방향 그래프에 있는 모든 정점쌍에 대해서 항상 경로가 존재하는 경우
- ex) 트리(Tree) : 사이클을 가지지 않는 연결 그래프
- 비연결 그래프(Disconnected Graph)
- 무방향 그래프에서 특정 정점쌍 사이에 경로가 존재하지 않는 경우
4. 사이클 vs 비순환 그래프
- 사이클(Cycle)
- 단순 경로의 시작 정점과 종료 정점이 동일한 경우
- 단순 경로(Simple Path) : 경로 중에서 반복되는 정점이 없는 경우
- 단순 경로의 시작 정점과 종료 정점이 동일한 경우
- 비순환 그래프(Acyclic Graph)
- 사이클이 없는 그래프
5. 완전 그래프
- 완전 그래프(Complete Graph)
- 그래프에 속해 있는 모든 정점이 서로 연결되어 있는 그래프
- 무방향 완전 그래프
- 정점 수 : n이면 간선의 수 : n*(n-1) / 2
그래프의 표현
1. 인접 행렬(Adjacent Matrix)
그래프를 2차원 배열로 구현한다.
- 정정(노드)의 개수가 N인 그래프를 인접 행렬로 표현
- 간선의 수와 무관하게 항상 n^2개의 메모리 공간이 필요하다.
- 무방향 그래프를 인접 행렬로 표현한다면 이 행렬은 대칭 행렬(Symmetric Matrix)이 된다.
- 물론 방향 그래프는 대칭 행렬이 안될 수 있다.
- 인접 리스트를 사용한 그래프 알고리즘들 또한 인접 행렬에서도 사용이 가능하다.
- 하지만 인접 행렬은 조금 효율성이 떨어진다.
- 인접 리스트는 어떤 노드에 인접한 노드들을 쉽게 찾을 수 있지만 인접 행렬에서는 인접한 노드를 찾기 위해서는 모든 노드를 전부 순회해야 한다.
2. 인접 리스트(Adjacent List)
인접 리스트(Adjacent List)로 그래프를 표현하는 것이 가장 일반적인 방법이다.
- 모든 정점(혹은 노드)을 인접리스트에 저장한다. 즉, 각각의 정점에 인접한 정점들을 리스트로 표시한 것이다.
- 배열(혹은 해시테이블)과 배열의 각 인덱스마다 존재하는 또 다른 리스트(배열, 동적 가변 크기 배열(ArrayList), 연결리스트(LinkedList) 등)를 이용해서 인접 리스트를 표현
- 정점의 번호만 알면 이 번호를 배열의 인덱스로 하여 각 정점의 리스트에 쉽게 접근할 수 있다.
- 무방향 그래프(Undirected Graph)에서 (a, b)간선은 두 번 저장된다.
- 한 번은 a 정점에 인접한 간선을 저장하고 다른 한 번은 b에 인접한 간선을 저장한다.
- 정점의 수 : N, 간선의 수 : E인 무방향 그래프의 경우
- N개의 리스트, N개의 배열, 2E개의 노드가 필요
- 트리에서는 특정 노드 하나(루트 노드)에서 다른 모든 노드로 접근이 가능하지만, 그래프에서는 특정 노드에서 다른 모든 노드로 접근이 가능하지는 않다.
인접 리스트와 인접 행렬 중 선택 방법
- 인접 행렬
- 그래프에 간선이 많이 존재하는 밀집 그래프(Dense Graph)의 경우
- 장점
- 두 정점을 연결하는 간선의 존재 여부 (M[i][j])를 O(1) 안에 즉시 알 수 있다.
- 정점의 차수는 O(N) 안에 알 수 있다. 인접 행렬 i번째 행 또는 열을 모두 더한다.
- 단점
- 어떤 노드에 인접한 노드들을 찾기 위해서는 모든 노드를 전부 순회해야 한다.
- 그래프에 존재하는 모든 간선의 수는 O(N^2) 안에 알 수 있다. => 인접 행렬 전체를 조사한다
- 인접 리스트
- 그래프 내에 적은 숫자의 간선만을 희소 그래프(Sparse Graph)의 경우
- 장점
- 어떤 노드에 인접한 노드들을 쉽게 찾을 수 있다.
- 그래프에 존재하는 모든 간선의 수는 O(N+E) 안에 알 수 있다. 인접 리스트 전체를 조사한다.
- 단점
- 간선의 존재 여부와 정점의 차수 : 정점 i의 리스트에 있는 노드의 수 즉, 정점 차수만큼의 시간이 필요
// 인접 리스트 구현
static final int N = 30; // 정점의 수
ArrayList[] list = new ArrayList[N];
for(int i=0; i< list.length; i++) {
list[i] = new ArrayList<Integer>();
}
// a, b 사이 양방향 링크 추가
list[a].add(b);
list[b].add(a);
그래프의 탐색
깊이 우선 탐색(Depth-First Search) 과 너비 우선 탐색(Breadth-First Search)
목적 : 시작점 X부터 시작해서 모든 정점을 1번씩!
1. 깊이 우선 탐색(DFS, Depth-First Search)
루트 노드(혹은 다른 임의의 노드)에서 시작해서 다음 분기(branch)로 넘어가기 전에 해당 분기를 완벽하게 탐색하는 방법
- 즉, 넓게(wide) 탐색하기 전에 깊게(deep) 탐색하는 것이다.
- 사용하는 경우 : 모든 노드를 한번씩 방문 하고자 하는 경우(DFS이 BFS보다 좀 더 간단하다)
1) 재귀 호출을 이용한 구현
void dfs(int x) {
check[x] = true; // 방문여부 check
for(int i=1; i<=n; i++) {
if(a[x][i] == 1 && check[i] == false) { // 다음 정점이 존재하면
dfs(i);
}
}
}
* 시간복잡도
for(int i=1; i<=n; ~~) 한 정점에 연결된 간선을 찾으려면 모든 정점을 방문해야 한다. 따라서
for문 한번 돌 때 O(V)
그러면 총 dfs(X) 호출이 V번 하게 되면 총 시간복잡도는 O(V^2)이다.
2) 인접리스트를 이용한 구현
void dfs(int x) {
check[x] = true;
for(int i=0; i<a[x].size(); i++) {
int y = a[x].get(i);
if(check[y] == false) {
dfs(y);
}
}
}
* 시간복잡도
dfs(X) * V번
인접리스트에서 for문은 한 정점에 연결된 간선의 개수이다.
모든 간선을 인접리스트에 저장했는데 인접리스트에 있는 간선을 모두 한번씩만 거쳐가므로
시간복잡도는 O(V+E)이다.
3) HashSet을 이용한 구현
static void DFS(HashMap<Character,String> 그래프, char 현재정점, HashSet<Character> 방문한정점) {
방문한정점.add(현재정점);
System.out.printf("%c ", 현재정점);
String 인접정점목록 = 그래프.get(현재정점);
for (char 인접정점 : 인접정점목록.toCharArray())
if (방문한정점.contains(인접정점) == false)
DFS(그래프, 인접정점, 방문한정점);
}
4) Stack을 이용한 구현
public static void DFS(HashMap<Character,String> 그래프, char 시작정점) {
HashSet<Character> 방문한정점 = new HashSet<>();
Stack<Character> 다음에방문할정점목록 = new Stack<>();
방문한정점.add(시작정점);
다음에방문할정점목록.push(시작정점);
while (다음에방문할정점목록.isEmpty() == false) {
char 현재정점 = 다음에방문할정점목록.pop();
System.out.printf("%c ", 현재정점);
String 인접정점목록 = 그래프.get(현재정점);
for (char 인접정점 : 인접정점목록.toCharArray())
if (방문한정점.contains(인접정점) == false) {
방문한정점.add(인접정점);
다음에방문할정점목록.add(인접정점);
}
}
}
2. 너비 우선 탐색(BFS, Breadth-First Search)
루트 노드(혹은 다른 임의의 노드)에서 시작해서 인접한 노드를 먼저 탐색하는 방법
- 사용하는 경우 : 두 노드 사이의 최단 경로 혹은 임의의 경로를 찾고 싶을 때
- ex) 지구상에 존재하는 모든 친구 관계를 그래프로 표현한 후 Ash와 Vanessa 사이에 존재하는 경로를 찾는 경우
- DFS의 경우 - 모든 친구 관계를 다 살펴봐야 할지도 모른다.
- BFS의 경우 - Ash와 가까운 관계부터 탐
1) 인접행렬 구현
Queue<Integer> q;
check[1] = true; q.add(1);
while(!q.isEmpty()) {
int x = q.peek(); q.remove();
for(int i=1; i<=n; i++) {
if(a[x][i] == 1 && check[i] == false) {
check[i] = true;
q.add(i);
}
}
}
시간복잡도 O(V^2)
2) 인접리스트 구현
Queue<Integer> q;
check[1] = true; q.add(1);
while(!q.isEmpty()) {
int x = q.peek(); q.remove();
for(int i=0; i<a[x].size(); i++) {
int y = a[x].get(i);
if(check[y] == false) {
check[y] = true; q.add(y);
}
}
}
시간복잡도 O(V+E)
3) HashSet을 이용한 구현
public static void BFS(HashMap<Character,String> 그래프, char 시작정점) {
HashSet<Character> 방문한정점 = new HashSet<>();
Queue<Character> 다음에방문할정점목록 = new LinkedList<Character>();
방문한정점.add(시작정점);
다음에방문할정점목록.add(시작정점);
while (다음에방문할정점목록.isEmpty() == false) {
char 현재정점 = 다음에방문할정점목록.remove();
System.out.printf("%c ", 현재정점);
String 인접정점목록 = 그래프.get(현재정점);
for (char 인접정점 : 인접정점목록.toCharArray())
if (방문한정점.contains(인접정점) == false) {
방문한정점.add(인접정점);
다음에방문할정점목록.add(인접정점);
}
}
}
'Computer Science > 자료구조' 카테고리의 다른 글
트리 (0) | 2020.10.28 |
---|---|
힙(Heap) (0) | 2020.08.20 |