Thuật toán sắp xếp tôpô: Python, C++ Ví dụ

⚡ Tóm tắt thông minh

Thuật toán sắp xếp tôpô (Topological Sort) sắp xếp các nút của đồ thị có hướng không chu trình sao cho mỗi nút xuất hiện trước các nút mà nó trỏ đến, sử dụng thuật toán Kahn để liên tục chọn các nút có bậc vào bằng không.

  • 📐 Định nghĩa: Thuật toán sắp xếp tôpô tạo ra một thứ tự tuyến tính của các đỉnh đồ thị có hướng không chu trình (DAG) trong đó mọi cạnh có hướng (u, v) đều có u đứng trước v.
  • 🔁 Thuật toán Kahn: Liên tục chọn một nút có số cạnh đi vào bằng 0, thêm nó vào thứ tự và giảm bậc đi vào của các nút lân cận của nó.
  • 🚫 Chu kỳ bị chặn: Đồ thị chứa chu trình không thể được sắp xếp theo tôpô, vì không có nút nào đạt đến bậc vào bằng 0 bên trong chu trình.
  • 💻 Mã Code: C++ và Python Các phương pháp triển khai sử dụng hàng đợi cộng với mảng bậc vào để tính toán thứ tự trong thời gian O(V + E).
  • 📊 Phức tạp: Độ phức tạp thời gian là O(V + E) và độ phức tạp không gian là O(V), trong đó V là số đỉnh và E là số cạnh.
  • 🛠️ Ứng dụng Lập lịch tác vụ và xây dựng, giải quyết phụ thuộc gói (apt, npm), phát hiện tắc nghẽn và điều kiện tiên quyết của khóa học đều sử dụng thứ tự tôpô.

Thuật toán sắp xếp cấu trúc liên kết

Thuật toán sắp xếp tô pô là gì?

Sắp xếp tôpô còn được gọi là thuật toán Kahn và là một thuật toán sắp xếp phổ biến. Sử dụng biểu đồ có hướng làm đầu vào, Sắp xếp tôpô sẽ sắp xếp các nút sao cho mỗi nút xuất hiện trước nút mà nó trỏ tới.

Thuật toán này được áp dụng trên đồ thị có hướng không chu trình (DAG) sao cho mỗi nút xuất hiện trong mảng đã sắp xếp trước tất cả các nút khác mà nó trỏ đến. Thuật toán này tuân theo một số quy tắc lặp đi lặp lại cho đến khi quá trình sắp xếp hoàn tất.

Để đơn giản hơn, hãy xem ví dụ sau:

Đồ thị có hướng

Đồ thị có hướng

Ở đây, ta thấy rằng “A” không có bậc vào. Bậc vào nghĩa là cạnh trỏ đến một nút. “B” và “C” có điều kiện tiên quyết là “A”, sau đó “E” có điều kiện tiên quyết là các nút “D” và “F”. Một số nút phụ thuộc vào các nút khác.

Dưới đây là một cách biểu diễn khác của đồ thị trên:

Sự phụ thuộc của mỗi nút

Sự phụ thuộc của mỗi nút (Sắp xếp tuyến tính)

Vì vậy, khi chúng ta chuyển DAG (Biểu đồ tuần hoàn có hướng) sang sắp xếp tôpô, nó sẽ cung cấp cho chúng ta một mảng có thứ tự tuyến tính, trong đó phần tử đầu tiên không có phần phụ thuộc.

Thuật toán sắp xếp cấu trúc liên kết

Dưới đây là các bước để thực hiện việc này:

Bước 1) Tìm nút có các cạnh đến bằng 0, một nút có độ bằng 0.

Bước 2) Lưu trữ nút có bậc vào bằng 0 đó vào Hàng đợi hoặc Ngăn xếp và xóa nút đó khỏi Đồ thị.

Bước 3) Sau đó, xóa cạnh đi ra từ nút đó. Thao tác này sẽ làm giảm số bậc vào của nút tiếp theo.

