在实际工作中,你肯定会经常的对树进行遍历,并在树和集合之间相互转换,你会频繁的使用递归。
事实上,这些算法在逻辑上都是一样的,因此可以抽象出一个通用的算法来简化工作。
在这篇文章里,我向你介绍,我封装的两个类,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 { ////// 遍历一个树 /// /// 树节点的类型 /// 树的根节点 /// 返回当前节点的子节点 /// 这个节点返回树的当前节点 public static void Each (TSrc root, Func childNodesFilter, Action callbackThatMapsElementFromSrcToDes) { SelfEach(root, childNodesFilter, callbackThatMapsElementFromSrcToDes); } public static void Each (TSrc root, Func > childNodesFilter, Action callbackThatMapsElementFromSrcToDes) { SelfEach(root, childNodesFilter, callbackThatMapsElementFromSrcToDes); } protected static void SelfEach (TSrc root, Func childNodesFilter, Action callbackThatMapsElementFromSrcToDes) { TSrc specifiedRootWithinSrc = root; IEnumerable childElements = childNodesFilter(specifiedRootWithinSrc); foreach(var eachElement in childElements as IEnumerable ?? childElements) { callbackThatMapsElementFromSrcToDes((TSrc)eachElement); SelfEach((TSrc)eachElement, childNodesFilter, callbackThatMapsElementFromSrcToDes); } } } }
1.6 一个例子
下面这个例子演示了如何遍历一个TreeView菜单:
List<string> list = new List(); Francis.TreeIteractor.Each ( 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并将其转换为一个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{ #region fields IEnumerable _src; IEnumerable _unGenericSrc; TSrc _specifiedSrcRoot; Func 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 src,Func bool>rootFilter) :this(src, default(TSrc), rootFilter){ } public TreeMap(IEnumerable src, TSrc specifiedSrcRoot) :this(src, specifiedSrcRoot, null){ } public TreeMap(IEnumerable src, TSrc specifiedSrcRoot) :this(src, specifiedSrcRoot, null){ } protected TreeMap(IEnumerable src, TSrc specifiedSrcRoot,Func bool>rootFilter) { if(src == null) throw new ArgumentException("src不能为空"); if(specifiedSrcRoot == null&&rootFilter==null) throw new ArgumentException("specifiedSrcRoot和rootFilter不能同时为空"); _src = src as IEnumerable ; _unGenericSrc = src; _specifiedSrcRoot = specifiedSrcRoot; _rootFilter = rootFilter; } #endregion #region OtherToTree /// /// 将一个源集合映射到一棵树 /// /// 判断源集合中的节点是否为指根节点的子节点 /// 将源集合中的元素映射到一个指定的元素 public void Each (Func bool> isChildNodeOfSpecifiedRoot, Func callbackThatMapsElementFromSrcToDes) { OtherToTree(SpecifiedRoot, default(TDes), isChildNodeOfSpecifiedRoot, callbackThatMapsElementFromSrcToDes); } protected virtual void OtherToTree (TSrc srcRoot, TDes des, Func bool> isChildNodeOfSpecifiedRoot, Func callbackThatMapsElementFromSrcToDes) { des= callbackThatMapsElementFromSrcToDes(srcRoot,des); foreach(var currentElement in _src != null ? _src : _unGenericSrc) { if(isChildNodeOfSpecifiedRoot((TSrc)currentElement, srcRoot)) { OtherToTree((TSrc)currentElement, des, isChildNodeOfSpecifiedRoot, callbackThatMapsElementFromSrcToDes); } } } /// /// 对源集合以树的方式进行遍历。 /// /// 删选子节点 /// 为你返回根节点 public void Each(Func bool> isChildNodeOfSpecifiedRoot, Action callbackThatMapsElementFromSrcToDes) { OtherToTree(SpecifiedRoot, isChildNodeOfSpecifiedRoot, callbackThatMapsElementFromSrcToDes); } private void OtherToTree(TSrc SpecifiedRoot, Func bool> isChildNodeOfSpecifiedRoot, Action 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对象显示到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 areas=new List(){ 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 treeMap=new TreeMap(); treeMap.Each( () ); //将数据从List_src加载到TreeView中。这里你不再需要写复杂的遍历算法 Area srcRoot = new Area() { Name = "china", Id = 0 }; TreeMap areaToTreeNode = new TreeMap(areas, srcRoot); areaToTreeNode.Each( //筛选条件用于筛选根节点,需要的是,要自己指定一个返回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; } ); }