数据结构编程笔记二十二:第七章 图 拓扑排序算法的实现

上次我们介绍了图的最短路径算法的实现,这次介绍基于邻接表的拓扑排序算法的实现。

还是老规矩:

程序在码云上可以下载。
地址:https://git.oschina.net/601345138/DataStructureCLanguage.git

本次拓扑排序程序共用到以下源文件,有一些已经在之前的文章介绍过。还是和以前一样,所有源文件需要放在同一目录下编译。
my_constants.h 各种状态码定义
LinkedList.cpp 不带头结点单链表各种基本操作的实现
ALGraph.h 图的邻接表存储结构表示定义
ALGraph.cpp 基于邻接表的图的基本操作实现
Stack.cpp 顺序栈的基本操作实现
TopologicalSort.cpp 拓扑排序算法的实现
拓扑排序测试.cpp 主函数,调用算法完成拓扑排序程序的演示

为了操作实现方便,我使用了单链表程序来简化部分操作,但是这个单链表与《 数据结构编程笔记四:第二章 线性表 单链表的实现》一文介绍的单链表有所区别,本文用到的单链表不带头结点,很多操作与带头结点的单链表有区别。望读者注意。具体程序参见《数据结构编程笔记十九:第七章 图 图的邻接表存储表示及各基本操作的实现》;

邻接表在《数据结构编程笔记十九:第七章 图 图的邻接表存储表示及各基本操作的实现》一文有所介绍,my_constants.h、ALGraph.h 、ALGraph.cpp和LinkedList.cpp四个源文件与此文中的同名源文件内容完全一致,没有修改。这里不再重复贴了(否则文章会很长,不能突出重点),但在码云上你可以下载到全部源文件,我会把它放在一个目录下。

拓扑排序操作在实现过程中使用了栈。我引入了已经实现的顺序栈来辅助完成拓扑排序。
顺序栈在《 数据结构编程笔记八:第三章 栈和队列 顺序栈和进位制程序的实现》一文中已经有所介绍,除了修改栈的数据类型为int,源代码其余部分我都没有改变。需要的读者可以参考这篇文章。我会在总结后面贴出这篇代码。

本次只贴拓扑排序的核心代码和主函数:

源文件:TopologicalSort.cpp

/*
    函数:FindInDegree
    参数:ALGraph G 图G(邻接表存储结构) 
          int indegree[] 存储顶点入度的数组 
    返回值:无
    作用:求顶点的入度
*/
void FindInDegree(ALGraph G, int indegree[]) {

    //工作指针p 
    ArcNode *p;

    //对存储入度的indegree数组赋初值0
    for(int i = 0; i < G.vexnum; i++) {
        indegree[i] = 0; 
    }//for

    //扫描每个顶点后面的弧链表 
    for(int i = 0; i < G.vexnum; i++) {

        //p指向顶点i后面弧链表的首元结点 
        p = G.vertices[i].firstarc;

        //顶点v的弧链表没有扫描完毕 
        while(p) { //while(p) <=> while(p != NULL) 
            //每找到一个弧结点,对应邻接点的入度+1 
            indegree[p->data.adjvex]++;

            //p指向下一个弧结点 
            p = p->nextarc;
        }//while 
    }//for
}//FindInDegree

/*
    函数:TopologicalSort
    参数:ALGraph G 有向图G(邻接表存储结构) 
    返回值:状态码,操作成功返回OK,操作失败返回ERROR 
    作用:若G无回路,则输出G的顶点的一个拓扑序列并返回OK,
          否则返回ERROR。
*/
Status TopologicalSort(ALGraph G) {

    //count为已输出顶点数,初值为0
    int i, k, count = 0;

    //入度数组,存放各顶点当前入度数
    int indegree[MAX_VERTEX_NUM];

    //声明栈S 
    Stack S;

    //工作指针p,指向弧结点 
    ArcNode *p; 

    //对各顶点求入度并存入数组indegree[]
    FindInDegree(G, indegree);

    //初始化零入度顶点栈S
    InitStack(S);

    //扫描所有顶点
    for(i = 0; i < G.vexnum; ++i) {

        //若其入度为0,则将i压入零入度顶点栈S
        //if(!indegree[i]) <=> if(indegree[i] == 0) 
        if(!indegree[i]) { 
            Push(S, i); 
        }//if
    }//for

    //当零入度顶点栈S不空
    //while(!StackIsEmpty(S)) <=> while(StackIsEmpty(S) == FALSE)
    while(!StackIsEmpty(S)) {

        //从栈中弹出一个零入度顶点的序号,并将其保存到i
        Pop(S, i);

        //输出i号顶点
        printf(" %d ", G.vertices[i].data);

        //已输出顶点数+1
        ++count;

        //扫描i号顶点的每个邻接顶点
        for(p = G.vertices[i].firstarc; p; p = p->nextarc) { 

            //i号顶点的邻接点序号为k
            k = p->data.adjvex;

            //k的入度减1,若减为0,则将k入栈S
            //if(!(--indegree[k])) <=> if((--indegree[k]) == 0)
            if(!(--indegree[k])) { 
                Push(S, k);
            }//if
        }//for
    }//while


    //零入度顶点栈S已空,图G还有顶点未输出
    if(count < G.vexnum) {
        printf("此有向图有回路\n");
        return ERROR;
    }//if
    else {
        return OK;
    }//else 
}//TopologicalSort