Thứ tự tôpô yêu cầu cấu trúc dữ liệu đồ thị không được có bất kỳ chu trình nào. Một đồ thị sẽ được coi là đồ thị có hướng không chu trình (DAG) nếu nó đáp ứng các yêu cầu sau:

  • Một hoặc nhiều nút có giá trị bậc bằng 0.
  • Đồ thị không chứa bất kỳ chu trình nào.

Chừng nào đồ thị vẫn còn các nút và đồ thị vẫn là một đồ thị có hướng không chu trình (DAG), chúng ta sẽ thực hiện ba bước trên. Nếu không, thuật toán sẽ rơi vào vòng phụ thuộc và thuật toán Kahn sẽ không thể tìm được nút có bậc vào bằng không.

Cách sắp xếp cấu trúc liên kết hoạt động

Ở đây, chúng ta sẽ sử dụng "Thuật toán Kahn" để sắp xếp topo. Giả sử ta có đồ thị sau:

Công việc sắp xếp tôpô

Dưới đây là các bước thực hiện thuật toán Kahn:

Bước 1) Tính toán cạnh không độ hoặc cạnh tới của tất cả các nút trong Biểu đồ.

Lưu ý:

  • Indegree có nghĩa là các cạnh được định hướng trỏ đến nút.
  • Mức độ ngoài có nghĩa là các cạnh được định hướng đến từ một nút.

Đây là bậc vào và bậc ra của đồ thị trên:

Bậc vào và bậc ra

Bước 2) Tìm nút có bậc vào bằng 0 hoặc không có cạnh đi vào. Nút có bậc vào bằng 0 nghĩa là không có cạnh nào đi về phía nút đó. Nút “A” có bậc vào bằng 0, nghĩa là không có cạnh nào trỏ đến nút “A”. Vì vậy, chúng ta sẽ thực hiện các bước sau:

  • Xóa nút này và các cạnh đi ra của nó.
  • Đặt nút vào Hàng đợi để đặt hàng.
  • Cập nhật số lượng liên kết vào của nút lân cận của “A”.

Công việc sắp xếp tôpô

Bước 3) Chúng ta cần tìm một nút có giá trị bậc vào bằng không. Trong ví dụ này, “B” và “C” có bậc vào bằng không. Ở đây, chúng ta có thể chọn một trong hai. Hãy chọn “B” và xóa nó khỏi đồ thị. Sau đó cập nhật giá trị bậc vào của các nút khác. Sau khi thực hiện các thao tác này, đồ thị và hàng đợi của chúng ta sẽ trông như sau:

Công việc sắp xếp tôpô

Bước 4) Nút “C” không có cạnh đi vào. Vì vậy, chúng ta sẽ xóa nút “C” khỏi đồ thị và đẩy nó vào hàng đợi. Chúng ta cũng có thể xóa cạnh đi ra từ “C”. Bây giờ, đồ thị của chúng ta sẽ trông như thế này:

Công việc sắp xếp tôpô

Bước 5) Ta thấy rằng các nút “D” và “F” có bậc vào bằng 0. Ta sẽ lấy một nút và đưa nó vào hàng đợi. Trước tiên, hãy lấy “D” ra. Khi đó, bậc vào của nút “E” sẽ là 1. Lúc này, sẽ không còn nút nào nối từ D đến E nữa. Ta cần làm tương tự với nút “F”, và kết quả sẽ như sau:

Công việc sắp xếp tôpô

Bước 6) Độ vào (cạnh đi vào) và độ ra (cạnh đi ra) của nút “E” đã trở thành 0. Như vậy, chúng ta đã đáp ứng tất cả các điều kiện tiên quyết cho nút “E”. Tại đây, chúng ta sẽ đặt “E” vào cuối hàng đợi. Vì vậy, chúng ta không còn nút nào nữa và thuật toán kết thúc tại đây.

Công việc sắp xếp tôpô

Biệt danh Code để sắp xếp tôpô

Đây là mã giả cho thuật toán sắp xếp tôpô sử dụng thuật toán Kahn.

function TopologicalSort( Graph G ):
  for each node in G:
    calculate the indegree
  start = Node with 0 indegree
  G.remove(start)
  topological_list = [start]
  while node with 0 indegree present:
    topological_list.append(node)
    G.remove(node)
    // Update indegree of present nodes
  return topological_list

