大數據工作流任務調度--有向無環圖(DAG)之拓撲排序

拓撲排序(Topological Sorting)

回顧基礎知識:

1、圖的遍歷:
圖的遍歷是指從圖中的某一個頂點出發,按照某種搜索方法沿着圖中的邊對圖中的所有頂點訪問一次且僅訪問一次。注意樹是一種特殊的圖,所以樹的遍歷實際上也可以看作是一種特殊的圖的遍歷。
2、圖的遍歷主要有兩種算法:廣度優先搜索(Breadth First Search,BFS)和深度優先搜索(Depth First Search,DFS)。

  • [1] 深度優先搜索(DFS)
    深度優先搜索的搜索策略是儘可能深地搜索一個圖。基本思想是:首先訪問圖中某一未訪問的頂點V1,然後由V1出發,訪問與V1鄰接且未被訪問的任一頂點V2,再訪問與V2鄰接且未被訪問的任一頂點V3,……重複上述過程。當不能再繼續向下訪問(即孤立點)時,依次退回到最近被訪問的頂點,若它還有鄰接頂點未被訪問過,則從該點開始繼續上述搜索過程,直到圖中所有頂點均被訪問過爲止。

  • [2] 廣度優先搜索(BFS)
    廣度優先搜索的基本思想是:首先訪問起始頂點v,接着由v出發,依次訪問v的各個未訪問過的鄰接頂點w1,w2,…,wi,然後再依次訪問w1,w2,…,wi的所有未被訪問過的鄰接頂點;再從這些訪問過的頂點出發,再訪問它們所有未被訪問過的鄰接頂點……依次類推,直到圖中所有頂點都被訪問過爲止。
    舉例說明:
    在這裏插入圖片描述
    其BFS遍歷如下:1 2 5 3 4 6 7
    其DFS遍歷如下:1 2 3 4 5 6 7

接下來說正題:

維基百科上拓撲排序的定義爲:

對於任何有向無環圖(Directed Acyclic Graph,DAG)而言,其拓撲排序爲其所有結點的一個線性排序(同一個有向圖可能存在多個這樣的結點排序)。該排序滿足這樣的條件——對於圖中的任意兩個結點U和V,若存在一條有向邊從U指向V,則在拓撲排序中U一定出現在V前面。

通俗來講:拓撲排序是一個有向無環圖(DAG)的所有頂點的線性序列, 該序列必須滿足兩個條件:

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

如何找出它的拓撲排序呢?這裏說一種比較常用的方法:

  1. 從DAG圖中選擇一個入度爲0的頂點並輸出。
  2. 從圖中刪除該頂點和所有以它爲起點的有向邊。
  3. 重複1和2直到當前的DAG圖爲空或當前圖中不存在入度爲0的頂點爲止。後一種情況說明有向圖中必然存在環。
穿插一下:有向圖結點的入度(indegree)和出度(outdegree)的概念。
假設有向圖中不存在起點和終點爲同一結點的有向邊。
入度:設有向圖中有一結點V,其入度即爲當前所有從其他結點出發,終點爲V的的邊的數目。也就是所有指向V的有向邊的數目。
出度:設有向圖中有一結點V,其出度即爲當前所有起點爲V,指向其他結點的邊的數目。也就是所有由V發出的邊的數目。

例如下面這個DAG圖:
DAG圖
結點1的入度:0,出度:2
結點2的入度:1,出度:2
結點3的入度:2,出度:1
結點4的入度:2,出度:2
結點5的入度:2,出度:0

它的拓撲排序流程爲:
DAG圖拓撲排序輸出

於是,得到拓撲排序後的結果是: {1,2,4,3,5} 。
如果沒有結點2 —> 結點4的這個箭頭,那麼如下:

我們可以得到它的拓撲排序爲:{1,2,4,3,5} 或者 {1,4,2,3,5} ,即對同一DAG圖來說,它的拓撲排序結果可能存在多個

拓撲排序主要用來解決有向圖中的依賴問題。

在講到實現的時候,有必要插以下內容:

由此我們可以進一步得出一個改進的深度優先遍歷或廣度優先遍歷算法來完成拓撲排序。以廣度優先遍歷爲例,這一改進後的算法與普通的廣度優先遍歷唯一的區別在於我們應當保存每一個結點對應的入度,並在遍歷的每一層選取入度爲0的結點開始遍歷(而普通的廣度優先遍歷則無此限制,可以從該喫呢個任意一個結點開始遍歷)。這個算法描述如下:

