C# 非递归列表转树形结构的实现
说道树结构,很容易想到以下的数据结构
class Node { public string ID { get; set; } public string ParentID { get; set; } public List<Node> Children { get; set; } }
一般的数据都是从数据库中读取,将数据转换为对应的实体主要有一下几种方式:
全部读取,直接转换为对应的树或者部分读取,每次点击节点的的时候再加载下级数据。
本文不讨论部分读取的情况,主要涉及的是全部读取,转为树的做法。
网上最常见的方式就是递归,思路是找出所有第一层节点,然后层层遍历,类似下面的代码
public static List<Node> GetTree(List<Node> nodes) { var list = nodes.FindAll(a => a.ParentID == "0"); foreach (var node in list) { GetTree(node, nodes); } return list; } public static void GetTree(Node paretnNode, List<Node> nodes) { foreach (var node in nodes) { if (node.ParentID == paretnNode.ID) { GetTree(node, nodes); paretnNode.Children.Add(node); } } }
写递归,总觉得好麻烦,而其需要两个方法,有没有非递归的形式?
由树的非递归遍历使用队列想到,可以用类似的方法实现,代码如下
public static List<Node> GetTree(List<Node> list, Func<Node, bool> IsRoot) { var _list = new List<Node>(list);//复制 不修改原始数据 for (int i = _list.Count() - 1; i > -1; i--)//不能使用foreach 删除或者添加元素。顺序遍历,删除元素之后,需要对当前索引执行--操作。逆序删除节点不需要特殊处理 { Node node = _list[i]; if (!IsRoot(node))//顶级节点 { Node pNode = _list.FirstOrDefault(a => a.ID == node.ParentID);//找到父节点 if (pNode != null) { pNode.Children.Add(node);//添加节点 } _list.RemoveAt(i);//无论是否找到 删除,剩下的全部为顶级节点 } } return _list; }
主要思路就是每次遍历时,判断是否为顶级节点,如果不是,找到父节点,添加到父节点的子集中,删除该元素。
到遍历完成时,剩下的全部都是顶级元素,自然就是需要的树结构。
上面的Node类只是最基本的树状结构,实际使用当中,节点还有很多其他的属性,马上想到的时候通过继承Node类来实现通用。
public class MyTreeNode : Node { public string Name { get; set; } } TreeNode.GetTree(new List<MyTreeNode>() { new MyTreeNode { ID = "1", ParentID = "0", Name = "节点1" }, new MyTreeNode { ID = "2", ParentID = "0", Name = "节点2" }, new MyTreeNode { ID = "3", ParentID = "1", Name = "节点3" }, new MyTreeNode { ID = "4", ParentID = "1", Name = "节点4" }, new MyTreeNode { ID = "5", ParentID = "2", Name = "节点5" }, new MyTreeNode { ID = "6", ParentID = "1", Name = "节点6" }, new MyTreeNode { ID = "7", ParentID = "5", Name = "节点7" }, new MyTreeNode { ID = "8", ParentID = "7", Name = "节点8" }, new MyTreeNode { ID = "9", ParentID = "20", Name = "节点9" } }, a => a.ParentID == "0");
编译报错:错误 2 参数 1: 无法从“System.Collections.Generic.List<test.MyTreeNode>”转换为“System.Collections.Generic.List<test.Node>”
纳尼?MyTreeNode明明继承于Node,List<MyTreeNode>竟然不能转为List<Node>?
事实就是这么残酷,我们将上面的GetTree方法参数 list 从List<Node>改为IEnumerable<Node> ,编译就能通过了。
此处涉及到协变和逆变,具体不展开,请查阅相关资料。
由于基类的GetTree方法 返回的 是List<Node>,子类调用方法之后,需要再转换,最简单的就是在foreach中
foreach (var item in _list) { Console.WriteLine(item.Name);//error } foreach (MyTreeNode item in _list) { Console.WriteLine(item.Name); }
在第一个foreah中,item的编译类型是Node,所以不能使用Name属性,在第二个foreach中,我们指定了遍历时的类型,代码等效于
foreach (var item in _list) { MyTreeNode newitem = (MyTreeNode)item; Console.WriteLine(newitem.Name); }
大功告成。本文到此告一段落。
----------------------------------------苦逼的分割线--------------------------------------------
如果使用的是.net4.0及以上版本,就不需要再往下看了,可是苦逼的我用的是.net3.5,不能使用协变。
难道传给GetTree方法中的参数还需要再转型一遍?这样的代码,实在是缺乏美感。有木有其他的方法?
最近主要在学js,很多框架都有warp实现,马上想到通过给Node类外面包装一层
public class Wrap { public string ID { get; set; } public string ParentID { get; set; } public List<Wrap> Children { get; set; } public Node Target { get; set; } public Wrap(Node node) { this.Target = node; this.ID = node.ID; this.ParentID = node.ParentID; this.Children = new List<Wrap>(); } public static List<Wrap> GetTree(IEnumerable<Wrap> list, Func<Wrap, bool> IsRoot) { var _list = new List<Wrap>(list); for (int i = _list.Count() - 1; i > -1; i--) { Wrap node = _list[i]; if (!IsRoot(node)) { Wrap pNode = _list.FirstOrDefault(a => a.ID == node.ParentID); if (pNode != null) { pNode.Children.Add(node); } _list.RemoveAt(i); } } return _list; } }
用起来还是不爽,List<Warp>遍历解包得到Node,还要转型为实际类型,感觉更麻烦了!
将Node改为泛型T,试试看
public class Wrap<T> where T : Node { public string ID { get; set; } public string ParentID { get; set; } public List<Wrap<T>> Children { get; set; } public T Target { get; set; } public Wrap(T node) { this.Target = node; this.ID = node.ID; this.ParentID = node.ParentID; this.Children = new List<Wrap>(); } public static List<Wrap<T>> GetTree(IEnumerable<Wrap<T>> list, Func<Wrap<T>, bool> IsRoot) { var _list = new List<Wrap<T>>(list); for (int i = _list.Count() - 1; i > -1; i--) { Wrap<T> node = _list[i]; if (!IsRoot(node)) { Wrap<T> pNode = _list.FirstOrDefault(a => a.ID == node.ParentID); if (pNode != null) { pNode.Children.Add(node); } _list.RemoveAt(i); } } return _list; } }
可喜可贺,现在只需要遍历解包就能得到正确的列表了,能不能更近一步?
可以:这个时候,该重载运算符这个大杀器出场了
public static List<Wrap<T>> GetTree(IEnumerable<T> list, Func<Wrap<T>, bool> IsRoot) { var _list = new List<Wrap<T>>(list.Count());//手动copy foreach (T t in list) { _list.Add(t); } for (int i = _list.Count() - 1; i > -1; i--) { Wrap<T> node = _list[i]; if (!IsRoot(node)) { Wrap<T> pNode = _list.FirstOrDefault(a => a.ID == node.ParentID); if (pNode != null) { pNode.Children.Add(node); } _list.RemoveAt(i); } } return _list; } public static implicit operator T(Wrap<T> warp) { return warp.Target; } public static implicit operator Wrap<T>(T t) { return new Wrap<T>(t); }
重载之后,使用起来和上面的那个版本基本一致。
仔细观察 Wrap的结构,可以发现和Node类的属性一摸一样,可以直接令Wrap继承于Node,代码职责是否清晰,就仁者见仁智者见智了。
其实这些都是扯淡,老老实实用上面的版本。。。。
--------------------------------最后的分割线------------------------------------
上面的代码,其中缺了一点,就是 Children中的排序,一般来说,树的每个节点都是有对应的顺序的。
(其实是因为倒序删除导致了顺序不对-_-,可以改为顺序删除)
补充如下:
………… if (pNode != null) { pNode.Children.Add(node);//添加节点 if (NeedOrder) { pNode.Children = pNode.Children.OrderBy(a => a.Sequence).ToList(); } } …………
总结:本文主要涉及到的知识点:linq,递归,泛型,协变,类型转换,操作符重载
实质是用双重循环(不包括排序)来替代递归,具体效率嘛,呵呵 你懂的,在实际生产环境谨慎使用