源文件:拓扑排序测试.cpp

//**************************引入头文件*****************************
#include    //使用了标准库函数 
#include   //使用了动态内存分配函数 

#include "my_constants.h"  //引入自定义的符号常量,主要是状态码 
#include "ALGraph.h"       //引入图的邻接表存储结构定义 
#include "LinkedList.cpp"  //引入单链表实现,用到其中的插入、删除等操作 
#include "ALGraph.cpp"     //引入图的邻接表存储结构基本操作实现
#include "Stack.cpp"       //引入顺序栈的基本操作实现 
#include "TopologicalSort.cpp" //引入图的拓扑排序算法实现 

int main() {

    printf("----------------图的拓扑排序(邻接表)演示程序------------------\n\n"); 

    //图的邻接表存储方式 
    ALGraph G;

    //创建图g并打印初始状态 
    printf("->测试图G的创建:(拓扑排序需要图的类型为有向图,请输入0)\n"); 
    CreateGraph(G);
    printf("图G创建成功!\n");
    printf("->打印创建后的图G:\n"); 
    Display(G);

    //测试拓扑排序 
    printf("\n->拓扑排序结果:"); 
    TopologicalSort(G);

    //测试销毁图G
    printf("\n->测试销毁图G:"); 
    DestroyGraph(G);
    printf("销毁成功!");

    printf("演示结束");
}//main

测试采用书上P182页图7.28。请读者自行验证拓扑排序算法的正确性。

程序运行过程中的输入和输出:

----------------图的拓扑排序(邻接表)演示程序------------------

->测试图G的创建:(拓扑排序需要图的类型为有向图,请输入0)
请输入图的类型(有向图输入0, 有向网输入1,无向图输入2,无向网输入3): 0
请输入图的顶点数,边数: 6,8
请输入6个顶点的值(用空格隔开):
1 2 3 4 5 6
请输入每条弧(边)的弧尾和弧头(以逗号作为间隔):
1,2
1,3
1,4
3,2
3,5
4,5
6,4
6,5
图G创建成功!
->打印创建后的图G:
此图为有向图!

图中共有6个顶点,8条弧(边),它们分别是:
+----+-----------------------------------------------
|顶点|   邻接顶点(和权值)
+----+-----------------------------------------------
|  1 | →4  →3  →2
+----+-----------------------------------------------
|  2 |
+----+-----------------------------------------------
|  3 | →5  →2
+----+-----------------------------------------------
|  4 | →5
+----+-----------------------------------------------
|  5 |
+----+-----------------------------------------------
|  6 | →5  →4
+----+-----------------------------------------------


->拓扑排序结果: 6  1  3  2  4  5
->测试销毁图G:销毁成功!演示结束
--------------------------------
Process exited with return value 0
Press any key to continue . . .

总结:
拓扑排序的过程就是从图中找出入度为0的顶点入栈,然后从栈中弹出顶点,然后找出与该顶点邻接的新的入度为0的顶点入栈,再弹出,如此循环,直到图中没有入度为0的顶点时看看顶点是否全部输出,若没有则说明图中存在环,拓扑排序失败,否则,拓扑排序成功。

下次的文章会介绍图这一章最后一个程序:关键路径。感谢大家一直以来的关注和支持。再见!

附:修改栈元素类型后的顺序栈源代码。

源文件:Stack.cpp

#define STACK_INIT_SIZE 20 //栈的初始大小 
#define STACKINCREMENT 5   //每次扩容增加多少 

//-------------------栈的顺序存储表示------------------------- 

typedef int SElemType;   //栈的元素为int类型 
typedef struct {          //栈的顺序存储表示                        
    SElemType *base;            //栈底指针,在栈构造之前和销毁之后,base的值为NULL 
    SElemType *top;             //栈顶指针
    int stacksize;              //当前已分配的存储空间,以元素为单位 
}Stack; 

//--------------------------栈的相关函数(供非递归后序遍历使用)----------------------------
/*
    函数:InitStack_Sq
    参数:Stack &S 顺序栈引用  
    返回值:状态码,OK表示操作成功 
    作用:构造一个空的顺序栈 
*/
Status InitStack(Stack &S){

    //动态申请顺序栈的内存空间,并检查内存空间是否成功分配
    //if(!(S.base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType))))
    //这句代码相当于以下两行代码:
    //S.base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType));
    //if(!S.base)  <=>  if(S.base == NULL) 
    if(!(S.base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType)))){
        printf("内存分配失败,程序即将退出!\n");
        exit(OVERFLOW);
    }//if

    //由于刚动态分配完的栈是空栈,所以栈顶指针和栈底指针都指向栈底   
    S.top = S.base;

    //栈的大小就是栈的初始化大小参数STACK_INIT_SIZE 
    S.stacksize = STACK_INIT_SIZE;

    //操作成功 
    return OK; 
}//InitStack_Sq

/*
    函数:DestoryStack_Sq
    参数:Stack &S 顺序栈引用  
    返回值:状态码,OK表示操作成功 
    作用:释放顺序栈S所占内存空间 
*/
Status DestoryStack(Stack &S){

    //栈底指针保存的是顺序栈内存空间的首地址 
    free(S.base);

    //操作成功 
    return OK; 
}//DestoryStack_Sq

/*
    函数:StackIsEmpty_Sq
    参数:Stack S 顺序栈S 
    返回值:若顺序栈S是空栈返回1,否返回0 
    作用:判断顺序栈S是否为空栈
*/
Status StackIsEmpty(Stack S){

    //栈顶指针和栈底指针都指向栈底表示此栈是空栈 
    return S.top == S.base; 
}//StackIsEmpty_Sq

/*
    函数:ReallocStack_Sq
    参数:Stack &S 顺序栈S引用 
    返回值:状态码,操作成功返回OK,否则返回ERRROR
    作用:将栈S扩容,每扩容一次,栈的大小增加STACKINCREMENT
*/
Status ReallocStack(Stack &S){

    //为顺序栈重新分配内存(扩容),扩展的空间大小是STACKINCREMENT
    /*if(!(S.base = (SElemType *)realloc(S.base, 
                (STACK_INIT_SIZE + STACKINCREMENT) * sizeof(SElemType))))
      这句代码相当于:
      S.base = (SElemType *)realloc(S.base, 
                        (STACK_INIT_SIZE + STACKINCREMENT) * sizeof(SElemType));
      if(!S.base) <=> if(S.base == NULL)
    */
    if(!(S.base = (SElemType *)realloc(S.base, 
               (STACK_INIT_SIZE + STACKINCREMENT) * sizeof(SElemType)))){
        printf("内存分配失败,程序即将退出!\n");
        exit(OVERFLOW);
    }//if

    //由于扩容前栈已经满了,所以栈顶指针位置就是栈底指针+原来栈的大小 
    S.top = S.base + S.stacksize;

    //扩容后,栈的大小增加了STACKINCREMENT 
    S.stacksize += STACKINCREMENT;

    //操作成功 
    return OK; 
}//ReallocStack_Sq

/*
    函数:Push_Sq
    参数:Stack &S 顺序栈引用
          SElemType e 被插入的元素e 
    返回值:成功获取顺序栈S栈顶元素的值后返回OK,否则返回ERRROR
    作用:(入栈、压栈)插入元素e为新的栈顶元素
*/
Status Push(Stack &S, SElemType e){ 

    //入栈时发现栈满了,就要追加存储空间(扩容) 
    if(S.top - S.base >= S.stacksize) {  

        //调用扩容函数
        ReallocStack(S);
    }//if

    //插入前,栈顶指针指向当前栈顶元素的下一个位置 
    //将e赋值给栈顶指针所指存储空间(插入元素e),栈顶指针后移
    //*S.top++ = e;  <=>  *(S.top) = e;  S.top++; 
    *S.top++ = e;

}//Push_Sq

