最近在做實際項目中遇到了一個問題,如何判斷一個層級結構的圖是否存在循環引用。剛開始想到了方法是用遞歸進行判斷,后來想到大學學過的拓撲排序可以解決該問題,於是翻了下數據結構這本書,閱讀了園友的文章,根據自己的理解寫下了這篇隨筆。
閱讀目錄
拓撲排序介紹
百度百科定義:
對一個有向無環圖(Directed Acyclic Graph簡稱DAG)G進行拓撲排序,是將G中所有頂點排成一個線性序列,使得圖中任意一對頂點u和v,若邊(u,v)∈E(G),則u在線性序列中出現在v之前。通常,這樣的線性序列稱為滿足拓撲次序(Topological Order)的序列,簡稱拓撲序列。簡單的說,由某個集合上的一個偏序得到該集合上的一個全序,這個操作稱之為拓撲排序。
上面的定義看完可能不知道是什么意思,舉兩個實際的例子就明白了。
1.大學課程排序
大學課程的學習是有先后順序的,C語言是基礎,數據結構依賴於C語言,其它課程也有類似依賴關系。這樣的一個課程安排是怎么實現的呢?
2.VS項目編譯順序
假設VS中有三個項目A,B,C,它們的關系如下圖。VS編譯器是如何判斷三個項目的編譯順序的呢?
問題引入及算法實現
這次實際項目中碰到的問題可以歸納為控件聯動選擇,即常見的省份,城市,地區聯動。為了實現通用的下拉連dog,設計了一套表結構,最終保存數據如下。
看到這里也許你不明白這個和拓撲排序能扯上什么關系,假如省份下拉又依賴於地區下拉,那這樣就會形成一個死循環。為了避免這樣的情況需要在數據保存時,校驗是否存在閉環。
下面給出,解決上述問題的兩種算法。
1.遞歸判斷
步驟如下
(1)找當前節點的父級節點(也可以叫依賴的節點)
(2)父級節點不為為空且不等於當前節點自己,則尋找父級節點對應的父級節點
(3)重復1,2。最終找到的節點=自己 ,則存在閉環,否則不存在
代碼實現
首先定義了一個類似的結構
public class Node { /// <summary> /// 當前節點ID /// </summary> public int Key { get; set; } /// <summary> /// 父級節點ID /// </summary> public int? Parent { get; set; } }
/// <summary> /// 遞歸判斷是否存在循環引用 /// </summary> public class RecursionSort { /// <summary> /// 遞歸判斷是否存在循環引用 /// </summary> public static bool CheckRecursion(List<Node> list) { foreach (var node in list) { if (RecursionSort.CheckRecursion(node.Key,node, list)) { return true; } } return false; } /// <summary> /// 遞歸判斷是否存在循環引用 /// </summary> /// <param name="list"></param> /// <returns></returns> private static bool CheckRecursion(int key,Node curNode, List<Node> list) { if (curNode.Parent == key) { return true; } //尋找父級節點對應的父級節點信息 if (curNode.Parent != null) { Node pNode = list.Where(e => e.Key == curNode.Parent).FirstOrDefault<Node>(); return CheckRecursion(key,pNode, list); } return false; } }
static void Main(string[] args) { //遞歸判斷 List<Node> list = new List<Node>(); list.Add(new Node { Key=1,Parent=2}); list.Add(new Node { Key = 2, Parent = 1 }); list.Add(new Node { Key = 3, Parent = 2 }); Console.WriteLine(RecursionSort.CheckRecursion(list)); Console.Read(); }
2.拓撲排序
步驟如下
(1) 從有向圖中選擇一個出度為0(即不依賴任何其它節點)的頂點並且輸出它。
(2) 從圖中刪去該頂點,並且刪去該頂點的所有邊。
(3) 重復上述兩步,直到剩余的圖中沒有出度為0的頂點。
我們來看一下上面舉的VS項目編譯順序列子按照上述算法的演示過程
第一步選擇 C節點
第二步選擇 B節點
至此完成了整個排序C,B,A 即先編譯C項目,再編譯B項目,最后編譯A項目
代碼實現如下
/// <summary> /// 拓撲節點類。 /// </summary> public class TopologicNode<T> { /// <summary> /// 獲取或設置節點的鍵值。 /// </summary> public T Key { get; set; } /// <summary> /// 獲取或設置依賴節點的鍵值列表。 /// </summary> public List<T> Dependences { get; set; } }
/// <summary> /// 拓撲排序類。 /// </summary> public class TopologicSort { /// <summary> /// 拓撲順序。 /// </summary> /// <typeparam name="TKey">節點的鍵值類型。</typeparam> /// <param name="list">一組節點。</param> /// <returns>拓撲序列。</returns> /// <exception cref="InvalidOperationException">如果存在雙向引用或循環引用,則拋出該異常。</exception> public static List<T> OrderBy<T>(List<TopologicNode<T>> list) { if (list == null) { throw new ArgumentNullException("參數空異常"); } List<T> listResult = new List<T>(); while (list.Count > 0) { //查找依賴項為空的節點 var item = list.FirstOrDefault(c => c.Dependences == null || c.Dependences.Count == 0); if (item != null) { listResult.Add(item.Key); //移除用過的節點,以及與其相關的依賴關系 list.Remove(item); foreach (var otherNode in list) { if (otherNode.Dependences != null) { otherNode.Dependences.Remove(item.Key); } } } else if (list.Count > 0) { //如果發現有向環,則拋出異常 throw new InvalidOperationException("存在循環引用"); } } return listResult; } }
//拓撲排序 //節點3依賴於2和1節點 list.Add(new Node { Key = 3, Parent = 1 }); List<TopologicNode<int>> listTopologicNode = new List<TopologicNode<int>>(); //構建排序節點 var group = (from p in list group p by p.Key into g select g); foreach (var g in group) { TopologicNode<int> node = new TopologicNode<int>(); node.Key = g.Key; node.Dependences = new List<int>(); foreach (Node value in g) { if (value.Parent != null) { node.Dependences.Add(value.Parent.GetValueOrDefault()); } } listTopologicNode.Add(node); } try { List<int> result = TopologicSort.OrderBy<int>(listTopologicNode); result.ForEach(e => { Console.WriteLine(e); }); } catch (Exception ex) { Console.WriteLine(ex.Message); }
運行結果如下
本章總結
本篇用到了Linq語法,如有不懂的可以到園里找找相關知識。后續我會專門寫一篇關於Linq,函數委托的文章,敬請期待!第一篇寫算法的隨筆到此完成,后續有其它算法靈感都會寫到博客園,拓撲排序的實際應用場景還有很多,最短路徑等等。如果您感覺本文不錯,對您有所幫助,請您不吝點擊下右邊的推薦按鈕,謝謝!
本章代碼示例代碼下載地址:http://code.taobao.org/svn/TopologicalSort ,請使用SVN進行下載!