Data Structure - graph JS 21.04.17


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





© 2020.11. by creamer

Powered by CREAMer