算法的力量――第2届LabVIEW网络编程竞赛程序解析
CSSRC.IT.LIPO
经实践测试,LabVIEW包含的Tree控件插入n个节点的时间复杂度为O(n2),其中n为Tree控件中的节点数目。当n较大时,利用Tree控件实现类似于树形层次的公司组织结构虽然简单,但效率却相对较低。
应用方案:
在LabVIEW中,利用自编Hash算法(MAD散列算法和分离链地址法),成功的实现了树形结构的公司组织形象存储,并将插入n个树形节点的时间复杂度降低到O(n),大大提高程序性能。
作为第2届LabVIEW网络编程竞赛的参赛作品之一,程序还利用了LabVIEW8.2中最新的面向对象(Object-Oriented)的特性――类和对象(lvclass),进行了相关的封装,最终因起出色的性能和创意荣获竞赛的第一名。
1 背景文章源于2007年第二届LabVIEW 程序设计竞赛。本届竞赛共有三道试题,笔者选择的是其中的第三题――《公司员工问题》。原题完整内容不在此详描(详细内容可以查阅虚拟仪器家园网论坛或NI中文技术论坛http://www.vihome.com.cn/bbs/viewthread.php?tid=5038&statsdata=159||4903)。问题主要元素是这样的:
1) 某公司(员工不超过5000)需要能够尽可能快的查找到某个员工的情况,从节约成本考虑,不使用数据库
2) 公司中的员工具有行政隶属关系,就是说此公司的每个员工有且只有一个直接上级(公司总裁无上级)
3) 职称业务说明等略……
4) 每个员工的基本数据为编号(非零),姓名,性别,身高,体重,职称六项,……
5) 员工信息可由输入信息文件为in.txt导入,数据信息的格式说明(略)
6) 创建和查询:要求能够输入创建任意编号查找此编号对应的员工并输出保存此员工的信息、及其直接上级、直接下级、所有下属的信息,如果创建添加同一个编号,需要有信息提示有重复编号,同样应用于插入,删除都必须考虑编号不能重复。
7) 对于查询的数据可以保存输出TXT文件后,并提示用户下一查询输入信息,如果此编号对应的人员不存在,则也提示用户重新输入,输出格式略。
2 思路从题目内容可以提取出以下关键点:
l 员工≤5000
l 快速查找员工相关信息
l 不使用数据库
l 行政隶属关系(唯一上级)
l 批量导入,查询输出。
l 编号查询: 上级、直接下级、所有下级
2.1 问题核心于是,很容易想到以下需要解决的两个核心要素:
1) 公司组织明显是一种类似于树形的层次结构,需要有支持这种的构建(如添加、更新、删除等)和存储;
图 1 树形结构示例
2) 为了查询,需要进行公司组织的遍历(当然创建时要做唯一性判断,遍历校验也需要在考虑之中),这种结构的遍历方法是必须要提供的。
2.2 NI的支持首先可以想到从LabVIEW中寻找解决方案。很幸运,对于这个问题,LabVIEW中有很好的支持:Tree控件!
通过LabVIEW的Find Example,我们又可以找到关于Tree的多个例子,从而插入、删除节点操作并不困难,另外在Traverse Tree and Set Custom Symbols.vi一例中还提供了广度遍历的方式。
图 2 Traverse Tree实例
这样我们思路中的核心问题都迎刃而解了。
这道题目公布时列出的竞赛难度为三颗星,是三道题目中难度最低的。估计也正是因此,众多参赛者没有选择这道挑战性似乎偏弱的赛题。
起初,笔者也这么认为,可是真正做下来,却收获颇多,其中奥妙,容我慢慢道来。
3 初次实现 3.1 测试驱动!有了思路,想必大部分程序都会跃跃欲试打开LabVIEW开始编写程序了。可是笔者并没有马上开始写代码,而是马上考虑如何测试程序的正确性:为了验证我的程序是否正确,我应该先要准备测试的数据!
这一“测试驱动”的思想来源于敏捷大师Kent Beck。在code前,考虑如何测试所编写的程序(或者说是验证编程的思路)并建立其相应的测试代码是非常有益的而且可以带来长期的效果,无论将来你做了什么改变,都可以验证你的修改是否正确,是否影响以前的设计。
于是,我先准备了两个输入文件[1],一个是20位员工信息的测试数据文件(in.txt),用于验证程序运行时导入是否正确;一个是4000位员工信息的测试数据文件(in4000.txt),用于测试程序在导入接近5000员工时的效率等情况。测试数据如下图示意:
图 3 测试数据示例
构造第二个测试文件有一定的偶然性,不过正是因为构造了这第二个测试文件,才有了下面提到的种种发现和参赛的程序作品。
3.2 茅开又塞有了测试数据,便可放心大胆的构造程序了。鉴于NI Example里提供的例子简单详尽,在此对如何构造不做冗言。
程序构造完毕,运行起来,导入事先准备的in.txt,一眨眼全部导入,结果一切正常,感觉似乎所有的工作都已完成了,就象一个项目经过开发完毕,测试通过,行将Release,发布给客户……
可是,有点不对劲!导入时从Tree的垂直滚动条的显示看起来好像速度越来越慢!当我试试了导入第二份测试数据(in4000.txt)时,不幸的发现了一个问题。
这4000条员工似乎并不情愿导入,在我这台性能并不算很差的PC上速度显得颇有些慢,当时花了十几分钟还没有完成,于是让没有耐心的我中止了运行中程序。
程序在导入4000千条员工信息时有性能问题!
3.3 继续测试根据NI的Example,Tree控件就是这么使用的,似乎没有可以优化改进的地方啊?思路似乎进入了困境。怎么优化?
还是“测试先行”!
先建立一个测试程序,测试导入员工时间开销和员工数目之间的变化关系。这样后面做了性能优化,还可以通过测试查看优化效果。
测试程序实现比较简单,先准备好员工数组,然后取其中的子数组,做个循环插入,在每次循环前后计时,时间差就是导入一组员工的时间开销。原理类似于下图:
图 4 测试原理示意
由于如果在导入员工使用子vi调用以及给Tree的Child Text字符数组构造赋值等都可能会带来一定的时间开销。为此,首先进行了不进行任何vi调用,直接插入Tree的测试,代码示意如下:
图 5 直接插入Tree操作示意
3.4 测试结果测试结果如下图,其中粉色和绿色分别是对测试结果的线性拟合和二次拟合结果。
图 6 测试数据结果图
很明显,利用Tree控件导入的时间增长应该是二次的(2次因子量级为E-7、1次因子量级为E-4),不过二次因子相对较小,所以数量较少时体现不明显。
另外我还构造了一些儿其他测试:使用子vi调用、构造并赋值不同列数的Child Text字符数组。下表是不同情况下相应的拟合结果:
| 说明 | 二次因子E-7 | 变化(%) |
1 | 直接插入Tree | 1.46 |
|
2 | 函数调用入Tree +0列字符串数组 | 1.52 | ↑4.11 |
3 | 函数调用+2列 | 1.56 | ↑6.85 |
4 | 函数调用+5列 | 1.78 | ↑21.9 |
5 | 函数调用+9列 | 1.89 | ↑29.4 |
通过以上的数据测试(在另外一台机器上测试也有类似的结果),可以得出如下的结论:
1) 对于NI的Tree控件:
把数量为n的节点插入Tree的时间复杂度为:O(n2)
单个节点插入的时间复杂度:
O(n2) / n = O(n)
2) 函数调用对性能的影响
较小,添加一次函数调用只使的二次因子↑4%。因此,将复杂功能的函数分解成若干的子函数调用,开销相对较小,是值得的方式。
3) 构造字符串数组对性能的影响
↑7%~30%,有一定的影响(尚不明确是构造字符串的影响还是2维数组的影响)。
值得注意:字符串数组构造较多时会有相当的影响。
4 优化实现既然Tree控件的效率不够理想,思路就需要重新考虑了,在新的设计中需要考虑到以下几点:
1) 实现某种存储方式
2) 提供快速的查找
3) 支持遍历(树型)
由此,我想到了散列(Hash)这一常用数据结构,它是存储方式和查找方法的复合,同时他的查找算法复杂度 O(1)。关于散列的基础说明,可以查阅文后的参考文献或其他相关材料。
图 7 散列(Hash)示意
4.1 散列函数和冲突处理程序中采用了MAD散列法:
Hash = floor(m*(Key*A-floor(Key*A)))
其中:
m为散列表长
Key即为不会重复的员工编号
A取为 (根据Knuth的建议)
以下是程序框图:
图 8 散列函数
至于冲突处理,这里采用的是实用的分离链地址法。
4.2 程序实现:利用LabVIEW8的面向对象特性,把整个程序分成3个类:
1) Employee类:对应员工信息的业务处理方法(略)。
EmployeeStore类:HashTable中的存储单元,包括Employee信息和所有直接下级编号数组。
2) EmployeeHashTable:员工散列表的HastTable存储、查找、下级遍历等。
Hash表的完整结构如下图:
图 9 Hash表的完整结构
核心为GetSetEmployee.vi。其中使用Register变量存储完整的HashTable,利用散列对HashTable进行查询、插入、删除操作。
4.3 性能测试
测试结果如下图,其中粉色和绿色分别是对测试结果的线性拟合和二次拟合结果。
从图中可以看出:增长的趋势应该是线性的(一次因子量级为E-5)。这既验证了散列算法的理论结果,也说明程序实现的效果:对于数量为n的节点插入时间复杂度:O(n)。
这样,单个节点插入的时间复杂度:
O(n) / n = O(1)
下表是在同一机器上对6000条同样员工数据进行测试的对比结果:
| HashTable实现 | Tree实现 |
数据曲线 | ||
拟合结果 | THash= 2.47E-5×n
| TTree = 2.01E-7×n2 + 1.88E-4×n |
5 结论
利用Hash算法,成功的实现了树形结构的公司组织形象存储,并将插入n个树形节点的时间复杂度降低到O(n),大大提高程序性能。
参考书籍:
[1] 《重构--改善既有代码的设计》,Martin Fowler。
[2] 《测试驱动开发》,Kent Beck。
[3] 《代码大全》,Mc. Connel等。
[4] 《算法导论》,Cormen, T.H.等。
[5] 《计算机编程艺术》 Donald Gnuth。
[1] 相关的数据文件可以在虚拟仪器家园网论坛或NI中文技术论坛中获得