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

时间:2022-05-13 11:38:25

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

还是老规矩:

程序在码云上可以下载。
地址: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,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