39. 拓扑排序

发布时间 2023-07-01 19:00:34作者: 夏冰翎

一、什么是拓扑排序

  拓扑排序是对有向无圈图的顶点的一种排序,它使得如果存在一条从 \(v_{i}\)\(v_{j}\) 的路径,那么排序中 \(v_{j}\) 出现在 \(v_{j}\) 的后面。有向边 (v,w) 表明任务 v 必须在任务 w 前完成。显然,如果图含有圈,那么拓扑排序是不可能的,因为对于圈上的两个顶点 v 和 w,v 先于 w 同时 w 又先于 v。此外排序不是唯一的,任何合理的排序都是可以的。

二、拓扑排序的实现

  一个简单的求拓扑排序的算法是先找到任意一个没有进入边的顶点。然后我们显印出该顶点,并将它和它的边一起从图中删除。然后,我们对图的其余部分同样应用这样的方法处理。

/**
 * @brief 拓扑排序
 * 
 * @param graph 待操作的图
 */
void TopSort(Graph graph)
{
    int counter = 0;
    int v = 0,w = 0;

    int topNum[graph->vertexNum];
  
    int indegree[graph->vertexNum];                                     //创建记录各顶点入度的数组
    FindIndegree(graph,indegree);                                       //统计各顶点的入度

    // 图中的每一个顶点
    for(counter = 0; counter < graph->vertexNum; counter++)
    {
        // 未输出入度为0的顶点
        v = FindNewVertexOfIndegreeZero(indegree,graph->vertexNum);     // 查找入度为0的顶点
        // 如果这样的v不存在
        if(v == -1)
        {
            printf("图存在回路!\n");
            break;
        }
        //输出v,或记录v的输出序号
        topNum[v] = counter;
        printf("%-5c",graph->vertex[v]);
        // v的每个邻接点w
        for(w = GetFirstNeighbor(graph,v); w >= 0; w = GetNextNeighbor(graph,v,w))
        {
            indegree[w]--;
        }
    }
}

/**
 * @brief 获取图中各结点的入度
 * 
 * @param graph 待操作的图
 * @param indegree 各顶点入度的数组
 */
void FindIndegree(Graph graph,int indegree[])
{
    int i = 0, j = 0;

    //初始化数组,默认初始值全部为 0
    for(i = 0; i < graph->vertexNum; i++) 
    {
        indegree[i]=0;
    }

    //遍历邻接表,根据各链表中结点的数据域存储的各顶点位置下标,在 indegree 数组相应位置+1
    for (i = 0; i < graph->vertexNum; i++) 
    {
        for(j = 0; j < graph->vertexNum; j++)
        {
            if(graph->edge[j][i] != 0)
            {
                indegree[i]++;
            }
        }
    }
}
/**
 * @brief 查找入度为0的顶点
 * 
 * @param indegree 各顶点入度的数组
 * @param N 数组的大小
 * @return int 如果找到则返回对应顶点的下标,否则返回-1
 */
int FindNewVertexOfIndegreeZero(int indegree[],int N)
{
    int i = 0;

    for(i = 0; i < N; i++)
    {
        if(indegree[i] == 0)
        {
            indegree[i]--;
            return i;
        }
    }

    return -1;
}

  因为 FindNewVertexOfIndegreeZero 是对 Indegree 数组的一个简单的顺序扫描,所以每次对它的调用都花费 \(O(|v|)\) 时间。由于 \(|V|\) 次这样的调用,因此该算法的时间复杂度为:\(T = O(V^{2})\)

  为了提高程序的运行效率,我们可以随时将入度为 0 的顶点放在一个容器中。首先,对每一个顶点计算它的入度。然后,将所有入度为 0 的顶点放入一个初始为空的队列中。当队列不空时,并将与 v 邻接的所有的顶点的入度减 1。只要一个顶点的入度降为 0,就把该顶点放入队列中。此时,拓扑排序就是顶点出队的顺序。

/**
 * @brief 拓扑排序
 * 
 * @param graph 待操作的图
 */
void TopSort(Graph graph)
{
    int counter = 0;
    int v = 0,w = 0;

    int topNum[graph->vertexNum];

    int indegree[graph->vertexNum];                 //创建记录各顶点入度的数组
    FindIndegree(graph,indegree);                   //统计各顶点的入度

    Queue Q = CreateQueue();

    // 图中的每一个顶点v
    for(v = 0; v < graph->vertexNum; v++)
    {
        if(indegree[v] == 0)
        {
            Enqueue(Q,v);
        }
    }

    while(!QueueIsEmpty(Q))
    {
        // 输出V,或者记录V的输出序号;
        v = Dequeue(Q);
        printf("%-5c",graph->vertex[v]);
        topNum[v] = ++counter;

        // v的每个邻接点w
        for(w = GetFirstNeighbor(graph,v); w >= 0; w = GetNextNeighbor(graph,v,w))
        {
            if(--indegree[w] == 0)                                               // w为v尚未访问的邻接顶点
            {
                Enqueue(Q,w);
            }
        }
    }

    if(counter != graph->vertexNum)
    {
        printf("图存在回路!\n");
    }
}

  如果使用邻接表,那么执行这个算法所用的时间复杂度为:\(T = O(|V| + |E|)\)

三、逆拓扑排序

  对于一个 AOV 网,如果采用下列步骤进行排序,则称之为 逆拓扑排序

  1. 从 AOV 网中选择一个没有后继(出度为 0)的顶点并输出;
  2. 从网中删除该顶点和所有以它为终点的有向边;
  3. 重复 ① 和 ② 直到当前的 AOV 网为空;