[Algorithm] Graphs - Topological Sorting Algorithm

그레프 - 토폴로지 알고리즘

Posted by lim.Chuck on October 10, 2024

토폴로지 정렬 (Topological Sorting)

토폴로지 정렬(Topological Sorting)은 유향 비순환 그래프(DAG, Directed Acyclic Graph)에서 정점들을 순서대로 나열하는 방법입니다. 이 정렬 방식은 그래프의 간선이 u -> v일 때, 항상 정렬 순서에서 uv보다 앞에 나오는 것을 보장합니다. 즉, 그래프의 선행 관계를 유지하면서 모든 노드를 나열하는 것입니다.

주요 특징:

  • 유향 그래프에서 사용되며, 사이클이 없는 그래프에서만 동작합니다.
  • 프로세스 간의 의존성을 표현하거나, 과목 수강 순서와 같은 우선순위가 있는 작업을 나열할 때 사용됩니다.

JavaScript 예제 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 위상 정렬을 수행하는 함수 (깊이 우선 탐색 방식)
function topologicalSortUtil(v, visited, stack, graph) {
  visited[v] = true;

  // 현재 노드의 모든 인접 노드를 탐색
  for (let i = 0; i < graph[v].length; i++) {
    const adjacent = graph[v][i];
    if (!visited[adjacent]) {
      topologicalSortUtil(adjacent, visited, stack, graph);
    }
  }

  // 탐색이 완료된 노드를 스택에 삽입
  stack.push(v);
}

// 그래프에 대한 위상 정렬 함수
function topologicalSort(graph) {
  const stack = [];
  const visited = new Array(graph.length).fill(false);

  // 모든 노드에 대해 위상 정렬 수행
  for (let i = 0; i < graph.length; i++) {
    if (!visited[i]) {
      topologicalSortUtil(i, visited, stack, graph);
    }
  }

  // 스택에서 요소를 역순으로 출력하여 위상 정렬 순서 얻기
  while (stack.length) {
    console.log(stack.pop() + " ");
  }
}

// 그래프 정의 (유향 비순환 그래프)
const graph = [
  [2, 3], // 노드 0에서 2, 3으로 가는 간선
  [3], // 노드 1에서 3으로 가는 간선
  [3], // 노드 2에서 3으로 가는 간선
  [], // 노드 3은 종착점
];

topologicalSort(graph);

Rust 예제 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
use std::collections::VecDeque;

// 깊이 우선 탐색을 수행하여 위상 정렬 순서를 계산하는 함수
fn topological_sort_util(v: usize, visited: &mut Vec<bool>, stack: &mut VecDeque<usize>, graph: &Vec<Vec<usize>>) {
    visited[v] = true;

    // 현재 노드의 인접한 노드들을 모두 탐색
    for &adj in &graph[v] {
        if !visited[adj] {
            topological_sort_util(adj, visited, stack, graph);
        }
    }

    // 스택에 노드를 삽입 (후입선출 방식)
    stack.push_front(v);
}

// 위상 정렬을 수행하는 함수
fn topological_sort(graph: &Vec<Vec<usize>>) -> Vec<usize> {
    let mut stack = VecDeque::new();
    let mut visited = vec![false; graph.len()];

    // 각 노드에 대해 DFS를 수행
    for i in 0..graph.len() {
        if !visited[i] {
            topological_sort_util(i, &mut visited, &mut stack, graph);
        }
    }

    // 스택에 저장된 위상 정렬 순서를 벡터로 반환
    stack.into_iter().collect()
}

fn main() {
    // 그래프 정의 (유향 비순환 그래프)
    let graph = vec![
        vec![2, 3],    // 노드 0에서 2, 3으로 가는 간선
        vec![3],       // 노드 1에서 3으로 가는 간선
        vec![3],       // 노드 2에서 3으로 가는 간선
        vec![]         // 노드 3은 종착점
    ];

    let result = topological_sort(&graph);
    for node in result {
        print!("{} ", node);
    }
}

