2015-04-17 26 views
11

當一個部署項目包含第二個部署項目的項目輸出並且第二個項目包含第一個項目的輸出時,通常會發生此錯誤。在C#中,如何查找循環依賴鏈?

我有一個方法來檢查循環依賴。在輸入中,我們有一個字典,其中包含例如<"A", < "B", "C" >><"B", < "A", "D" >>,這意味着A取決於BC,我們對A->B有循環依賴關係。

但通常我們有一個更復雜的情況,有一個依賴鏈。 如何修改這個方法來查找一個依賴鏈?例如,我想要一個包含鏈A->B->A的變量,而不是類A與類B有衝突。

private void FindDependency(IDictionary<string, IEnumerable<string>> serviceDependence) 
+0

你有什麼試過?爲什麼你的算法不工作?它有什麼問題?我們不是在這裏爲你寫代碼。 –

+1

@ThomasWeller我更新了我的代碼。但它運作緩慢 – Anatoly

+0

拓撲排序可以幫助http://en.wikipedia.org/wiki/Topological_sorting –

回答

18

在圖中查找週期的簡單方法是使用遞歸深度優先圖着色算法,其中節點標記爲「訪問」或「訪問」。如果在訪問一個節點時發現它已經處於「訪問」狀態,那麼就有一個循環。標記爲「已訪問」的節點可以跳過。例如:

public class DependencyExtensions 
{ 
    enum VisitState 
    { 
     NotVisited, 
     Visiting, 
     Visited 
    }; 

    public static TValue ValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue) 
    { 
     TValue value; 
     if (dictionary.TryGetValue(key, out value)) 
      return value; 
     return defaultValue; 
    } 

    static void DepthFirstSearch<T>(T node, Func<T, IEnumerable<T>> lookup, List<T> parents, Dictionary<T, VisitState> visited, List<List<T>> cycles) 
    { 
     var state = visited.ValueOrDefault(node, VisitState.NotVisited); 
     if (state == VisitState.Visited) 
      return; 
     else if (state == VisitState.Visiting) 
     { 
      // Do not report nodes not included in the cycle. 
      cycles.Add(parents.Concat(new[] { node }).SkipWhile(parent => !EqualityComparer<T>.Default.Equals(parent, node)).ToList()); 
     } 
     else 
     { 
      visited[node] = VisitState.Visiting; 
      parents.Add(node); 
      foreach (var child in lookup(node)) 
       DepthFirstSearch(child, lookup, parents, visited, cycles); 
      parents.RemoveAt(parents.Count - 1); 
      visited[node] = VisitState.Visited; 
     } 
    } 

    public static List<List<T>> FindCycles<T>(this IEnumerable<T> nodes, Func<T, IEnumerable<T>> edges) 
    { 
     var cycles = new List<List<T>>(); 
     var visited = new Dictionary<T, VisitState>(); 
     foreach (var node in nodes) 
      DepthFirstSearch(node, edges, new List<T>(), visited, cycles); 
     return cycles; 
    } 

    public static List<List<T>> FindCycles<T, TValueList>(this IDictionary<T, TValueList> listDictionary) 
     where TValueList : class, IEnumerable<T> 
    { 
     return listDictionary.Keys.FindCycles(key => listDictionary.ValueOrDefault(key, null) ?? Enumerable.Empty<T>()); 
    } 
} 

然後,你可以使用它像:

 var serviceDependence = new Dictionary<string, List<string>> 
     { 
      { "A", new List<string> { "A" }}, 
      { "B", new List<string> { "C", "D" }}, 
      { "D", new List<string> { "E" }}, 
      { "E", new List<string> { "F", "Q" }}, 
      { "F", new List<string> { "D" }}, 
     }; 
     var cycles = serviceDependence.FindCycles(); 
     Debug.WriteLine(JsonConvert.SerializeObject(cycles, Formatting.Indented)); 
     foreach (var cycle in cycles) 
     { 
      serviceDependence[cycle[cycle.Count - 2]].Remove(cycle[cycle.Count - 1]); 
     } 
     Debug.Assert(serviceDependence.FindCycles().Count == 0); 

更新

您的問題已更新請求「最有效的算法」尋找循環依賴。原始答案中的代碼是遞歸的,因此對於數千個級別的依賴關係鏈,可能有StackOverflowException。這裏有一個非遞歸版本明確的堆棧變量:

public static class DependencyExtensions 
{ 
    enum VisitState 
    { 
     NotVisited, 
     Visiting, 
     Visited 
    }; 