Sắp xếp cấu trúc liên kết cũng có thể được thực hiện bằng cách sử dụng DFS (Tìm kiếm sâu đầu tiên) phương pháp. Tuy nhiên, cách tiếp cận đó là phương pháp đệ quy. Thuật toán của Kahn hiệu quả hơn phương pháp DFS.

C++ Thực hiện sắp xếp tôpô

#include<bits/stdc++.h>
using namespace std;
class graph{
  int vertices;
  list<int> *adjecentList;
public:
  graph(int vertices){
    this->vertices = vertices;
    adjecentList = new list<int>[vertices];
  }
  void createEdge(int u, int v){
    adjecentList[u].push_back(v);
  }
  void TopologicalSort(){
    // filling the vector with zero initially
    vector<int> indegree_count(vertices,0);

    for(int i=0;i<vertices;i++){
      list<int>::iterator itr;
      for(itr=adjecentList[i].begin(); itr!=adjecentList[i].end();itr++){
        indegree_count[*itr]++;
      }
    }
    queue<int> Q;
    for(int i=0; i<vertices;i++){
      if(indegree_count[i]==0){
        Q.push(i);
      }
    }
    int visited_node = 0;
    vector<int> order;
    while(!Q.empty()){
      int u = Q.front();
      Q.pop();
      order.push_back(u);

      list<int>::iterator itr;
      for(itr=adjecentList[u].begin(); itr!=adjecentList[u].end();itr++){
        if(--indegree_count[*itr]==0){
          Q.push(*itr);
        }
      }
      visited_node++;
    }
    if(visited_node!=vertices){
      cout<<"There's a cycle present in the Graph.\nGiven graph is not DAG"<<endl;
      return;
    }
    for(int i=0; i<order.size();i++){
      cout<<order[i]<<"\t";
    }
  }
};
int main(){
  graph G(6);
  G.createEdge(0,1);
  G.createEdge(0,2);
  G.createEdge(1,3);
  G.createEdge(1,5);
  G.createEdge(2,3);
  G.createEdge(2,5);
  G.createEdge(3,4);
  G.createEdge(5,4);
  G.TopologicalSort();
}

Đầu ra

0       1       2       3       5       4

Python Thực hiện sắp xếp tôpô

from collections import defaultdict
class graph:
    def __init__(self, vertices):
        self.adjacencyList = defaultdict(list)
        self.Vertices = vertices  # No. of vertices
    # function to add an edge to adjacencyList
    def createEdge(self, u, v):
        self.adjacencyList[u].append(v)
    # The function to do Topological Sort.
    def topologicalSort(self):
        total_indegree = [0]*(self.Vertices)
        for i in self.adjacencyList:
            for j in self.adjacencyList[i]:
                total_indegree[j] += 1
        queue = []
        for i in range(self.Vertices):
            if total_indegree[i] == 0:
                queue.append(i)
        visited_node = 0
        order = []
        while queue:
            u = queue.pop(0)
            order.append(u)
            for i in self.adjacencyList[u]:
                total_indegree[i] -= 1

                if total_indegree[i] == 0:
                    queue.append(i)
            visited_node += 1
        if visited_node != self.Vertices:
            print("There's a cycle present in the Graph.\nGiven graph is not DAG")
        else:
            print(order)
G = graph(6)
G.createEdge(0,1)
G.createEdge(0,2)
G.createEdge(1,3)
G.createEdge(1,5)
G.createEdge(2,3)
G.createEdge(2,5)
G.createEdge(3,4)
G.createEdge(5,4)
G.topologicalSort()

Đầu ra

[0, 1, 2, 3, 5, 4]

Đồ thị tuần hoàn của thuật toán sắp xếp tôpô

Đồ thị chứa chu trình không thể được sắp thứ tự về mặt tôpô, vì đồ thị có chu trình thể hiện sự phụ thuộc theo kiểu chu trình. Ví dụ, hãy xem đồ thị này:

Đồ thị tuần hoàn của thuật toán sắp xếp tôpô

