봄봄.devlog

그래프(Graph) 본문

Computer Science/자료구조

그래프(Graph)

jihyun03 2020. 8. 15. 17:55

💡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
Comments