    public static TValue ValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue) 
    { 
     TValue value; 
     if (dictionary.TryGetValue(key, out value)) 
      return value; 
     return defaultValue; 
    } 

    private static void TryPush<T>(T node, Func<T, IEnumerable<T>> lookup, Stack<KeyValuePair<T, IEnumerator<T>>> stack, Dictionary<T, VisitState> visited, List<List<T>> cycles) 
    { 
     var state = visited.ValueOrDefault(node, VisitState.NotVisited); 
     if (state == VisitState.Visited) 
      return; 
     else if (state == VisitState.Visiting) 
     { 
      Debug.Assert(stack.Count > 0); 
      var list = stack.Select(pair => pair.Key).TakeWhile(parent => !EqualityComparer<T>.Default.Equals(parent, node)).ToList(); 
      list.Add(node); 
      list.Reverse(); 
      list.Add(node); 
      cycles.Add(list); 
     } 
     else 
     { 
      visited[node] = VisitState.Visiting; 
      stack.Push(new KeyValuePair<T, IEnumerator<T>>(node, lookup(node).GetEnumerator())); 
     } 
    } 

    static List<List<T>> FindCycles<T>(T root, Func<T, IEnumerable<T>> lookup, Dictionary<T, VisitState> visited) 
    { 
     var stack = new Stack<KeyValuePair<T, IEnumerator<T>>>(); 
     var cycles = new List<List<T>>(); 

     TryPush(root, lookup, stack, visited, cycles); 
     while (stack.Count > 0) 
     { 
      var pair = stack.Peek(); 
      if (!pair.Value.MoveNext()) 
      { 
       stack.Pop();      
       visited[pair.Key] = VisitState.Visited; 
       pair.Value.Dispose(); 
      } 
      else 
      { 
       TryPush(pair.Value.Current, lookup, stack, visited, cycles); 
      } 
     } 
     return cycles; 
    } 

    public static List<List<T>> FindCycles<T>(this IEnumerable<T> nodes, Func<T, IEnumerable<T>> edges) 
    { 
     var cycles = new List<List<T>>(); 
     var visited = new Dictionary<T, VisitState>(); 
     foreach (var node in nodes) 
      cycles.AddRange(FindCycles(node, edges, visited)); 
     return cycles; 
    } 

    public static List<List<T>> FindCycles<T, TValueList>(this IDictionary<T, TValueList> listDictionary) 
     where TValueList : class, IEnumerable<T> 
    { 
     return listDictionary.Keys.FindCycles(key => listDictionary.ValueOrDefault(key, null) ?? Enumerable.Empty<T>()); 
    } 
} 

這應該是合理有效的N*log(N) + E其中N是節點的數量和E是邊數。 Log(N)來自構建visited哈希表,可以通過使每個節點記住它的來消除。這似乎是合理的表現;在下面的測試工具,時間找到平均長度4393的17897次循環與125603只總依賴10000個節點是圍繞10.2秒:

public class TestClass 
{ 
    public static void TestBig() 
    { 
     var elapsed = TestBig(10000); 
     Debug.WriteLine(elapsed.ToString()); 
    } 

    static string GetName(int i) 
    { 
     return "ServiceDependence" + i.ToString(); 
    } 

    public static TimeSpan TestBig(int count) 
    { 
     var serviceDependence = new Dictionary<string, List<string>>(); 
     for (int iItem = 0; iItem < count; iItem++) 
     { 
      var name = GetName(iItem); 
      // Add several forward references. 
      for (int iRef = iItem - 1; iRef > 0; iRef = iRef/2) 
       serviceDependence.Add(name, GetName(iRef)); 
      // Add some backwards references. 
      if (iItem > 0 && (iItem % 5 == 0)) 
       serviceDependence.Add(name, GetName(iItem + 5)); 
     } 

     // Add one backwards reference that will create some extremely long cycles. 
     serviceDependence.Add(GetName(1), GetName(count - 1)); 

     List<List<string>> cycles; 

     var stopwatch = new Stopwatch(); 
     stopwatch.Start(); 
     try 
     { 
      cycles = serviceDependence.FindCycles(); 
     } 
     finally 
     { 
      stopwatch.Stop(); 
     } 

     var elapsed = stopwatch.Elapsed; 

     var averageLength = cycles.Average(l => (double)l.Count); 
     var total = serviceDependence.Values.Sum(l => l.Count); 

     foreach (var cycle in cycles) 
     { 
      serviceDependence[cycle[cycle.Count - 2]].Remove(cycle[cycle.Count - 1]); 
     } 
     Debug.Assert(serviceDependence.FindCycles().Count == 0); 

     Console.WriteLine(string.Format("Time to find {0} cycles of average length {1} in {2} nodes with {3} total dependencies: {4}", cycles.Count, averageLength, count, total, elapsed)); 
     Console.ReadLine(); 
     System.Environment.Exit(0); 

     return elapsed; 
    } 
} 
+0

我怎樣才能調用FindCycles()沒有參數? – Anatoly

+2

@Anatoly - 我將它實現爲[擴展方法](https://msdn.microsoft.com/en-us/library/bb383977.aspx),它看起來像是IDictionary >'。 – dbc

+1

@Anatoly - 答案更新了一些性能信息。 – dbc

6

構建一個包含每個輸入的所有直接依賴關係的字典。對於其中的每一個,添加所有獨特的間接依賴關係(例如,遍歷給定項目的每個依賴關係,如果父項不存在,則添加它)。重複,只要你至少對字典進行了一次更改。如果有一個項目本身存在依賴關係,那麼它就是一個週期性的依賴關係:)

這當然是相對低效的,但它非常簡單易懂。如果您正在創建一個編譯器,那麼您可能只需構建一個所有依賴關係的有向圖,並在其中搜索路徑 - 您可以找到大量現成的算法來查找有向圖中的路徑。

+0

你能寫一個代碼嗎?或改變我的? – Anatoly

+3

@Anatoly:改變你的代碼?你的1行幾乎沒有... –

+1

@ThomasWeller是的,我更新了 – Anatoly

0

拓撲排序是做到這一點的方法。我有一個在Vb.net的實現here