Đồ thị này không phải là đồ thị có hướng không chu trình (DAG) vì A, B và C tạo thành một chu trình. Nếu để ý, bạn sẽ thấy không có nút nào có bậc vào bằng 0. Theo thuật toán Kahn, nếu phân tích đồ thị trên:

  • Tìm một nút có độ bằng 0 (không có cạnh nào đến).
  • Hãy xóa nút đó khỏi đồ thị và đẩy nó vào hàng đợi. Tuy nhiên, trong đồ thị trên, không có nút nào có bậc vào bằng 0. Mỗi nút đều có giá trị bậc vào lớn hơn 0.
  • Trả về hàng đợi rỗng vì không tìm thấy nút nào có bậc vào bằng không.

Chúng ta có thể phát hiện chu kỳ bằng cách sử dụng thứ tự tôpô theo các bước sau:

Bước 1) Thực hiện sắp xếp topo.

Bước 2) Tính tổng số phần tử trong danh sách được sắp xếp theo cấu trúc liên kết.

Bước 3) Nếu số lượng phần tử bằng tổng số đỉnh thì không có chu trình.

Bước 4) Nếu nó không bằng số đỉnh, thì cấu trúc dữ liệu đồ thị đã cho có ít nhất một chu trình.

Phân tích độ phức tạp của sắp xếp theo topo

Có hai loại độ phức tạp trong thuật toán. Đó là:

  1. Thời gian phức tạp
  2. Không gian phức tạp

Những độ phức tạp này được biểu diễn bằng một hàm cung cấp độ phức tạp chung.

Độ phức tạp về thời gian: Độ phức tạp thời gian của thuật toán Sắp xếp tôpô là như nhau. Có các trường hợp xấu nhất, trung bình và tốt nhất về độ phức tạp thời gian. Độ phức tạp thời gian của thuật toán Sắp xếp tôpô là O(E + V), trong đó E là số cạnh trong đồ thị và V là số đỉnh trong đồ thị.

Hãy cùng nhau vượt qua sự phức tạp này:

Bước 1) Lúc đầu, chúng tôi sẽ tính toán tất cả các mức độ. Để làm điều đó, chúng ta cần đi qua tất cả các cạnh và ban đầu, chúng ta sẽ gán tất cả độ góc của đỉnh V bằng 0. Vì vậy, các bước tăng dần mà chúng tôi hoàn thành sẽ là O(V+E).

Bước 2) Chúng ta sẽ tìm thấy nút có giá trị độ bằng 0. Chúng ta cần tìm kiếm từ số V của đỉnh. Vì vậy, các bước hoàn thành sẽ là O(V).

Bước 3) Đối với mỗi nút có độ bằng 0, chúng tôi sẽ loại bỏ nút đó và giảm độ. Việc thực hiện thao tác này cho tất cả các nút sẽ mất O(E).

Bước 4) Cuối cùng, chúng ta sẽ kiểm tra xem có chu kỳ nào hay không. Chúng ta sẽ kiểm tra xem tổng số phần tử trong mảng đã sắp xếp có bằng tổng số nút hay không. Nó sẽ mất O (1).

Như vậy, đây là độ phức tạp thời gian riêng lẻ cho mỗi bước của quá trình sắp xếp topo hoặc thứ tự topo. Ta có thể nói rằng độ phức tạp thời gian từ phép tính trên sẽ là O(V + E); ở đây, O là hàm độ phức tạp.

Không gian phức tạp: Chúng tôi cần không gian O(V) để chạy thuật toán sắp xếp tôpô. Dưới đây là các bước mà chúng tôi cần không gian cho chương trình:

  • Chúng tôi phải tính toán tất cả mức độ của các nút có trong Biểu đồ. Vì Biểu đồ có tổng số nút V nên chúng ta cần tạo một mảng có kích thước V. Vì vậy, không gian cần thiết là O(V).
  • Cấu trúc dữ liệu Hàng đợi được sử dụng để lưu trữ nút có mức độ bằng 0. Chúng tôi đã xóa các nút có độ bằng 0 khỏi Biểu đồ ban đầu và đặt chúng vào Hàng đợi. Đối với điều này, không gian cần thiết là O(V).
  • Mảng được đặt tên là “order”, lưu trữ các nút theo thứ tự tôpô. Điều đó cũng đòi hỏi O(V) khoảng trống.

