拓扑排序是对有向无圈图的顶点的一种排序方式,使得如果存在一条从vi到vj的路径,那么在排序中vj就在vi的之后出现。在图1中的图表示迈阿密州立大学的课程先修结构(course prerequisite structure)。有向边(v,w)表明课程v必须在课程w选修前修完。这些课程的拓扑排序是不破坏课程先修要求的任意的课程序列。
图1 表示课程先修结构的无圈图
显然,如果图含有圈,那么进行拓扑排序是不可能的,因为对于圈上的两个顶点v和w, v先于w同时w又先于v。此外,拓扑排序不必是唯一的,任何合理的排序都是可以的。在图2中,v1 ,v2,v5,v4,v3,v7,v6和 v1,v2,v5,v4,v7,v3, v6两个都是拓扑排序。
图2.一个无圈图
一个简单的求拓扑排序的算法是先找出任意一个没有入边(incoming edge)的顶点。然后我们显示出该顶点,并将它和它的边一起从图中删除。然后,我们对图的其余部分继续应用这样的方法来处理。
为了将上述方法形式化,我们把顶点v的入度(indegree)定义为边(u,v)的条数。我们计算图中所有顶点的入度。假设每一个顶点的入度均被存储,并且图被读入一个邻接表中,则此时可以应用图3中的算法生成一个拓扑排序。
void Graph::topsort()
{
for(int counter=0;counter<NUM_VERTICES;counter++)
{ Vertex v = findNewVertexOfIndegreeZero();
if(v==NOT_A_VERTEX )
throw CycleFoundException{ };
v.topNum = counter;
for each Vertex w adjacent to v
w.indegree--;
}
}
图3.简单拓扑排序代码
函数findNewVertexOfIndegreeZero扫描数组,寻找一个尚未被分配拓扑编号的入度为0的顶点。如果这样的顶点不存在,那么它返回NOT_A_VERTEX。这就说明该图有圈。
因为函数findNewVertexOfIndegreeZero是对顶点数组的一个简单的顺序扫描,所以每次对它的调用都花费O(|V|)时间。由于有|V|次这样的调用,因此该算法的运行时间为O(|V*V|)。
通过更仔细地关注这样的数据结构,我们可以做得更好。产生如此差的运行时间的原因在于对顶点数组的顺序扫描。如果图是稀疏的,那么我们就可以预知,在每次迭代期间只有少数顶点的入度被更新。然而,虽然只有一小部分发生变化,但在搜索入度为0的顶点时我们(潜在地)查看了所有的顶点。
我们可以通过将所有(未分配拓扑编号)的入度为0的顶点放在一个特殊的盒子中而消除这种无效的劳动。此时findNewVertexOfIndegreeZero函数返回(并删除)该盒子中的任一顶点。当我们将它的邻接顶点的入度减1时,检查每一个顶点并在它的入度降为0时把它放入盒子中。
为实现这个盒子,可以使用一个栈或一个队列。我们将使用队列。首先,对每个顶点计算它的入度。然后,将所有入度为0的顶点放入一个初始为空的队列中。当队列不空时,删除一个顶点v,并将邻接到v的所有顶点的入度均减1。只要一个顶点的入度降为0,就把该顶点放入队列中。此时,拓扑排序就是顶点出队的顺序。图4显示了每一阶段之后的状态。
图4对图2中的图应用拓扑排序的结果
这个算法的伪代码实现在图5中给出。和前面一样,我们将假设图已经被读到一个邻接表中,并假设入度均被算出且和顶点一起被存储。我们还假设每个顶点有一个域,叫作topNum, 其中存放的是顶点的拓扑编号。
void Graph::topsort()
{
Queue<Vertex> q;
int counter =0;
q.makeEmpty();
for each Vertex v
if(v.indegree == 0)
q.enqueue(v);
while(!q.isEmpty ( ))
{
Vertex v = q.dequeue();
v.topNum = ++counter;//分配下一个拓扑编号
for each Vertex w adjacent to v
if(--w.indegree ==0)
q.enqueue(w);
}
if(counter != NUM_VERTICES)
throw CycleFoundException { };
}
图 5.实施拓扑排序的伪代码
如果使用邻接表,那么执行这个算法所用的时间为O(|E|+|V|)。当认识到for循环体对每条边最多执行一次时,这个结果是显然的。入度的计算可以由下列代码完成。同样,计算的开销也是O(|E|+|V|),尽管这里存在一些嵌套的循环。
for each Vertex v
v.indegree=0;
for each Vertex v
for each Vertex w adjacent to v
w.indegree++;
队列操作对每个顶点最多进行一次,而其他的初始化步骤,包括入度的计算,所花费的时间也与图的大小呈正比。
参考文献: 数据结构与算法分析--C++语言描述(第四版)(Make Allen Weiss)