고코딩
[자료구조] 그래프(Graph) 개념 본문
그래프(Graph)
그래프는 아이템(사물 또는 추상적 개념)들과 이들 사이의 연결관계를 표현한다.
단순히 정점,노드(N, node)와 그 노드를 연결하는 간선(E, edge)을 하나로 모아 놓은 자료 구조라고 할 수 있다.
Ex) 지도, 지하철 노선도의 최단 경로, 전기 회로의 소자들, 도로(교차점과 일방 통행길), 선수 과목 등
그래프와 트리의 차이점
- 오일러 경로
- 그래프에 존재하는 모든 간선(edge)을 한번만 통과하면 다시 정점(vertex)으로 돌아오는 그래프를 말한다.
- 그래프에 존재하는 모든 정점에 연결된 간선의 갯수가 짝수일때만 오일러 경로가 존재한다.
그래프(Graph)와 관련된 용어
- 정점(Vertex): 위치라는 개념. 노드(node)라고도 부름
- 간선(edge): 위치 간의 관계. 즉, 노드를 연결하는 선
- 인접 정점(adjacent vertex): 간선에 의해 직접 연결된 정점
- 정점의 차수(degree): 무방향 그래프에서 하나의 정점에 인접한 정점의 수
- 무방향 그래프에 존재하는 정점의 모든 차수의 합 그래프의 간선 수의 2배
- 진입 차수(in-degree): 방향 그래프에서 외부에서 오는 간선의 수
- 진출 차수(out-degree): 방향 그래프에서 외부로 향하는 간선의 수
- 경로 길이(path length): 경로를 구성하는데 사용도니 간선의 수
- 단순 경로(simple path): 경로 중에서 반복되는 정점이 없는 경우
- 사이클(cycle): 단순 경로의 시작 정점과 종료 정점이 동일한 경우
그래프(Graph)의 특징
- 그래프는 네트워크 모델이다.
- 2개 이상의 경로가 가능하다. -> 노드들 사이에 무방향/방향에서 양방향 경로를 가질 수 있다.
- self-loop뿐 아니라 loop/circuit 모두 가능하다
- 루트 노드라는 개념이 없다.
- 부모-자식의 관계가 아니다
- 순회는 DFS(깊이우선탐색)나 BFS(너비우선탐색)로 이루어진다.
- 그래프는 순환 혹은 비순환이다.
- 그래프는 크게 방향 그래프와 무방향 그래프가 있다.
- 간선의 유무는 그래프에 따라 다르다.
그래프의 유형
무방향그래프(Undirected Graph)
- 서로 대칭적인 관계를 연결해서 나타낸 그래프
- 간선에 방향이 없는 그래프, 무방향 그래프의 간선은 간선을 통해서 양 방향으로 갈수 있다.
- 정점 A와 B를 연결하는 간선은 (A,B)와 같이 표현한다.
- 무방향그래프에서는 (A,B) == (B,A) 이다.
- ex) 양방향 통행 도로
방향그래프(Directed Graph)
- 간선을 화살표로 표현. 간선에 방향성이 존재하는 그래프이다.
- A -> B 로 갈 수 있는 간선은 <A,B>로 표시한다.
- <A,B> 와 <B,A>는 다르다.
- ex) 일방 통행
가중치그래프(Weighted Graph)
- 간선에 비용이나 가중치가 부여된 그래프이다.
- 가중치는 두 정점 사이의 거리, 지나는 시간, 금액등이 될수 있으며 또한 음수인 경우도 존재한다.
- 두 정점 간의 이동방향이 다를경우 가중치가 다를 수 있다.
- ex) 도시-도시의 연결, 도로의 길이, 회로 소자의 용량, 통신망의 사용료 등
연결 그래프
- 무방향 그래프에 있는 모든 정점쌍에 대해서 항상 경로가 존재하는 경우
- ex) 트리: 사이클을 가지지 않는 연결 그래프
비연결 그래프
- 무방향 그래프에서 특정 정점쌍 사이에 경로가 존재하지 않는 경우
순환(사이클) 그래프
- 단순 경로의 시작 정점과 종료 정점이 동일한 경우
비순환 그래프
- 사이클이 없는 그래프
완전 그래프
- 그래프에 속해잇는 모든 정점이 서로 연결되어 있는 그래프
- 무방향 완전 그래프
- 정점 수N개 이면 간선의 수 N*(N-1)/2
그래프의 표현2가지
그래프는 주로 2가지 방법을 이용하여 표현한다. 인접행령, 인접리스트
메모리나 성능을 고려해서 결정해야 한다.
1. 인접행렬
인접 행렬은 N x N Boolean 행렬로서 matrix[i][j]가 true 라면 i->j로의 간선이 있다는 뜻이다.
if(간선(i,j)가 존재한다면)
matrix[i][j] = 1;
else
matrix[i][j] = 0;
a[i][j] = 0 이면 간선이 없다, a[i][j] = 1 이면 간선이 있다.
N개의 정점을 가진 그래프라면 NxN배열에 저장하여 표현한다. -> 간선의 수와 무관하게 항상 N^2개의 메모리 공간이 필요하다.
무방향 그래프를 인접행렬로 표현한다면 이 행렬은 대칭 행렬이 된다.
인접 행렬에서도 너비우선탐색이 가능하다. 단, 효율성이 떨어진다. 인접한 노드를 찾기 위해서는 모든 노드를 전부 순회해야 하기 때문이다.
단점
정점의 개수 N이 커지면 필요한 메모리크기는 N^2에 비례하여 커짐, 어떤 정점의 인접정점을 찾을때마다 N개의 정점을 탐색해야한다. 정점의 개수에 비례하여 시간도 증가한다.
2. 인접 리스트(Adjacency List)
인접 리스트(Adjacency List)로 그래프를 표현하는 것이 가장 일반적인 방법이다.
-
모든 정점을 인접리스트에 저장한다. 즉, 각각의 정점에 인접한 장점들을 리스트로 표시한 것이다.
- 배열(혹은 해시테이블)과 배열의 각 인덱스마다 존재하는 또 다른 리스트를 이용해서 인접 리스트를 표현
-
무방향 그래프에서 (a,b) 간선은 두번 저장된다.
- 한 번은 a정점에 인접한 간선을 저장하고 다른 한 번은 b에 인접한 간선을 저장한다.
- 정점의 수:N, 간선의 수: E인 무방향 그래프의 경우 -> N개의 리스트, N개의 배열, 2E개의 노드가 필요
-
트리에선 특정 노드 하나(루트 노드)에서 다른 모든 노드로 접근이 가능 -> Tree클래스 불필요
-
그래프에선 특정 노드에서 다른 모든 노드로 접근이 가능하지는 않음 -> Graph 클래스 필요
class Graph { public Node[] nodes; } class Node { public String name; public Node[] children; }
-
-
경로에 관한 정보가 아니다. 가중치 정보를 표시하려면 노드에 가중치 필드를 추가하여 표현해야 한다.
3. 인접리스트와 인접 행렬 선택 방법
- 인접리스트
- 그래프 내에 적은 숫자의 간선만을 가지는 희소 그래프(Sparse Graph)의 경우 -> 인접 행렬로 표현하면 공간낭비가 심함
- 장점
- 어떤 노드에 인접한 노드들을 쉽게 찾을 수 있다.
- 그래프에 존재하는 모든 간선의 수는 O(N+E) 안에 알수 있다. -> 인접 리스트 전체를 조사한다.
- 단점
- 간선의 존재 여부와 정점의 차수: 정점 i의 리스트에 있는 노드의 수 즉, 정점 차수만큼의 시간이 필요
- 인접 행령
- 그래프에 간선이 많이 존재하는 밀집 그래프(Dense Graph)의 경우
- 장점
- 두 정점을 연결하는 간선의 존재여부를 O(1)안에 즉시 알수 있다.(M[i][j] == 1 인지 아닌지 보면 됨)
- 정점의 차수는 O(N)안에 알 수 있다. : 인접 배열의 i번 째 행 또는 열을 모두 더한다.
- 단점
- 어떤 노드에 인접한 노드들을 찾기 위해서는 모든 노드를 전부 순회해야 한다.
- 그래프에 존재하는 모든 간선의 수는 O(N^2)안에 알 수 있다. : 인접 행렬 전체를 조사한다.
그래프 탐색
일반적인 방법은 2가지가 있다. 깊이 우선 탐색(Depth-First Search), 너비 우선 탐색(Breadth-First Search)
깊이 우선 탐색(DFS, Depth-First Search)
루트 노드(혹은 다른 임의의 노드)에서 시작해서 다음 분기(branch)로 넘어가기 전에 해당 분기를 완벽하게 탐색하는 방법
- 넓게 탐색하기 전에 깊게(deep) 탐색하는 것이다
- 사용하는 경우: 모든 노드를 방문 하고자 하는 경우에 이 방법을 선택한다.(깊이 우선 탐색이 너비 우선 탐색보다 좀 더 간단하다
import java.util.Iterator;
import java.util.LinkedList;
public class Graph {
private int v; //노드의 개수
private LinkedList<Integer> adj[]; //인접리스트
//생성자
Graph(int v){
this.v = v;
adj = new LinkedList[v];
for(int i=0; i<v ;i++) {
adj[i] = new LinkedList();
}
}
//노드를 연결
void addEdge(int v, int w) {
adj[v].add(w); //v노드와 w노드 사이에 간선이 있다는 뜻
}
//DFS에 의해 사용되는 함수
void DFSUtil(int v, boolean visited[]) {
//현재 노드를 방문한 것으로 표시하고 값을 출력
visited[v] = true;
System.out.print(v+" ");
//방문한 노드의 인접 노드들을 다 가져온다.
Iterator<Integer> i = adj[v].listIterator();
while(i.hasNext()) {
int n = i.next();
//방문하지 않은 노드면 해당 노드를 시작 노드로 다시 DFSUtil 호출
if(!visited[n]) {
DFSUtil(n, visited);
}
}
}
//주어진 노드를 시작 노드로 DFS탐색
void DFS(int v) {
//노드 방문 여부 판단, 초깃값: false
boolean visited[] = new boolean[this.v];
//v를 시작노드로 DFSUtil 순환 호출
DFSUtil(v,visited);
}
//모든 노드에서 DFS탐색
void DFS() {
//노드 방문 여부 판단, 초깃값: false
boolean visited[] = new boolean[this.v];
//비연결형 그래프의 경우, 모든 정점을 하나씩 방문
for(int i=0; i<this.v ;i++) {
if(visited[i] == false)
DFSUtil(i, visited);
}
}
}
public class Main {
public static void main(String[] args) throws IOException{
Graph g = new Graph(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
System.out.println("DFS - 2");
g.DFS(2);
System.out.println("DFS ");
g.DFS();
}
}
너비 우선 탐색(BFS, Breadth-First Search)
루트 노드(혹은 다른 임의의 노드)에서 시작해서 인접한 노드를 먼저 탐색하는 방법
- 깊게 탐색하기 전에 넓게(wide) 탐색하는 것이다.
- 사용하는 경우: 두 노드 사이의 최단 경로 혹은 임의의 경로를 찾고 싶을 때 이 방법을 선택한다.
import java.util.Iterator; import java.util.LinkedList; import java.util.Queue;
public class Graph {
private int v; //노드의 개수
private LinkedList adj[]; //인접리스트
//생성자
Graph(int v){
this.v = v;
adj = new LinkedList[v];
for(int i=0; i< this.v ; i++) {
adj[i] = new LinkedList();
}
}
//노드를 연결
void addEdge(int v, int w) {
adj[v].add(w);
}
//s를 시작노드로 한 BFS로 탐색하면서 탐색한 노드들을 출력
void BFS(int s) {
//노드의 방문 여부 판단 초깃값 false
boolean visited[] = new boolean[this.v];
//BFS구현을 위한 큐 생성
Queue<Integer> queue = new LinkedList<Integer>();
//현재 노드를 방문한 것으로 표시하고 큐에 삽입
visited[s]=true;
queue.add(s);
//큐가 빌때 까지 반복
while(!queue.isEmpty()) {
s = queue.poll();
System.out.print(s + " ");
//방문한 노드와 인접한 모든 노드를 가져온다.
Iterator<Integer> i = adj[s].listIterator();
while(i.hasNext()) {
int n = i.next();
//방문하지 않은 노드면 방문한것으로 표시하고 큐에 삽입
if(!visited[n]) {
visited[n] = true;
queue.add(n);
}
}
}
}
}
```java
public class Main {
public static void main(String[] args) throws IOException{
Graph g = new Graph(4);
g.addEdge(0, 1);
g.addEdge(0, 2);
g.addEdge(1, 2);
g.addEdge(2, 0);
g.addEdge(2, 3);
g.addEdge(3, 3);
g.BFS(2);
}
}
참고
https://gmlwjd9405.github.io/2018/08/13/data-structure-graph.html
https://herong.tistory.com/entry/Data-Structure-%EA%B7%B8%EB%9E%98%ED%94%84Graph
'자료구조' 카테고리의 다른 글
[자료구조] Hash의 개념 및 설명 (1) | 2021.01.11 |
---|---|
[자료구조] 분리집합(Disjoint set) 개념 및 JAVA 구현 (0) | 2021.01.07 |
[자료구조] 힙 개념과 JAVA로 구현 (1) | 2021.01.06 |
[자료구조] 이진트리의 순회 및 java구현 (0) | 2020.12.29 |
[자료구조] 트리(Tree)의 개념과 정리 (0) | 2020.12.29 |