使用 C# 代碼實現拓撲排序

0.參考資料

尊重他人的勞動成果,貼上參考的資料地址,本文僅做學習記錄之用。html

  1. https://www.codeproject.com/Articles/869059/Topological-sorting-in-Csharp
  2. https://songlee24.github.io/2015/05/07/topological-sorting/
  3. http://www.javashuo.com/article/p-vidabxub-dn.html

1.介紹

本身以前並無接觸過拓撲排序,頂多據說過拓撲圖。在寫前一篇文章的時候,看到 Abp 框架在處理模塊依賴項的時候使用了拓撲排序,來確保頂級節點始終是最早進行加載的。第一次看到以爲很神奇,看了一下維基百科頭也是略微大,本身的水平也是停留在冒泡排序的層次。ヽ(≧□≦)ノgit

看了第二篇參考資料才大體瞭解,在此記錄一下。github

2.原理

先來一個基本定義:算法

在圖論中,拓撲排序(Topological Sorting)是一個有向無環圖(DAG, Directed Acyclic Graph)的全部頂點的線性序列。且該序列必須知足下面兩個條件:框架

  1. 每一個頂點出現且只出現一次。
  2. 若存在一條從頂點 A 到頂點 B 的路徑,那麼在序列中頂點 A 出如今頂點 B 的前面。

有向無環圖(DAG)纔有拓撲排序,非DAG圖沒有拓撲排序一說。ide

例如,有一個集合它的依賴關係以下圖:學習

能夠看到他有一個依賴關係:3d

  1. Module D 依賴於 Module E 與 Module B 。
  2. Module E 依賴於 Module B 與 Module C 。
  3. Module B 依賴於 Module A 與 Module C 。
  4. Module C 依賴於 Module A 。
  5. Module A 無依賴 。

這個就是一個 DAG 圖,咱們要獲得它的拓撲排序,一個簡單的步驟以下:code

  1. 從 DAG 圖中選擇一個沒有前驅的頂點並輸出。
  2. 從 DAG 圖中刪除該頂點,以及以它爲起點的有向邊。
  3. 重複步驟 一、2 直到當前的 DAG 圖爲空,或者當前圖不存在無前驅的頂點爲止

按照以上步驟,咱們來進行一個排序試試。htm

最後的排序結果就是:

Module D -> Module E -> Module B -> Module C -> Module A

emmmm,其實一個有向無環圖能夠有一個或者多個拓撲序列的,由於有的時候會存在一種狀況,即如下這種狀況:

這個時候你就可能會有這兩種結果

D->E->B->C->F->A

D->E->B->F->C->A

由於 F 與 C 是平級的,他們初始化順序即使不一樣也沒有什麼影響,由於他們的依賴層級是一致的,不過細心的朋友可能會發現這個順序好像是反的,咱們還須要將其再反轉一次。

3.實現

上面這種方法僅適用於已知入度的時候,也就是說這些內容自己就是存在於一個有向無環圖之中的,若是按照以上方法進行拓撲排序,你須要維護一個入度爲 0 的隊列,而後每次迭代移除入度爲 0 頂點所指向的頂點入度。

例若有如下圖:

img

按照咱們以前的算法,

  1. 首先初始化隊列,將 5 與 4 這兩個入度爲 0 的頂點加入隊列當中。
  2. 執行 While 循環,條件是隊列不爲空。
  3. 以後首先拿出 4 。
  4. 而後針對其指向的頂點 0 與 頂點 1 的入度減去 1。
  5. 減去指向頂點入度的時候同時判斷,被減去入度的頂點其值是否爲 0 。
  6. 這裏 1 入度被減去 1 ,爲 0 ,添加到隊列。
  7. 0 頂點入度減去 1 ,爲 1。
  8. 隊列如今有 5 與 1 這兩個頂點,循環判斷隊列不爲空。
  9. 5 指向的頂點 0 入度 減去 1,頂點 0 入度爲 0 ,插入隊列。

