Data Structure - graph JS 21.04.17
in Dev on Java Script, Algorithm
graph : 여러개의 점들이 서로 복잡하게 연결되어 있는 관계를 표현한 자료구조.
서로 다른 점(정점:vertex)들이 직접적인 관계를 가지고 있다면 바로 이어주는 선(간선:edge)이 존재하고, 간접적인 관계를 가지고 있다면 다른 여러 점을 거쳐서 이어지는 선이 존재.
포털 사이트의 검색 엔진, SNS, 네비게이션 (길찾기) 등에 사용.
graph 실사용 예제
비가중치 그래프
여 예제에서는 간선은 이동 할 수 있음을 알려줄 뿐, 그 외의 정보는 없다. 가중치가 적혀 있지 않은 그래프를 비가중치 그래프라고 한다.
서울에 사는 A는 차를 타고 대전에 살고 있는 B를 태워 부산으로 간다.
정점(vertex) : 서울, 대전, 부산
간선(edge) : 서울--대전, 대전--부산, 서울--부산
간선은 이동 할 수 있음을 나타낸다.정점에 미국을 추가하게 된다면 자동차로 미국에 갈 수 없으니 어떠한 간선도 생기지 않고, 그래프에서는 이를 관계가 없다라고 표현.
가중치 그래프
가중치 그래프는 단순한 연결 유무만 판단하는 것이 아닌 더 많은 정보를 담을 수 있다.
위 예제를 가중치 그래프로 바꿔 주어 각 도시간의 거리를 표시.
정점(vertex) : 서울, 대전, 부산
간선(edge) : 서울-140km-대전, 대전-200km-부산, 서울-350km-부산
알아둬야 할 그래프 관련 용어
무향 그래프(undirected graph) : 정점 간 쌍방향으로 이동 가능 (서울에서 부산을 갈 수 있고 부산에서 서울 갈 수 있다.)
방향 그래프(directed graph) : 정점 간 일방향으로 이동 가능 (남한에서 북한으로 갈 수 있지만 북한에서 남한으로 나올 수 없다.)
진입 차수 (in-degree) : 한 정점에 들어오는 간선
진출 차수 (out-degree) : 한 정점에서 나가는 간선
인접 : 두 정점간에 간선이 직접 이어져있다면 두 정점은 인접한 정점.
자기 루프(self loop) : 정점에서 진출하는 간선이 곧 바로 자기 자신에게 진입하는 경우. 다른 정점을 거치지 않는다.
사이클(cycle) : 한 정점에서 출발하여 다시 해당 정점으로 돌아 갈 수 있는 경우. (서울 -> 대전 -> 부산 -> 서울)
그래프의 표현 방식 : 인접 행렬 & 인접 리스트
인접 행렬 (Adjacency Matrix)
인접 : 두 정점을 바로 이어주는 간선이 존재.
인접 행렬 : 정점들간의 인접함을 표시해주는 행렬. 2차원 배열의 모습을 가지고 있다.
- 2차원 배열 만들기
1. new Array : 배열 생성
new Array(배열의 길이)
new Array(1)
// [empty]
new Array(3)
// [empty × 3]
// === [empty, empty, empty]
2. 배열.fill : 배열 채워주기
배열.fill(값)
new Array(3).fill('값');
// ["값", "값", "값"]
new Array(3).fill(0);
// [0, 0, 0]
3. 반복 : 빈배열에 생성한 배열 반복해서 넣어주기
let result = [];
for(let i = 0; i < 만들고 싶은 배열의 길이; i++) {
result.push(new Array(만들고 싶은 배열의 길이).fill(0));
}
- 인접 행렬 구현
class GraphWithAdjacencyMatrix {
constructor() {
this.matrix = [];
}
//버텍스 추가
addVertex() {
// 현재 matrix 배열의 길이
const currentLength = this.matrix.length;
for (let i = 0; i < currentLength; i++) {
this.matrix[i].push(0);
}
this.matrix.push(new Array(currentLength + 1).fill(0));
}
// 버텍스(인덱스)가 있는지 확인
contains(vertex) {
if( 0 <= vertex && vertex < this.matrix.length ) {
return true;
}
return false;
}
// 엣지 추가
addEdge(from, to) {
const currentLength = this.matrix.length;
if (from === undefined || to === undefined) {
console.log("2개의 인자가 있어야 합니다.");
return;
}
// 간선을 추가 수 없는 경우(from, to 범위를 넘어서는 경우)
if (from + 1 > currentLength || to + 1 > currentLength || from < 0 || to < 0) {
console.log("범위가 매트릭스 밖에 있습니다.");
return;
}
this.matrix[from][to] = 1;
}
// 버텍스 간 엣지 확인
hasEdge(from, to) {
if (this.matrix[from][to] === 1) {
return true;
}
return false;
}
// 엣지 제거
removeEdge(from, to) {
const currentLength = this.matrix.length;
if (from === undefined || to === undefined) {
console.log("2개의 인자가 있어야 합니다.");
return;
}
// 간선을 지울 수 없는 경우(from, to 범위를 넘어서는 경우)
if (from + 1 > currentLength || to + 1 > currentLength || from < 0 || to < 0) {
return;
}
// 엣지 제거
this.matrix[from][to] = 0;
}
}
언제 인접 행렬을 사용하면 좋을까?
한 개의 큰 표와 같은 모습을 한 인접 행렬은 두 정점 사이에 관계가 있는지, 없는지 확인 시 사용. 예를 들어, A에서 B로 진출하는 간선이 있는지 파악하기 위해선 0 번째 줄의 1 번째 열에 어떤 값이 저장되어있는지 바로 확인 가능
가장 빠른 경로(shortest path)를 찾고자 할 때 주로 사용
인접 리스트 (Adjacency List)
인접 리스트는 각 정점이 어떤 정점과 인접한지 리스트의 형태로 볼 수 있는 표현 방식. 각 정점마다 하나의 리스트를 가지고 있으며, 이 리스트에는 자신과 인접한 다른 정점들이 담겨 있음
// A-B-C가 인접한 경우
A - [B]
B - [A, C] // === B - [C, A] // A와 C의 순서는 중요하지 않다. 순서가 중요한 경우 정렬 필요
C - [B]
- 인접 리스트 구현
// undirected graph (무향 그래프)
// adjacency list (인접 리스트)
class GraphWithAdjacencyList {
constructor() {
this.vertices = {};
}
// gal = { vertices : { } };
addVertex(vertex) {
// TODO: 정점을 추가합니다.
// 넘겨받은 인자(정점)은 키가 되며, 빈 배열을 값으로 할당합니다.
// 이미 존재하는 정점이라면, 덮어 씌워지지 않아야 합니다.
if(this.vertices[vertex] === undefined) {
this.vertices[vertex] = [];
}
// === this.vertices[vertex] = this.vertices[vertex] || [];
// 인스턴스의 vertices의 키인 vertex는 = true인 경우 this.vertices[vertex], false인 경우(undefined)인 경우 [];
}
contains(vertex) {
// 인자로 넘겨받은 정점의 존재여부를 반환합니다.
return !!this.vertices[vertex];
// !!를 쓰면 true 또는 false 값으로 반환 가능
// 즉, 인스턴스의 해당 키 값(this.vertices[vertex])이 undefinded인 경우 === false
// 인스턴스의 해당 키 값(this.vertices[vertex])이 있는 경우 === true
}
addEdge(fromVertex, toVertex) {
// TODO: 간선을 추가합니다.
// - fromVertex의 인접 리스트에 toVertex를 추가하고
// - toVertex의 인접 리스트에 fromVertex를 추가합니다.
// 넘겨받은 2개의 정점 모두 존재하는 정점이어야 합니다.
if (!this.contains(fromVertex) || !this.contains(toVertex)) {
return;
}
if (!this.hasEdge(fromVertex, toVertex)) {
this.vertices[fromVertex].push(toVertex);
}
if (!this.hasEdge(toVertex, fromVertex)) {
this.vertices[toVertex].push(fromVertex);
}
}
hasEdge(fromVertex, toVertex) {
// 만약 정점(fromVertex)이 존재하지 않는다면
if (!this.contains(fromVertex)) {
// false를 반환합니다
return false;
}
// 존재한다면 해당 정점의 리스트에 toVertex가 포함되어있는지 반환합니다
return !!this.vertices[fromVertex].includes(toVertex);
}
removeEdge(fromVertex, toVertex) {
// TODO: 간선을 삭제합니다.
// 인자로 넘겨받은 두 정점이 모두 존재한다면
// - fromVertex의 인접 리스트에 있는 toVertex를 삭제하고
// - toVertex의 인접 리스트에 있는 fromVertex를 삭제합니다.
if (!this.contains(fromVertex) || !this.contains(toVertex)) {
return;
}
if (this.hasEdge(fromVertex, toVertex)) {
const index = this.vertices[fromVertex].indexOf(toVertex);
this.vertices[fromVertex].splice(index, 1);
}
// TODO: 두번째 정점의 인접 리스트에 첫번째 정점이 있을 경우
if (this.hasEdge(toVertex, fromVertex)) {
const index = this.vertices[toVertex].indexOf(fromVertex);
this.vertices[toVertex].splice(index, 1);
}
}
removeVertex(vertex) {
// TODO: 정점을 삭제합니다.
// 인자로 넘겨받은 정점(A)이 존재한다면
// - 이 정점(A)을 삭제함은 물론,
// - 다른 모든 정점들의 리스트를 순회하며 넘겨받은 정점(A)과 이어져 있는 간선을 제거합니다
if (this.contains(vertex)) {
// 간석 삭제
for(let v in this.vertices) {
// 정점의 키의 값 순회.
// 만약 배열이 vertex를 요소로 가지고 있다면
if (this.vertices[v].includes(vertex)) {
this.vertices[v].splice(this.vertices[v].indexOf(vertex),1);
}
}
// 정점 삭제
delete this.vertices[vertex];
}
}
}
인접 리스트는 언제 사용하면 좋을까?
- 인접 행렬은 연결 가능한 모든 경우의 수를 저장합니다. 서로 인접하지 않다면 0을, 인접하다면 1을 저장하기 때문에 메모리를 많이 차지. 그러므로 메모리를 효율적으로 사용하고 싶을 때 인접 행렬 대신 인접 리스트를 사용
그래프 관련 더 알아보기
- 그래프 간단 설명 : https://www.youtube.com/watch?v=fVcKN42YXXI