案情综述
树形结构在开发过程中很常见,菜单树、文件目录树、组织架构数、树形控件……等等,由于树形结构的数据结构比较特殊,不适于直接存储,特别是存储在关系型数据库中。所以通常数据库中对树形结构的存储都进行了某种转换,最常见的方式就是以节点<->父节点对的方式进行描述和存储,这种存储的优点是,结构简单,层次描述清晰,特别是对于层级不确定的情况,弹性很大,理论上可以存储任意深度和广度的树,缺点是在初始化树的时候需要进行数据转换,在节点数大的情况下,转换效率就成了首要问题。最常见的转换方式——也是针对节点<->父节点对的存储方式设计的——就是递归转换了。
递归算法很常见,并且通常是代码简洁和高效率的代表,但是在上述树形结构的实践过程中,很容易发生性能问题。
现场重建
下面的叙述,是基于工程实践中实际发生的一个低性能案例及其优化过程改编的,测试的平台基于一台笔记本,i5
M560双核CPU,主频2.67G,内存4G,运行Windows 7 x64操作系统,开发环境是Visual Studio
2010,.Net Framework 4.0。
实际案例中,树形结构是WinForm的TreeView控件,它的TreeNode具有Nodes属性,Nodes属性是一个TreeNode的列表,以此实现树形结构的组装与呈现。为了简化程序编译,我们用一个TreeNodeDemo对象来代表这个结构,只实现最基本的数据结构:
public
class TreeNodeDemo
{
private List<TreeNodeDemo>
_nodes;
public List<TreeNodeDemo> Nodes
{
get
{
return _nodes;
}
}
public TreeNodeDemo()
{
_nodes = new
List<TreeNodeDemo>();
}
public string Name
{
get;
set;
}
public object Tag
{
get;
set;
}
}
案例中存储节点数据的数据表和模型对象也比较复杂,这里简化成一个NodeData对象,除了必须的ID<->ParentID属性对,只有一个Name属性:
public
class NodeData
{
public int ID
{
get;
set;
}
public int ParentID
{
get;
set;
}
public string Name
{
get;
set;
}
public NodeData(int id, int parentID, string name)
{
ID = id;
ParentID = parentID;
Name = name;
}
}
节点的ID、ParentID不能为空,每个节点的ParentId必须指向某个已存在的NodeData的ID属性,ParentID
= 0的节点是第一级节点(位于根节点下),不存在ID = 0的节点。
先建一个初始化递归存储对象列表的方法,用比较常规的结构,名为InitNormalNodeDatas,把性能指标放大一些,放10,000个节点,大致平均地分布在7级目录中。用的方法比较笨,能把数据填进去就行了:
public static List<NodeData>
InitNormalNodeDatas()
{
List<NodeData> result = new
List<NodeData>(10000);
//先初始化10个一级节点(id=1-10);
int n=0;
for (int i = 0; i < 10; i++)
result.Add(new NodeData(++n, 0, "ID = " + n.ToString() + ";
ParentID = 0"));
//初始化90个二级节点(id=11-100);节点数10+90=100
for (int i = 1; i < 10; i++)
for (int j = 0; j < 10; j++)
result.Add(new NodeData(++n, i, "ID = " + n.ToString() + ";
ParentID = " + i.ToString()));
//初始化800个三级节点(id=101-800);节点数100+800=900
for (int i = 11; i < 91; i++)
for (int j = 0; j < 10; j++)
result.Add(new NodeData(++n, i, "ID = " + n.ToString() + ";
ParentID = " + i.ToString()));
//初始化5000个四级节点(id=901-5900);节点数5000+900=5900
for (int i = 101; i < 601; i++)
for (int j = 0; j < 10; j++)
result.Add(new NodeData(++n, i, "ID = " + n.ToString() + ";
ParentID = " + i.ToString()));
//初始化3000个五级节点(id=5901-8900);节点数5900+3000=8900
for (int i = 901; i < 1201; i++)
for (int j = 0; j < 10; j++)
result.Add(new NodeData(++n, i, "ID = " + n.ToString() + ";
ParentID = " + i.ToString()));
//初始化1000个六级节点(id=8901-9900);节点数8900+1000=9900
for (int i = 5901; i < 6001; i++)
for (int j = 0; j < 10; j++)
result.Add(new NodeData(++n, i, "ID = " + n.ToString() + ";
ParentID = " + i.ToString()));
//初始化100个七级节点(id=9901-10000);节点数9900+100=10000
for (int i = 8901; i < 8911; i++)
for (int j = 0; j < 10; j++)
result.Add(new NodeData(++n, i, "ID = " + n.ToString() + ";
ParentID = " + i.ToString()));
return result;
}
BuildTreeOld是原来的构建树方法的入口,ANodeOld是建树方法的核心,是一个递归方法:
public static TreeNodeDemo
BuildTreeOld(List<NodeData>
nodeDatas)
{
TreeNodeDemo result = new TreeNodeDemo();
if (nodeDatas != null &&
nodeDatas.Count > 0)
{
var rows = from i in nodeDatas
where ( i.ParentID==0)
select i;
if (rows != null && rows.Count()
> 0)
{
foreach (NodeData row in rows)
{
TreeNodeDemo node = new TreeNodeDemo();
node.Name = row.Name;