Đây là độ phức tạp không gian riêng lẻ. Vì vậy, chúng ta cần tối đa hóa các không gian này trong thời gian chạy. Độ phức tạp không gian được ký hiệu là O(V), trong đó V là số lượng đỉnh trong đồ thị.

Ứng dụng sắp xếp tôpô

Thuật toán sắp xếp tôpô có rất nhiều ứng dụng. Dưới đây là một số ví dụ:

  • Nó được sử dụng khi một Operahệ thống ting cần thực hiện việc phân bổ nguồn lực.
  • Tìm chu trình trong đồ thị. Chúng ta có thể kiểm tra xem đồ thị có phải là đồ thị có hướng không chu trình (DAG) hay không bằng cách sử dụng thuật toán sắp xếp tôpô.
  • Thứ tự câu trong các ứng dụng tự động hoàn thành.
  • Nó được dùng để phát hiện bế tắc.
  • Các loại lập lịch trình hoặc lập lịch khóa học khác nhau sử dụng thuật toán sắp xếp topo.
  • Giải quyết sự phụ thuộc. Ví dụ: nếu bạn cố gắng cài đặt một gói, gói đó cũng có thể cần các gói khác. Thứ tự cấu trúc liên kết tìm ra tất cả các gói cần thiết để cài đặt gói hiện tại.
  • Linux sử dụng sắp xếp cấu trúc liên kết trong “apt” để kiểm tra sự phụ thuộc của các gói.

Câu Hỏi Thường Gặp

Thuật toán sắp xếp tôpô tạo ra một thứ tự tuyến tính các đỉnh của đồ thị có hướng không chu trình (DAG) sao cho với mọi cạnh có hướng từ u đến v, u xuất hiện trước v trong thứ tự đó.

Bất kỳ chu trình nào cũng giam giữ mọi nút trong đó với bậc vào khác 0 và không bao giờ giảm xuống 0, do đó thuật toán Kahn không thể chọn nút tiếp theo. Một thứ tự tôpô hợp lệ yêu cầu một đồ thị có hướng không chu trình.

Thuật toán Kahn sử dụng hàng đợi và bộ đếm bậc vào một cách lặp đi lặp lại. Thuật toán sắp xếp topo dựa trên DFS duyệt đệ quy qua đồ thị và đẩy các nút đã hoàn thành vào ngăn xếp. Cả hai đều chạy trong độ phức tạp O(V + E).

Độ phức tạp thời gian là O(V + E) vì mỗi đỉnh và cạnh được xử lý một lần. Độ phức tạp không gian là O(V) đối với mảng bậc vào, hàng đợi và mảng thứ tự đầu ra.

Đúng vậy. Khi hai hoặc nhiều nút có bậc vào bằng 0 tại cùng một bước, bất kỳ nút nào cũng có thể được chọn trước. Thứ tự chọn khác nhau sẽ tạo ra các thứ tự tôpô hợp lệ khác nhau của cùng một đồ thị có hướng không chu trình (DAG).

Các trình quản lý gói như apt, npm và pip sử dụng thứ tự tôpô để giải quyết các phụ thuộc. Hệ thống xây dựng, trình lập lịch tác vụ và trình lập kế hoạch điều kiện tiên quyết khóa học cũng dựa vào nó.

Các framework học máy như TensorFlow và PyTorSắp xếp đồ thị tính toán theo thứ tự topo để lên lịch các lượt truyền tiến và truyền ngược. Mạng Bayes cũng yêu cầu một thứ tự topo trên các biến.

Đúng vậy. Các công cụ hỗ trợ AI như GitHub Copilot tạo ra mã mẫu cho thuật toán Kahn. C++, Python, hoặc là JavaCác nhà phát triển vẫn cần xác minh việc phát hiện chu kỳ và xử lý hàng đợi chính xác.

Tóm tắt bài viết này với: