数据结构编程笔记二十二:第七章 图 拓扑排序算法的实现
来源:互联网 发布:ios开发数据存储 编辑:程序博客网 时间:2024/06/18 08:12
上次我们介绍了图的最短路径算法的实现,这次介绍基于邻接表的拓扑排序算法的实现。
还是老规矩:
程序在码云上可以下载。
地址: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 <stdio.h> //使用了标准库函数 #include <stdlib.h> //使用了动态内存分配函数 #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,21,31,43,23,54,56,46,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 0Press 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
- 数据结构编程笔记二十二:第七章 图 拓扑排序算法的实现
- 数据结构编程笔记二十:第七章 图 最小生成树算法的实现
- 数据结构编程笔记二十一:第七章 图 最短路径算法的实现
- 数据结构编程笔记二十三:第七章 图 关键路径算法的实现
- 数据结构编程笔记二十七:第十章 内排序 各种排序算法的实现
- 算法:图的拓扑排序-数据结构(23)
- 数据结构看书笔记(十二)--排序算法
- (十二)数据结构之快速排序算法实现
- 《数据结构与算法》学习笔记30 有向图的拓扑排序
- 【数据结构】拓扑排序算法
- 数据结构--拓扑排序算法
- 拓扑排序的算法实现
- 数据结构编程笔记二十八:第十章 内排序 各种排序算法效率比较
- 数据结构-----图的拓扑排序和关键路径算法
- 数据结构与算法之有向图的拓扑排序
- java数据结构与算法-有向图的拓扑排序
- 大话数据结构 code 第七章 09拓扑排序_TopologicalSort
- 【学习笔记----数据结构21-图的拓扑排序】
- SQLMAP详解+实战操作命令
- 从long polling 到 websockets
- jboss全局配置1
- Django安装与测试
- Linux下延时与内核定时器
- 数据结构编程笔记二十二:第七章 图 拓扑排序算法的实现
- PAT--1025. PAT Ranking
- 动态数组链表
- 初识java
- 浅谈web语义化
- Caffe框架初步理解
- 664Strange Printer
- HDU 5869 Different GCD Subarray Query (离线处理 树状数组)
- 前端性能优化