ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 위상정렬 Topological Sort
    Algorithm/Theory 2020. 6. 7. 13:21

    목표

    - 위상정렬 (Topological Sort)를 이해하고 설명할 수 있다.
    - 위상정렬의 조건을 알고 언제 어떻게 사용하는지 이해한다.

     


     

    위상정렬?

    \

    특정한 순서를 가진 조건에서 사용한다.

    DAG(방향성 비순환 그래프) 에서만 사용할 수 있다.

    \

     

     

    "위상" 이라는 사전적 정의에서 알 수 있듯이, 위상정렬이란 어떤 순서를 가지고 있는 경우에 사용할 수 있는 정렬 방법입니다.

    게임을 하는데 어떤 스킬을 익히기 위해 반드시 먼저 배워야 하는 사전스킬이 있죠? 이런 식으로 어떤 순서를 가지고 있을 때 사용합니다.

    이 말인즉 위상정렬은 방향성을 가지고 있어야 합니다. 그리고 이 의미를 잘 생각해보면 사이클이 생기면 안된다는 것을 알 수 있습니다. 

    A -> B -> C 순서를 가지고 있어야 하는데 여기서 C -> A가 되어버린다면? 말이 안되겠죠?

    방향성을 가지고 사이클이 없는 그래프를 DAG(Directed Acyclic Graph) 방향성 비순환 그래프라고 합니다.

     

    구현

    위상정렬은 다음 방식으로 진행됩니다.

    \

    1. 진입차수가 0인 노드를 큐에 담는다.

    2. 큐에서 노드를 꺼내고 해당 노드에 연결된 간선을 모두 제거한다.

    3. 다시 진입차수가 0인 노드를 큐에 담는다.

    4. 큐에서 꺼낸 순서가 정렬 순서가 된다.

    \

     

    위상정렬은 스택이나 큐를 사용해서 구현할 수 있는데요, 오늘은 큐를 사용해서 구현해보려고 합니다.

    진입차수라는 개념이 등장하는데요, 이는 자신으로 향하는 화살표의 개수를 의미합니다. 

    진입차수가 없다는 말은 바로 접근할 수 있다는 의미가 되겠죠. 진입차수가 없는 것부터 시작해서 간선을 끊어내면서 정렬해나가는 방식이라고 할 수 있습니다.

     

    다음과 같은 그래프를 생각해봅시다. 
    각 노드의 진입차수를 계산해보면, 아래와 같고 큐에 진입차수가 0인 1을 큐에 담습니다.

    노드 1 2 3 4 5
    진입차수 0 1 1 2 1
    Queue 1
    결과  

     

    다음으로 큐에 하나를 꺼내고 꺼낸 노드에 연결된 간선을 모두 제거합니다.
    그리고 다시 진입차수가 0인 노드 2를 큐가 담습니다. 

     

    노드 1 2 3 4 5
    진입차수   0 1 2 1
    Queue 2
    결과 1

     

    다시 2를 꺼내고 2에 연결된 간선을 모두 제거하고 진입차수가 0인 3을 큐에 담습니다.
    2에서 4로 가고 있었던 간선도 제거했으니 4의 진입차수는 1이 되겠죠?

     

    노드 1 2 3 4 5
    진입차수     0 1 1
    Queue 3
    결과 1, 2

     

     

    이렇게 반복해나가면 됩니다.

    정상적인 위상정렬이라면 딱 N번만 실행되어야 합니다. 

    아직 정렬이 끝나지 않았는데 큐가 비어있다면 사이클이 생겼다는 의미가 됩니다. 이때는 위상정렬을 할 수 없습니다.

     

    이제 코드로 직접 살펴보겠습니다.

    처음 맨 처음에 진입 차수가 0인 노드를 확인해 큐에 담아 시작하고, 간선을 제거하고 나서 간선이 제거된 노드의 진입차수가 0인지 확인하면서 큐에 담는 방식으로 구현합니다.

    편의상 Graph 구조체를 정의해서 사용했습니다.

    struct Graph<T>: Equatable where T: Equatable {
        var childs: [T]
        
        mutating func push(_ child: T) {
            self.childs.append(child)
        }
    }
    
    var n = 5
    var inDegree = [Int](repeating: 0, count: n) // 각 정점의 진입차수 정보
    var trees = [Graph<Int>]()
    
    func topologicalSort() -> [Int] {
        // 결과를 담을 배열과 queue가 필요하다.
        var result = [Int]()
        var q = [Int]()
        
        // 진입차수가 0인 노드를 큐에 담는다.
        for i in 0..<n {
            print(i)
            if inDegree[i] == 0 { q.append(inDegree[i]) }
        }
        
        // 위상정렬은 총 n 번만 실행되어야 한다.
        for _ in 0..<n {
            
            // 큐가 비어있으면 사이클이 생긴 것이다.
            if q.isEmpty {
                print("사이클이 생겨버렸어요 ⁉️")
                return
            }
            
            // 큐에서 노드를 하나 꺼내고 인접 간선을 모두 제거한다.
            // 진입차수가 0인 노드를 큐에 담고를 반복한다.
            result.append(q.first!) // 위에서 확인하고 왔으므로 큐가 비어있지 않음이 보장된다.
            let node = q.removeFirst()
            
            for child in trees[node].childs {
                inDegree[child] -= 1
                if inDegree[child] == 0 {
                    // 새롭게 진입차수가 0 이 된 노드를 큐에 삽입한다.
                    q.append(child)
                }
            }
        }
        
        return result
    }

     

    시간복잡도

    마지막으로 위상정렬의 시간복잡도는 O(V + E) 입니다.

    앞서 잠깐 말씀드렸다시피 위상정렬은 딱 N번만 실행(V)되어야 합니다. 그리고 각 횟수에 간선을 모두 끊어내는 작업(E)이 들어가죠.

    따라서 O(V+E) 만으로 해결할 수 있습니다. 위상정렬을 사용할 수 있는 조건만 갖춰진다면 매우 빠른 강력한 알고리즘입니다.

     

    긴 글 읽어주셔서 감사합니다 :)

    'Algorithm > Theory' 카테고리의 다른 글

    Binary Search 이진탐색  (0) 2020.06.12
    Permutation 순열  (0) 2020.06.11
    Recursion 재귀  (0) 2020.06.11
    합병정렬 Merge sort  (0) 2020.05.14
    프림 알고리즘  (0) 2020.03.06

    댓글

Designed by Tistory.