사용처:

  • 작업 스케줄링: 여러 작업 간의 의존 관계가 있는 프로젝트에서 각 작업을 어떤 순서로 해야 할지를 결정할 때 유용합니다.
  • 컴파일러 최적화: 컴파일러는 코드가 실행되기 전에 변수나 함수의 의존성을 분석하고, 어떤 순서로 코드를 실행해야 할지를 결정할 때 사용합니다.
  • 수강 과목 결정: 학문의 이수 조건이 있을 때, 어떤 과목을 먼저 들어야 할지 정하는데 유용합니다.

토폴로지 정렬은 의존 관계가 존재하는 여러 작업을 적절한 순서로 배치해야 할 때 필수적으로 사용됩니다.

Topological Sorting

Topological sorting is a way to arrange vertices in a directed acyclic graph (DAG) in a linear order, such that for every directed edge u -> v, vertex u comes before vertex v. It ensures that precedence relationships between vertices (or tasks) are preserved.

Key Characteristics:

  • Used for directed acyclic graphs (DAG).
  • Commonly applied to problems where tasks have dependencies, like task scheduling, course prerequisites, or process execution.

JavaScript Example Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Function to perform topological sorting using DFS
function topologicalSortUtil(v, visited, stack, graph) {
  visited[v] = true;

  // Visit all the adjacent vertices
  for (let i = 0; i < graph[v].length; i++) {
    const adjacent = graph[v][i];
    if (!visited[adjacent]) {
      topologicalSortUtil(adjacent, visited, stack, graph);
    }
  }

  // Push the current node to the stack after visiting all its adjacent nodes
  stack.push(v);
}

// Main function to perform topological sorting
function topologicalSort(graph) {
  const stack = [];
  const visited = new Array(graph.length).fill(false);

  // Perform the sort for each unvisited vertex
  for (let i = 0; i < graph.length; i++) {
    if (!visited[i]) {
      topologicalSortUtil(i, visited, stack, graph);
    }
  }

  // Print the topological order by reversing the stack
  while (stack.length) {
    console.log(stack.pop() + " ");
  }
}

// Graph representation (DAG)
const graph = [
  [2, 3], // Node 0 -> Nodes 2, 3
  [3], // Node 1 -> Node 3
  [3], // Node 2 -> Node 3
  [], // Node 3 has no outgoing edges
];

topologicalSort(graph);

Rust Example Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
use std::collections::VecDeque;

// Function to perform DFS for topological sorting
fn topological_sort_util(v: usize, visited: &mut Vec<bool>, stack: &mut VecDeque<usize>, graph: &Vec<Vec<usize>>) {
    visited[v] = true;

    // Visit all adjacent vertices
    for &adj in &graph[v] {
        if !visited[adj] {
            topological_sort_util(adj, visited, stack, graph);
        }
    }

    // Push the vertex to the stack once all its adjacent vertices are visited
    stack.push_front(v);
}

// Main function for topological sorting
fn topological_sort(graph: &Vec<Vec<usize>>) -> Vec<usize> {
    let mut stack = VecDeque::new();
    let mut visited = vec![false; graph.len()];

    // Call DFS for each unvisited vertex
    for i in 0..graph.len() {
        if !visited[i] {
            topological_sort_util(i, &mut visited, &mut stack, graph);
        }
    }

    // Convert the stack into a vector to return the topological order
    stack.into_iter().collect()
}

fn main() {
    // Define a directed acyclic graph (DAG)
    let graph = vec![
        vec![2, 3],    // Node 0 -> Nodes 2, 3
        vec![3],       // Node 1 -> Node 3
        vec![3],       // Node 2 -> Node 3
        vec![]         // Node 3 has no outgoing edges
    ];

    let result = topological_sort(&graph);
    for node in result {
        print!("{} ", node);
    }
}

Use Cases:

  • Task Scheduling: When tasks have dependencies, topological sorting helps determine the order in which tasks should be completed.
  • Compiler Optimization: Compilers analyze variable and function dependencies to determine the order of code execution.
  • Course Prerequisites: In education, topological sorting can help determine the sequence of courses to be taken based on prerequisite dependencies.

Topological sorting is essential when you need to organize tasks with dependencies into a valid sequence for execution.