初始化一個Map或者類似數據結構來保存每一個結點的入度。
對於圖中的每一個結點的子結點,將其子結點的入度加1。
選取入度爲0的任意一個結點開始遍歷,並將該節點加入輸出。
對於遍歷過的每個結點,更新其子結點的入度:將子結點的入度減1。
重複步驟3,直到遍歷完所有的結點。
如果無法遍歷完所有的結點,則意味着當前的圖不是有向無環圖。不存在拓撲排序。

廣度優先遍歷拓撲排序的核心Java代碼如下:

public class TopologicalSort {
  /**
   * 判斷是否有環及拓撲排序結果
   *
   * 有向無環圖(DAG)纔有拓撲(topological)排序
   * 廣度優先遍歷的主要做法:
   *    1、遍歷圖中所有的頂點,將入度爲0的頂點入隊列。
   *    2、從隊列中poll出一個頂點,更新該頂點的鄰接點的入度(減1),如果鄰接點的入度減1之後等於0,則將該鄰接點入隊列。
   *    3、一直執行第2步,直到隊列爲空。
   * 如果無法遍歷完所有的結點,則意味着當前的圖不是有向無環圖。不存在拓撲排序。
   *
   *
   * @return key返回的是狀態, 如果成功(無環)爲true, 失敗則有環, value爲拓撲排序結果(可能是其中一種)
   */
  private Map.Entry<Boolean, List<Vertex>> topologicalSort() {
	//入度爲0的結點隊列
    Queue<Vertex> zeroIndegreeVertexQueue = new LinkedList<>();
    //保存結果
    List<Vertex> topoResultList = new ArrayList<>();
    //保存入度不爲0的結點
    Map<Vertex, Integer> notZeroIndegreeVertexMap = new HashMap<>();

    //掃描所有的頂點,將入度爲0的頂點入隊列
    for (Map.Entry<Vertex, VertexInfo> vertices : verticesMap.entrySet()) {
      Vertex vertex = vertices.getKey();
      int inDegree = getIndegree(vertex);

      if (inDegree == 0) {
        zeroIndegreeVertexQueue.add(vertex);
        topoResultList.add(vertex);
      } else {
        notZeroIndegreeVertexMap.put(vertex, inDegree);
      }
    }
    
	//掃描完後,沒有入度爲0的結點,說明有環,直接返回
    if(zeroIndegreeVertexQueue.isEmpty()){
      return new AbstractMap.SimpleEntry(false, topoResultList);
    }

    //採用topology算法, 刪除入度爲0的結點和它的關聯邊
    while (!zeroIndegreeVertexQueue.isEmpty()) {
      Vertex v = zeroIndegreeVertexQueue.poll();
      //得到相鄰結點
      Set<Vertex> subsequentNodes = getSubsequentNodes(v);

      for (Vertex subsequentVertex : subsequentNodes) {

        Integer degree = notZeroIndegreeVertexMap.get(subsequentVertex);

        if(--degree == 0){
          topoResultList.add(subsequentVertex);
          zeroIndegreeVertexQueue.add(subsequentVertex);
          notZeroIndegreeVertexMap.remove(subsequentVertex);
        }else{
          notZeroIndegreeVertexMap.put(subsequentVertex, degree);
        }

      }
    }

    //notZeroIndegreeVertexMap如果爲空, 表示沒有環
    AbstractMap.SimpleEntry resultMap = new AbstractMap.SimpleEntry(notZeroIndegreeVertexMap.size() == 0 , topoResultList);
    return resultMap;

  }
}

注意輸出結果是該圖的拓撲排序序列之一。
每次在入度爲0的集合中取頂點,並沒有特殊的取出規則,取頂點的順序不同會得到不同的拓撲排序序列(如果該圖有多種排序序列)。

由於輸出每個頂點的同時還要刪除以它爲起點的邊。如果圖有V個頂點,E條邊,則一般該算法的時間複雜度爲O(V+E)。這裏實現的算法最終key返回的是狀態, 如果成功(無環)爲true, 失敗則有環, 無環時value爲拓撲排序結果(可能是其中一種)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章