13--图的应用之拓扑排序

788 阅读7分钟

有关图的存储和其他数据结构与算法的相关知识请查看文章数据结构与算法基础知识文章汇总

一、相关概念

1.AOV网

在一个表示工程的有向图中,用顶点表示活动,弧表示活动之间的优先关系,这样的有向图称为顶点表示活动的AOV网(Activity On Vertex Network)。

如下图所示,C1~C5顶点表示活动,活动之间的有向线段即为弧表示活动之间的优先关系image.png

2.拓扑序列

G=(V,E)上一个具有n个顶点的有向图,V中的顶点序列为V1,V2,...,Vn。若满足从顶点Vi到Vj有一条路径,且顶点序列Vi必然在Vj之前,则我们称这样的顶点序列为拓扑序列

如图中的C1->C2->C3->C4->C5 image.png

3.拓扑排序

所畏的拓扑排序就是对有向图构造拓扑序列的过程,构造拓扑排序的过程会产生两个结果:

  • 1.如果图在构造拓扑排序过程中全部顶点被输出,则说明图是一个不存在环的AOV网;请看下图,假设C5->C3,则从C3到C5的路径中,不能保证C3一定在C5之前,即不满足拓扑序列的定义。

image.png

  • 2如果图在构造拓扑排序的过程中没有输出全部顶点,即图中有环,则图不是AOV网,如上图。

二、拓扑排序的应用

如下图,以拍电影为例: image.png

在电影的拍摄中,需要完成很多的环节,各个环节之间有一定的先后关系,当一些环节完成时,才能进行后面的环节,比如只有导演确定好了,演员确定好了,剧本写好了,才能开始拍摄电影,只有拍摄好各个场景了,才能进行后期的制作,等等。在拍电影时,一开始可能只能确定所有的环节中任意的两个环节之间的关系,并不能像上图那样把整个流程都画的这么明确,所以需要通过拓扑排序得出如上图所示的所有环节之间环环相扣的关系图。

所以,拓扑排序就是解决一个工程能否顺序进行的问题。

思考:如何构造下图的拓扑排序?

image.png

三、拓扑排序分析

1.分析

  • 1.根据拓扑排序拓扑序列的定义我们知道,拓扑序列一定至少有一个开始的顶点和结束的顶点,开始的顶点一定没有顶点通过弧指向它,结束的顶点一定没有弧指向其他的顶点。我们称开始的顶点入度为0,结束的顶点出度为0
  • 2.根据1中所得到AOV网的特征,我们可以遍历图的所有顶点,当判断到入度为0的顶点时,将该顶点入栈,同时将该顶点从图中移除,然后遍历与该顶点有连接的顶点,将顶点与它们的连接关系断开,并且将它们的入度分别减1,如此循环下来,即可将所有入度为0的顶点输出,当所有顶点都输出的时候,说明此图是一个不存在环的AOV网。所有顶点输出的顺序即为AOV网的拓扑排序

2.过程演示

第一步 image.png

图中灰色的顶点表示入度为0的顶点,下同

第二步

image.png

第三步

image.png

第四步

image.png

第五步

image.png

第六步

image.png

第七步

image.png

第八步

image.png

第九步

image.png

注:V6颜色搞错了,应该是灰色,下同。

第十步

image.png

第十一步

image.png

最终输出了所有顶点

image.png 结果:3 -> 1 -> 2 -> 6 -> 0 -> 4 -> 5 -> 8 -> 7 -> 12 -> 9 -> 10 -> 13 -> 11

四、代码实现

1.选择合适的存储结构

  • 1.图的存储有邻接矩阵的顺序存储和邻接表的链式存储;
  • 2.前面对拓扑排序的分析中,即有移除断开连接等等的删除操作,这删除操作在顺序存储中没有链式存储灵活,所以我们选择邻接表的链式存储

2.数据结构的定义

1.边表结点的数据结构

//边表结点

typedef struct EdgeNode
{
    //邻接点域,存储该顶点在邻接表中的下标
    int adjvex;

    //用于存储权值,对于非网图可以不需要
    int weight;

    //指针域,指向下一个相连的邻接点
    struct EdgeNode *next;
}EdgeNode;

2.顶点表数据结构

//顶点表结点
typedef struct VertexNode
{
    //顶点入度值
    int in;

    //顶点域,存储顶点数据
    int data;

    //边表头指针,指向与之相连的边表信息
    EdgeNode *firstedge;

}VertexNode, AdjList[MAXVEX];

3.邻接表的图的结构

typedef struct
{
    AdjList adjList;//邻接表

    //图中当前顶点数和边数
    int numVertexes,numEdges;

}graphAdjList,*GraphAdjList;

4.邻接矩阵的数据结构

typedef struct
{
    int vexs[MAXVEX];

    int arc[MAXVEX][MAXVEX];

    int numVertexes, numEdges;

}MGraph;

无法直接由代码构造邻接表,需要通过邻接矩阵来转换

3.邻接表的链式存储实现

1.状态值和数据类型定义

#define OK 1

#define ERROR 0

#define TRUE 1

#define FALSE 0

#define MAXEDGE 20

#define MAXVEX 14

#define INFINITYC 65535

//Status是函数的类型,其值是函数结果状态代码,如OK等
typedef int Status;

2.构造AOV网的邻接矩阵