這樣反覆循環,最終隊列所有清空,退出循環,獲得拓撲排序的結果4, 5, 2, 0, 3, 1 。

4.深度優先搜索實現

在參考資料 1 的代碼當中使用的是深度優先算法,它適用於有向無環圖。

有如下有向環圖 G2:

對上圖 G2 進行深度優先遍歷,首先從入度爲 0 的頂點 A 開始遍歷:

它的步驟以下:

  1. 訪問 A 。

  2. 訪問 B 。

  3. 訪問 C 。

    在訪問了 B 後應該是訪問 B 的另一個頂點,這裏能夠是隨機的也能夠是有序的,具體取決於你存儲的序列順序,這裏先訪問 C 。

  4. 訪問 E 。

  5. 訪問 D 。

    這裏訪問 D 是由於 B 已經被訪問過了,因此訪問頂點 D 。

  6. 訪問 F 。

    由於頂點 C 已經被訪問過,因此應該回溯訪問頂點 B 的另外一個有向邊指向的頂點 F 。

  7. 訪問 G 。

所以最後的訪問順序就是 A -> B -> C -> E -> D -> F -> G ,注意順序仍是不太對哦。

看起來跟以前的方法差很少,實現當中,其 Sort() 方法內部包含一個 visited 字典,用於標記已經訪問過的頂點,sorted 則是已經排序完成的集合列表。

在字典裏 Key 是頂點的值,其 value 值用來標識是否已經訪問完全部路徑,爲 true 則表示正在處理該頂點,爲 false 則表示已經處理完成。

如今咱們來寫實現吧:

public static IList<T> Sort<T>(IEnumerable<T> source, Func<T, IEnumerable<T>> getDependencies)
{
    var sorted = new List<T>();
    var visited = new Dictionary<T, bool>();

    foreach (var item in source)
    {
        Visit(item, getDependencies, sorted, visited);
    }

    return sorted;
}

public static void Visit<T>(T item, Func<T, IEnumerable<T>> getDependencies, List<T> sorted, Dictionary<T, bool> visited)
{
    bool inProcess;
    var alreadyVisited = visited.TryGetValue(item, out inProcess);

    // 若是已經訪問該頂點,則直接返回
    if (alreadyVisited)
    {
        // 若是處理的爲當前節點,則說明存在循環引用
        if (inProcess)
        {
            throw new ArgumentException("Cyclic dependency found.");
        }
    }
    else
    {
        // 正在處理當前頂點
        visited[item] = true;

        // 得到全部依賴項
        var dependencies = getDependencies(item);
        // 若是依賴項集合不爲空,遍歷訪問其依賴節點
        if (dependencies != null)
        {
            foreach (var dependency in dependencies)
            {
                // 遞歸遍歷訪問
                Visit(dependency, getDependencies, sorted, visited);
            }
        }

        // 處理完成置爲 false
        visited[item] = false;
        sorted.Add(item);
    }
}

頂點定義:

// Item 定義
public class Item
{
    // 條目名稱
    public string Name { get; private set; }
    // 依賴項
    public Item[] Dependencies { get; set; }

    public Item(string name, params Item[] dependencies)
    {
        Name = name;
        Dependencies = dependencies;
    }

    public override string ToString()
    {
        return Name;
    }
}

調用:

static void Main(string[] args)
{
    var moduleA = new Item("Module A");
    var moduleC = new Item("Module C", moduleA);
    var moduleB = new Item("Module B", moduleC);
    var moduleE = new Item("Module E", moduleB);
    var moduleD = new Item("Module D", moduleE);

    var unsorted = new[] { moduleE, moduleA, moduleD, moduleB, moduleC };

    var sorted = Sort(unsorted, x => x.Dependencies);

    foreach (var item in sorted)
    {
        Console.WriteLine(item.Name);
    }

    Console.ReadLine();
}

結果:

相關文章
相關標籤/搜索