在實際工作中,你肯定會經常的對樹進行遍歷,並在樹和集合之間相互轉換,你會頻繁的使用遞歸。
事實上,這些算法在邏輯上都是一樣的,因此可以抽象出一個通用的算法來簡化工作。
在這篇文章里,我向你介紹,我封裝的兩個類,TreeIterator和TreeMap,使用他們,你不必再寫遞歸就可以在任意的樹和任意的集合之間相互裝換。
一 TreeIterator
1.1 TreeIterator功能描述:
TreeIterator封裝了對樹的遍歷算法,他提供了如下功能:
1)遍歷樹
2)將任意一顆樹轉換為一個任意集合。
使用TreeIterator只需要一個方法調用就可以完成樹的遍歷,或者將樹映射到一個集合,譬如,你可以使用TreeIterator方便的對一個dom進行遍歷,或者將其導出到一個集合中,期間你可以將dom節點自由的映射到任意一個自定義對象。
1.2 TreeIterator優點:
不必再手寫遞歸來遍歷樹,在實際工作中,這可以極大的節省你的時間,因為在寫遞歸的時候,會經常忽略遞歸條件而造成死遞歸。
1.3 TreeIterator缺點:
TreeIterator內部使用了遞歸,所以會影響到性能。其次,第一次使用TreeIterator時,需要適應一下,一旦使用了幾次,你會發現他確實可以減少你的工作量。
1.4 TreeIterator的設計思路:
在實際項目中,可能經常的要對一顆樹進行遍歷,然后將其導出到一個集合中,通常的做法是手寫一個遞歸。然而,細心一點就會發現,這些遞歸操作的邏輯大部分都是相同的, 首先,都要獲取根節點的所有子節點,然后遞歸的遍歷每一個子節點,並將每一個子節點映射到一個自定義對象中。偽代碼如下:
public void Each(currentNode){ childNodesOfCurrentNode=get child nodes of currentNode;//獲取當前節點的所有子節點。 foreach(var eachElement in childNodesOfCurrentNode){ Map(eachElement,target);//將當前子節點映射到target對象中 Each(eachElement);//然后對每一個子節點遞歸遍歷。 } }
事實上,完全可以將這些重復的邏輯封裝起來,然而要想做到一個通用的封住算法,需要解決如下幾個問題:
1.如何獲取當前節點的子節點。
2.將這個子節點映射到什么類型的對象。
這兩個問題都是一個變化點,需要將他們留給使用者來做,因此可以使用委托來將這兩個變化點提取成一個參數,這樣,就可以讓使用者來返回子節點以及將子節點映射到什么類型。
1.5 TreeIterator的完整代碼:

using System; using System.Collections.Generic; using System.Collections; using System.Text; namespace Francis { public class TreeIteractor { /// <summary> /// 遍歷一個樹 /// </summary> /// <typeparam name="TSrc">樹節點的類型</typeparam> /// <param name="root">樹的根節點</param> /// <param name="childNodesFilter">返回當前節點的子節點</param> /// <param name="callbackThatMapsElementFromSrcToDes">這個節點返回樹的當前節點</param> public static void Each<TSrc>(TSrc root, Func<TSrc, IEnumerable> childNodesFilter, Action<TSrc> callbackThatMapsElementFromSrcToDes) { SelfEach(root, childNodesFilter, callbackThatMapsElementFromSrcToDes); } public static void Each<TSrc>(TSrc root, Func<TSrc, IEnumerable<TSrc>> childNodesFilter, Action<TSrc> callbackThatMapsElementFromSrcToDes) { SelfEach(root, childNodesFilter, callbackThatMapsElementFromSrcToDes); } protected static void SelfEach<TSrc>(TSrc root, Func<TSrc, IEnumerable> childNodesFilter, Action<TSrc> callbackThatMapsElementFromSrcToDes) { TSrc specifiedRootWithinSrc = root; IEnumerable childElements = childNodesFilter(specifiedRootWithinSrc); foreach(var eachElement in childElements as IEnumerable<TSrc> ?? childElements) { callbackThatMapsElementFromSrcToDes((TSrc)eachElement); SelfEach((TSrc)eachElement, childNodesFilter, callbackThatMapsElementFromSrcToDes); } } } }
1.6 一個例子
下面這個例子演示了如何遍歷一個TreeView菜單:

List<string> list = new List<String>(); Francis.TreeIteractor.Each<TreeNode>( treeView1.Nodes[0],//treeView1是Winform中的TreeView樹菜單 root => { return root.Nodes;//返回當前節點的子節點 }, root => { //root是當前節點,因此,這里你可以將其映射到任意和對象,並將這個對象加入到你的集合中。 list.Add(root.Text); } ); list.ForEach(element => { textBox1.Text +=element+ "\r\n"; });
2.1 TreeMap功能描述
TreeMap提供了和和TreeIterator相反的功能,他可以將一個任意的集合映射到一個樹,這聽起來很不可思議:
1)以樹的方式遍歷一個集合
2)將一個集合映射到一顆樹。
舉個例子,List集合中存儲了Area對象,而Area對象里面存儲了省市的id,name,pid,此時你可能想要遍歷List<Area>並將其轉換為一個dom存儲到xml文件中,或者,你要將它顯示到一個TreeView菜單中。完成這項工作,你需要寫不少的代碼,而使用TreeMap只需要一個方法的調用就OK了。
2.2 TreeMap優點:
同樣,他可以減少你的工作量,這也是使用它的最大原因。
2.3 TreeMap缺點:
他比TreeIterator更加復雜,需要多使用幾次從而適應他。
在使用TreeMap時需要自己制定一個條件用於退出遞歸。
2.4 TreeMap設計思路:
怎么將一個任意的集合轉換為一個任意的樹呢,我想首先應該在集合中指定一個根,然后還要指定一個篩選條件,用來篩選當前根的子節點,在篩選的過程中還需要構造樹,這聽起來很復雜。
2.5 TreeMap完整代碼:

using System; using System.Collections.Generic; using System.Collections; namespace Francis.Common{ public class TreeMap<TSrc> { #region fields IEnumerable<TSrc> _src; IEnumerable _unGenericSrc; TSrc _specifiedSrcRoot; Func<TSrc,bool> _rootFilter; #endregion #region property protected TSrc SpecifiedRoot { get { if(_specifiedSrcRoot == null) { bool finded = false; foreach(var eachElement in _src) { if(_rootFilter(eachElement)) { _specifiedSrcRoot = eachElement; finded = true; break; } } if(!finded) throw new ArgumentException("沒有找到指定的源根節點,請仔細檢查rootFilter的內部邏輯!"); } return _specifiedSrcRoot; } } #endregion property #region initialization public TreeMap(IEnumerable<TSrc> src,Func<TSrc,bool>rootFilter) :this(src, default(TSrc), rootFilter){ } public TreeMap(IEnumerable<TSrc> src, TSrc specifiedSrcRoot) :this(src, specifiedSrcRoot, null){ } public TreeMap(IEnumerable src, TSrc specifiedSrcRoot) :this(src, specifiedSrcRoot, null){ } protected TreeMap(IEnumerable src, TSrc specifiedSrcRoot,Func<TSrc,bool>rootFilter) { if(src == null) throw new ArgumentException("src不能為空"); if(specifiedSrcRoot == null&&rootFilter==null) throw new ArgumentException("specifiedSrcRoot和rootFilter不能同時為空"); _src = src as IEnumerable<TSrc>; _unGenericSrc = src; _specifiedSrcRoot = specifiedSrcRoot; _rootFilter = rootFilter; } #endregion #region OtherToTree /// <summary> /// 將一個源集合映射到一棵樹 /// </summary> /// <param name="isChildNodeOfSpecifiedRoot">判斷源集合中的節點是否為指根節點的子節點</param> /// <param name="callbackThatMapsElementFromSrcToDes">將源集合中的元素映射到一個指定的元素</param> public void Each<TDes>(Func<TSrc, TSrc, bool> isChildNodeOfSpecifiedRoot, Func<TSrc, TDes,TDes> callbackThatMapsElementFromSrcToDes) { OtherToTree(SpecifiedRoot, default(TDes), isChildNodeOfSpecifiedRoot, callbackThatMapsElementFromSrcToDes); } protected virtual void OtherToTree<TDes>(TSrc srcRoot, TDes des, Func<TSrc, TSrc, bool> isChildNodeOfSpecifiedRoot, Func<TSrc, TDes, TDes> callbackThatMapsElementFromSrcToDes) { des= callbackThatMapsElementFromSrcToDes(srcRoot,des); foreach(var currentElement in _src != null ? _src : _unGenericSrc) { if(isChildNodeOfSpecifiedRoot((TSrc)currentElement, srcRoot)) { OtherToTree((TSrc)currentElement, des, isChildNodeOfSpecifiedRoot, callbackThatMapsElementFromSrcToDes); } } } /// <summary> /// 對源集合以樹的方式進行遍歷。 /// </summary> /// <param name="isChildNodeOfSpecifiedRoot">刪選子節點</param> /// <param name="callbackThatMapsElementFromSrcToDes">為你返回根節點</param> public void Each(Func<TSrc, TSrc, bool> isChildNodeOfSpecifiedRoot, Action<TSrc> callbackThatMapsElementFromSrcToDes) { OtherToTree(SpecifiedRoot, isChildNodeOfSpecifiedRoot, callbackThatMapsElementFromSrcToDes); } private void OtherToTree(TSrc SpecifiedRoot, Func<TSrc, TSrc, bool> isChildNodeOfSpecifiedRoot, Action<TSrc> callbackThatMapsElementFromSrcToDes) { callbackThatMapsElementFromSrcToDes(SpecifiedRoot); foreach(var currentElement in _src != null ? _src : _unGenericSrc) { if(isChildNodeOfSpecifiedRoot((TSrc)currentElement, SpecifiedRoot)) { OtherToTree((TSrc)currentElement, isChildNodeOfSpecifiedRoot, callbackThatMapsElementFromSrcToDes); } } } #endregion } }
2.6 一個例子:
下面這個例子演示了如何將一個List<Area>對象顯示到TreeView樹菜單中。

class Area{ public int Id{get;set;} public string Name{get;set;} public int PId{get;set;} } //Winform窗體的Load事件。 public void Form1_Load(object sender,EventArgs e){ List<Area> areas=new List<Area>(){ new Area(){Id=1,Name="北京",PId=0}, new Area(){Id=2,Name="海淀區",PId=1}, new Area(){Id=3,Name="朝陽區",PId=1}, new Area(){Id=4,Name="東城區",PId=1} }; TreeMap<Area,TreeNode> treeMap=new TreeMap<Area,TreeNode>(); treeMap.Each( () ); //將數據從List<Node>_src加載到TreeView中。這里你不再需要寫復雜的遍歷算法 Area srcRoot = new Area() { Name = "china", Id = 0 }; TreeMap<Area> areaToTreeNode = new TreeMap<Area>(areas, srcRoot); areaToTreeNode.Each<TreeNode>( //篩選條件用於篩選根節點,需要的是,要自己指定一個返回false的值,用於退出遞歸,否則將會出現死遞歸。 (eachElementWithinSrc, specifiedRoot) => { //遞歸條件。 if(eachElementWithinSrc.Id == specifiedRoot.Id) return false; return eachElementWithinSrc.PId == specifiedRoot.Id; }, //將Area映射到TreeNode (srcElement, root) => { //首先判斷,如果根節點等於null,就創建一個根節點。 if(root == null) { root = treeView1.Nodes.Add("china"); return root; } //創建一個新節點 TreeNode newNodeAsRoot = new TreeNode(srcElement.Name); newNodeAsRoot.Tag = srcElement; //將新節點添加到root下,使其成為root的子節點。 root.Nodes.Add(newNodeAsRoot); //返回新創建的節點。 return newNodeAsRoot; } ); }