void CreateMGraph(MGraph *G)
{
    int i, j;

    G->numEdges=MAXEDGE;
    G->numVertexes=MAXVEX;

    //初始化图
    for(i = 0; i < G->numVertexes; i++)
    {
        G->vexs[i]=i;
    }

    //初始化图 
    for(i = 0; i < G->numVertexes; i++)
    {
        for ( j = 0; j < G->numVertexes; j++)
        {
            G->arc[i][j]=0;
        }
    }

    G->arc[0][4]=1;
    G->arc[0][5]=1;
    G->arc[0][11]=1;
    G->arc[1][2]=1;
    G->arc[1][4]=1;
    G->arc[1][8]=1;
    G->arc[2][5]=1;
    G->arc[2][6]=1;
    G->arc[2][9]=1;
    G->arc[3][2]=1;
    G->arc[3][13]=1;
    G->arc[4][7]=1;
    G->arc[5][8]=1;
    G->arc[5][12]=1;
    G->arc[6][5]=1;
    G->arc[8][7]=1;
    G->arc[9][10]=1;
    G->arc[9][11]=1;
    G->arc[10][13]=1;
    G->arc[12][9]=1;
}

3.邻接矩阵转换成邻接表

void CreateALGraph(MGraph G,GraphAdjList *GL)
{
    int i,j;
    EdgeNode *e;
    
    //创建图
    *GL = (GraphAdjList)malloc(**sizeof**(graphAdjList));

    //对图中的顶点数.弧数赋值
    (*GL)->numVertexes=G.numVertexes;
    (*GL)->numEdges=G.numEdges;

    //读入顶点信息,建立顶点表
    for(i= 0;i <G.numVertexes;i++)
    {
        (*GL)->adjList[i].in=0;
        (*GL)->adjList[i].data=G.vexs[i];

        //将边表置为空表
        (*GL)->adjList[i].firstedge = NULL;
    }

    //建立边表
    for(i=0;i<G.numVertexes;i++)
    {
        for(j=0;j<G.numVertexes;j++)
        {
            if(G.arc[i][j]==1)
            {
                //创建空的边表结点
                e=(EdgeNode *)malloc(**sizeof**(EdgeNode));

                //邻接序号为j
                e->adjvex=j;

                // 将当前顶点上的指向的结点指针赋值给e
                e->next=(*GL)->adjList[i].firstedge;

                //将当前顶点的指针指向e
                (*GL)->adjList[i].firstedge=e;
                (*GL)->adjList[j].in++;
            }
        }
    }
}

4.构造图的拓扑排序

1.代码实现

//若AOV网图无回路则输出拓扑排序的序列并且返回状态值1,若存在回路则返回状态值0
Status TopologicalSort(GraphAdjList GL){

    EdgeNode *e;
    int i,k,gettop;

    //用于栈指针下标
    int top=0;

    //用于统计输出顶点的个数
    int count=0;

    //建栈将入度为0的顶点入栈(目的:为了避免每次查找时都要遍历顶点表查找有没有入度为0的顶点)
    int *stack=(int *)malloc(GL->numVertexes * sizeof(int) );

    //1.遍历邻接表-顶点表,将入度in为0的顶点入栈
    for(i = 0; i<GL->numVertexes; i++){
        //将入度为0的顶点入栈
        if(0 == GL->adjList[i].in)
        {
            stack[++top]=i;
        }
    }

    printf("top = %d\n",top);

    //2.循环栈结构(当栈中有元素则循环继续)
    while(top != 0)
    {
        //出栈
        gettop=stack[top--];
        printf("%d -> ",GL->adjList[gettop].data);

        //输出顶点,并计数
        count++;

        //遍历与栈顶相连接的弧
        for(e = GL->adjList[gettop].firstedge; e; e = e->next)
        {
            //获取与gettop连接的顶点
            k=e->adjvex;

            //1.将与gettop连接的顶点入度减1;
            //2.判断如果当前减1后为0,则入栈
            if( !(--GL->adjList[k].in) ){
                //将k入栈到stack中,并且top加1;
                stack[++top]=k;
            }
                
        }
    }

    /*思考:3 -> 1 -> 2 -> 6 -> 0 -> 4 -> 5 -> 8 -> 7 -> 12 -> 9 -> 10 ->13 -> 11
     这并不是唯一的拓扑排序结果.
     分析算法:将入度为0的顶点入栈的时间复杂度为O(n), 而之后的while 循环,每个顶点进一次栈,并且出一次栈. 入度减1, 则共执行了e次. 那么整个算法的时间复杂度为O(n+e)*/
    printf("\n");

    //判断是否把所有的顶点都输出. 则表示找到了拓扑排序;
    if(count < GL->numVertexes)
        return ERROR;
    else
        return OK;
}

2.代码调试

int main(int argc, const char * argv[]) {

    // insert code here...

    printf("Hello, 拓扑排序!\n");

    MGraph G;

    GraphAdjList GL;

    int result;

    CreateMGraph(&G);

    CreateALGraph(G,&GL);

    result=TopologicalSort(GL);

    printf("result:%d",result);

    return 0;
}

3.输出结果

image.png

五、总结

  • 1.图的拓扑排序是构造拓扑序列的过程,它能解决现实生活中工程能否顺序进行按何种顺序进行的问题;
  • 2.构造图的拓扑排序的过程中选择了邻接表的链式存储栈的思想,在解决问题的过程中能合理的应用数据结构能帮助我们事半功倍