拓扑排序的定义和原理等我不再赘述,各种教材和网络上都有详细解释,今天我主要谈一谈两种实现拓扑排序的算法。
Kahn算法:
<span style="white-space:pre"> </span>将所有入度为0的顶点加入队列q;
while(!q.empty() )
{
u = q.front();
q.pop();
list.push(u);
for (u的每个邻接点v)
{
删除边(u, v);
if (indegree(v) == 0)
q.push(v);
}
}
if (图G还有边存在)
return 存在环
else
return list;
以上这种是比较典型的求拓扑排序的算法,算法复杂度为O(v+e),常可用来判断该图是否是DAG(有向无环图)
在算法导论上,介绍的则是另外一种算法,它是基于DFS的,实现十分简单,仅需要在DFS中多加一个语句即可。
基于DFS的拓扑排序算法(前提:图是DAG):
L ← 用于存放排序结果的数组
S ← 出度为0的顶点的集合
for (S中的每个顶点)
dfs(n)
void dfs(node n)
{
if (!vis[n])
{
vis[n] = true;
for (每一个顶点m,满足m->n)
dfs(m);
}
L.push(n);
}
可以看到,我们只是在dfs函数快退出时将结点加入到L中而已。
(注意,for中的顶点m,满足的是m->n而不是n->m)
下面简单证明一下它的正确性:
对任意的边m->n,当调用dfs(n)的时候,有如下两种情况:
1) dfs(m)还没有被调用,此时会调用dfs(m),只有dfs(m)返回之后,dfs(n)才会返回
2) dfs(m)已经被调用过并返回了
(由于本图是DAG,所以不存在dfs(m)已经被调用,但是在dfs(n)在被调用时还未返回的情况)
无论是以上哪一种情况,m都会先于n被添加到L中。所以对于任意边m->n,在L中,m总会在n前面。
本算法的复杂度为O(v+e),需要注意的一些点是,本算法是建立在图为DAG的基础上的,当然,可以进行一些修改来做环路检测,另外, 本算法的起点是对每个出度为0的顶点进行dfs,而Kahn算法则是从每个入度为0的顶点出发。为何本算法需要从出度为0的顶点出发呢?因为出度为0的顶点必然排在最后面,而最先调用dfs的顶点最后才加入L中。
对比两种算法,有着异曲同工之妙,一个从入度为0的顶点出发,一个从出度为0的顶点出发;一个对m->n中的每个n进行操作,一个对m->n中的每个m进行操作。
如果需要判断该图是否为DAG,那么第一种算法是不错的选择,如果已经知道该图为DAG,则第二种算法更加简洁!