数据结构与算法----拓扑排序

540 阅读5分钟

简介

拓扑排序是非闭环图存在的线性解,由入度为0的节点开始进行遍历,结果非唯一

入度:多少个顶点指向自己,或者自己前面有多少个顶点指向自己

出度:自己指向多少个顶点,或者自己后面指向多少个顶点

活动:一个较大的工程往往被划分成许多子工程,我们把这些子工程称作活动(activity),可以理解为,图中的边

拓扑排序一般采用邻接表来保存图,非常适合拓扑排序或者后续的关键路径

拓扑排序案例

拓扑排序是由入度为零的节点开始遍历,因此如果有多个入度为零的点,那么会有多个结果

以上图为例:则拓扑排序结果可能为:

V1-V2-V3-V4-V5-V6-V7-V8-V9(以出入队列方式处理入度为0的边的拓扑结果)

V1-V4-V6-V3-V2-V5-V8-V7-V9(本程序得到的结果,以出入栈方式处理入度为0的边的拓扑结果)

上面拓扑结果为参照,存在多种情况,可以自行调整,

本程序案例流程如下:

(注意:入队是队尾进队首出,本程序的出入栈是先进后出,并不是一次性把结果全放入栈中,而是边入栈边出栈,下一轮都会出栈一个,详情可以参考前面栈和队列的文章)

1.取top为要遍历的节点默认为-1,即没有,从V1-V9循环遍历,找出入度为0的边,将其压入栈中,其他的依次类推(注意:该出栈的最终结果即为拓扑排序结果),此时拓扑结果为空

2.开始拓扑,出栈一个(V1),并且清除出边关系,查找出队的出边链表,对齐指向的顶点入度 减1,发现入度为0的顶点,则入栈,以便于下次遍历

3.继续拓扑,出栈一个(V4),并且清除出边关系,查找出队的出边链表,对齐指向的顶点入度 减1,发现入度为0的顶点,则入栈,以便于下次遍历

4.继续拓扑,出栈一个(V6),并且清除出边关系,查找出队的出边链表,对齐指向的顶点入度 减1,发现入度为0的顶点,则入栈,以便于下次遍历

5.继续拓扑,出栈一个(V3),并且清除出边关系,查找出队的出边链表,对齐指向的顶点入度 减1,发现入度为0的顶点,则入栈,以便于下次遍历

6.继续拓扑,出栈一个(V2),并且清除出边关系,查找出队的出边链表,对齐指向的顶点入度 减1,发现入度为0的顶点,则入栈,以便于下次遍历

7.继续拓扑,出栈一个(V5),并且清除出边关系,查找出队的出边链表,对齐指向的顶点入度 减1,发现入度为0的顶点,则入栈,以便于下次遍历

8.继续拓扑,出栈一个(V8),并且清除出边关系,查找出队的出边链表,对齐指向的顶点入度 减1,发现入度为0的顶点,则入栈,以便于下次遍历

9.继续拓扑,出栈一个(V7),并且清除出边关系,查找出队的出边链表,对齐指向的顶点入度 减1,发现入度为0的顶点,则入栈,以便于下次遍历

10.继续拓扑,出栈一个(V9),此时队列为空,图也已经遍历完毕,得出最终拓扑结果V1-V4-V6-V3-V2-V5-V8-V7-V9

代码实现

代码实现和案例略有不同,唯一的区别是,这里巧妙的利用的入度为0的点,将其标记为链栈的一部分,添加top作为栈顶指针,来协助进行出入栈

代码流程如下:

1.声明栈顶指针top,找到入度为0的顶点,入栈,每次入栈,入度参数指向原栈顶,现栈顶top指向当前顶点

2.设置参数num标识着遍历拓扑了多少个顶点,避免出现局部闭环问题

3.开始循环,出栈进行拓扑排序

4.打印出活动,找出出边链表首指针,方便处理出边

5.对入度为0顶点进行出栈操作

6.循环查找出边链表,对其指向顶点减少1,并且查看入度是否为0,如果为0,将该顶点入栈,直到所有出边都处理完毕,结束本次循环

7.继续进行拓扑流程,回到步骤3,直到栈内元素为空,则生成拓扑结果

//拓扑排序,从入度为零的边(没有依赖其他边)开始遍历,每遍历一个则消除其出度,如此往复
void showTuppoSort(LSVertexNode *list, int count) {
    //巧妙利用入度为0的顶点作为栈的一部分,设置top指向-1即栈底,入度为0的id对齐进行入栈,top指向他,其被加入栈底
    int top = -1;
    for (int i = 0; i < count; i++) {
        if (!list[i].id) {
            list[i].id = top;
            top = i;
        }
    }

    int num = 0; //num表示的是拿出了几个,如果 num<count 表示里面有环
    while (top != -1) {
        num++; //默认找到了,数量+1;
        printf("%d ", top + 1);
        
        LSAdjvexNode *link = list[top].link; //获取出边链表link
        
        top = list[top].id; //及时出栈,top指向下一个元素
        //处理该顶点所有出边
        while (link) {
            int idx = link->adjvex; //这个adjvex指向的不是索引,比索引要大一个
            list[idx].id--; //对出边对应顶点入度减1
            if (!list[idx].id) {
                //入度为0,入栈,可以作为下次待遍历的点
                list[idx].id = top; //相当于链栈的栈顶元素指向了其下面的元素,即入栈、压栈
                top = idx; //更新栈顶指针
            }
            link = link->next; //进入下一轮直到所有出边都处理完毕
        }
    }
    if (num < count) {
        printf("\n中间有回路! \n");
    }
    //注意结果可能和你想想的不太一样,只要入度为零的谁在前后都一样
}

了解此逻辑后,则可以进行关键路径学习