/*
    函数:Pop_Sq
    参数:Stack &S 顺序栈引用
          SElemType &e 带回被删除的元素值e 
    返回值:删除成功返回OK,否则返回ERRROR
    作用:(出栈,弹栈)若栈不空,则删除S的栈顶元素,用e返回其值
*/
Status Pop(Stack &S, SElemType &e){ 

    //在空栈中执行出栈操作没有意义,所以要判断栈是否为空
    //注意栈是否为空和栈是否存在不是一个概念,所以不可以用 
    //S.base != NULL判断栈是否为空 
    if(StackIsEmpty(S)) { 
         return ERROR;
    }//if

    //删除前,栈顶指针指向当前栈顶元素的下一个位置
    //--S.top;之后,栈顶指针刚好指向被删除元素 
    //栈顶指针前移,保存被删除的元素值到e
    //e=*--S.top;  <=>  --S.top;   e=*(S.top);
    e = *--S.top;

    //操作成功 
    return OK; 
}//Pop_Sq

/*
    函数:GetTop
    参数:Stack S 顺序栈S
    返回值:成功获取顺序栈S栈顶元素的值后返回OK,否则返回ERRROR
    作用:用e返回栈顶元素的值,但是栈顶元素不做出栈操作 
*/
Status GetTop(Stack S, SElemType &e){

    //空栈没有栈顶元素,所以要先判断栈是否为空 
    //注意栈是否为空和栈是否存在不是一个概念,所以不可以用 
    //S.base != NULL判断栈是否为空 
    if(StackIsEmpty(S)) { 
         return ERROR;
    }//if

    //注意:栈顶指针指向栈顶元素的下一个位置
    e = *(S.top - 1);  
    /*   注意:此处不能使用“e = *(--S.top); ”的原因 
         1. --S.top自减操作改变了栈顶指针本身的指向,使得该指针向前移动一位,相当于删除了原来栈中的最后一个元素(最后一个元素出栈); 
         2. S.top-1 仅仅表示栈顶指针的上一个位置,并没有改变S.top的值,*(S.top-1)表示取栈顶指针前一个位置的值,即栈顶元素的值  
         3. 这两种写法造成的结果是不同的,如果是纯代数运算,两者没有差别,但在指向数组
            (顺序结构在C语言中是用一维数组描述的)的指针变量运算中,这两个表达式有特殊含义 
            在指针运算中,“指针变量-1 ”表示该指针变量所指位置的前一个位置,
            这种做法并不改变指针变量本身的值。 
            --指针变量   不仅使得该指针指向原来所指位置的上一个位置, 还修改了指针变量本身的值
            在栈中,栈顶指针和栈底指针所指向的位置有特殊的含义,故两者不等价。       
     */ 

     //操作成功 
     return OK; 
}//GetTop_Sq

/*
    函数:StackLength_Sq
    参数:Stack S 顺序栈S 
    返回值:若顺序栈S是空栈返回1,否返回0 
    作用:判断顺序栈S是否为空栈
*/
Status StackLength(Stack S){

    //栈的长度就是栈顶指针和栈底指针之间的元素个数 
    return (S.top - S.base); 
}//StackLength_Sq

/*
    函数:Print
    参数:ElemType e 被访问的元素 
    返回值:状态码,操作成功返回OK,操作失败返回ERROR 
    作用:访问元素e的函数,通过修改该函数可以修改元素访问方式,
          该函数使用时需要配合遍历函数一起使用。 
*/
Status Print_Stack(SElemType e){
    printf("%5d  ", e);
    return OK;
}//Print 

/*
    函数:StackTraverse_Sq
    参数:Stack S 顺序栈S 
          Status(* visit)(SElemType) 函数指针,指向元素访问函数。 
    返回值:状态码,操作成功返回OK,操作失败返回ERROR 
    作用:调用元素访问函数按出栈顺序完成顺序栈的遍历,但并未真正执行出栈操作 
*/
Status StackTraverse(Stack S, Status(* visit)(SElemType)) {

    //在空栈中执行遍历操作没有意义,所以要判断栈是否为空
    //注意栈是否为空和栈是否存在不是一个概念,所以不可以用 
    //S.base != NULL判断栈是否为空 
    if(StackIsEmpty(S)) {
        printf("此栈是空栈"); 
        return ERROR;
    }//if

    //调用元素访问函数依次访问栈中的每个元素
    for(int i = 0; i < StackLength(S); ++i){

        //调用元素访问函数,一旦访问失败则退出  
        if(!visit(S.base[i])) {
            return ERROR;
        }//if 
    }//for

    //输出换行,是控制台显示美观 
    printf("\n");

    //操作成功 
    return OK;
}//StackTraverse_Sq

你可能感兴趣的:(学习笔记)