GacUI需要用到拓扑排序的地方很多,包括但不限于:
CodePack.exe
一个把一堆C++代码打包成几个成对的h和cpp代码的工具。这里就需要拓扑排序。因为在配置文件里(譬如说这个),我只定义了哪一些文件需要合并。而最后文件与文件的#include关系,是自动算出来的。拓扑排序在这里起到的作用,就是如果排序不成功,那我就要输出错误信息。
现在我输出错误信息只是告诉说你错了,并不能告诉你是谁跟谁搞在一起导致出错的。强连通分量在这里就起到了很好的效果,他识别出了循环引用的最小的集合,那么我就可以把这个集合输出到错误信息里,这样你就知道配置文件里面哪里写的不对。
Workflow 编译器
Workflow脚本语言支持C#那样子的class和struct。class可以继承,struct可以在成员里面引用别的struct。如果我们把class a继承自class b,和struct a用了struct b,都看成a依赖b的话,那么所有的class或者所有的struct就构成了一个图。这个图必须是偏序结构的,否则就意味着,要么你循环继承class,要么你虚幻嵌套struct,这都是错误的。
那强连通分量是什么作用呢?其实仍然是为了输出错误信息。如果你有一个很大的Workflow程序,我告诉你某个class循环继承了自己,看起来其实不是很友好。如果我可以告诉你到底是哪几个class互相继承,你改起代码来自然就方便多了。每一个强连通分量都代表了一个错误信息,很方便。
Workflow C++代码生成器
Workflow生成C++代码还有一些额外的要求。譬如说你在GacUI里面,指明了一个窗口的ref.CodeBehind属性为true,那么GacUI就会为你这个窗口单独生成一对C++文件,否则就全部加进大文件里。这样可以有效减少文件数量。你需要单独生成文件的理由,自然是你需要把自己的C++代码合并进这个窗口生成的C++代码里,就像流行的GUI库编辑器做的那样。典型的有事件处理函数,或者是你自己用C++添加的成员等等。
这就带来了两个问题。第一个问题是,如果你有三个窗口,a继承出b,而b继承出c。本来abc都是生成到同一个文件里面的,但是后来你给b加上了ref.CodeBehind=true,这会导致c也必须生成到一个独立的文件。因为如果ac在一个文件,b在另一个文件的话,你就没法正确#include。
显然,你ref.CodeBehind=true的一些窗口,使得ref.CodeBehind=false的一些窗口不一定可以全部放到一个头文件里。在这里识别出强连通分量就可以很好地减少分裂的头文件数量。当然并不是每一个强连通分量就是一个文件,这样也是很多余的。具体的办法我还没开始想,不过肯定是水到渠成的问题,因为明显只要对每一个强连通分量按照一定的规则染色,就搞定了。
第二个问题是,Workflow的类可以有嵌套类,嵌套类也会影响生成文件的安排,但是就算你只有一个文件,还会带来另一个问题。譬如说我有这样的C++代码:
class Fuck : public Bitch::Dung{public: class Shit{};};class Bitch : public Fuck::Shit{public: class Dung{};};
你会发现,不管你怎么调整顺序,不管你怎么向前声明,你都没办法让他编译通过。当然C#是不会有这个问题的,以C#和COM作为模板的Workflow自然也不会有这个问题。但是你真的这么写了,我就没办法替你生成C++代码。
那么自然,Workflow的C++代码生成器必须在这个时候报错。这里我们仍然要进行拓扑排序,但是图的每一个节点,其实就是每一个top level class和所有内部类的集合。在这里自然就是{Fuck, Fuck::Shit}以及{Bitch, Bitch::Dung}。在检查继承关系之后,我们发现这两个节点是循环引用的,因此会被分配到同一个强连通分量里。如果排序的结果,有一个强连通分量有超过一个节点的话,那么就意味着这种代码没办法生成C++代码,因此就可以抱错了。报错的时候,我又可以生成好看的错误信息了。
实现
其他的我就不说了,还有很多。如果你们好好看了上面的维基百科的链接,就知道Kosaraju算法是表达为递归的。在敲代码之前,我也考虑过到底要不要把递归化为循环,让爆栈不那么容易发生。后来想想算了,因为这里的递归的层数,跟你C++代码#include的层数,和类继承的层数是一致的。如果你的Workflow类一共继承了1000层,那你也不要怪我GacGen.exe崩溃,我不管的(逃。因此我毅然选择了递归。
Vlpp里面一共有三个文件:PartialOrdering.h、PartialOrdering.cpp和TestPartialOrdering.cpp。大家有兴趣的话就去看,里面有实现以及测试用例。
经过我的估算,这个类的三个主要函数的worst case复杂度分别是:
- InitWithGroup:O(ElgV)
- InitWithFunc:O(V² + ElgV)
- Sort:O(V+E)
总的来说,整个东西的复杂度还是会被控制在O(nlgn)或者O(n²),还行。
之前瞎JB搞得算法的worst case是O(n³lgn),看起来很吓人,不过因为我处理的图都是稀疏图,所以平均下来也不会这么难看。既然已经把靠谱的算法做进GacUI了,那么接下来就是把每一处用到垃圾拓扑排序的地方删掉,用新写的算法替换上
如果你想要自学编程却找不到资料?关注我吧。关注“程序员资源课”,你想要的资料我这里都有。
![4a83237a0265fb38b6690c4737983727.png](https://i-blog.csdnimg.cn/blog_migrate/a5ca729bb7a9c00d7a2d746c360633d4.jpeg)