1.课程设计内容与要求
用字符文件提供数据建立DAG(有向无环图)合适的存储结构。编写程序,输出所有可能的拓扑排序序列。要求输出的拓扑排序结果用顶点序号或字母表示。输出结果需存于字符文件。输出结果中应显示全部拓扑排序序列的数目。如果DAG存在环(即拓扑排序失败),输出结果中应显示拓扑排序序列的数目为0。
课程设计报告要求给出详细算法描述,在结论部分应分析算法的时间复杂度和空间复杂度,并给出分析的结果。
实验目的:掌握图的存储结构;掌握图的拓扑排序算法
2.程序设计报告
程序采用C++语言(非面向对象)进行开发。
2.1 总体设计
程序采用深度优先搜索算法配合上回溯算法生成所有可能得拓扑排序序列。
使用辅助入度数组和辅助判断是否访问数组进行节点的选择,利用递归实现每一组可能得序列遍历完后回溯到上一级递归的状态,再次选择不同的节点进行再次递归。
通过主函数main调用众多子函数完成功能。执行流程图如下图所示。
为了得到所有拓扑序列,采用回溯法,并用一个栈来存储拓扑序列的顺序。最开始以每一个节点作为起始点尝试能否递归下去(后面的每次递归也是尝试以每一个节点作为下一个拓扑排序节点),每一次递归都将此次入栈的节点在辅助判断是否被访问数组中的值改为false,并且将此节点所有的邻接点的入度在入度数组中的值全部减一。以这个状态进行下一层级的递归。当下一层级的所有递归都完成之后,将状态回溯,此节点的辅助判断是否被访问数组中的值改为true,并且将此节点所有的邻接点的入度在入度数组中的值全部加一。以此状态回溯到上一层级的递归,然后上一层级会尝试以另一节点开始递归。直到栈的长度等于节点总个数,说明栈中的序列是一个拓扑排序序列的逆序列,在输出函数中将逆序列反向输出到文件中,一个拓扑排序序列就被获取。然后向上一层级递归回溯。直到所有递归完成,所有可能的拓扑排序序列全部输出到文件中。再调用计算个数函数,得到文件中的行数(一行就是一个序列),最后将行数(个数)输出到文件中。
最后将动态申请的内存释放,做好收尾工作,所有的功能就实现完成。
2.2 详细数据结构设计
1. 栈存储结构
top:栈顶
base:栈底
size:空间大小
- 图的类型
DG:有向图
DN:有向网
UDG:无向图
UDN:无向网
- 图的邻接表节点
adjvex:数据域,以一个char类型作为数据类型
nextarc:下一个邻接点,类似于链表
- 顶点节点
data:数据域,以一个char类型作为数据类型
firstarc:第一个邻接点
- 图的邻接表
vexs:节点数组
vex_num:节点数
arc_num:弧数
kind:图类型
- 输入文件cd_input.txt的格式
第一行为节点数和弧数,第二行为各节点的数据,后面为各弧。
示例如下图所示。
- 输出文件cd_output.txt的格式
每一个拓扑序列占一行,最后是总拓扑序列的个数。
示例如下图所示。
- 辅助入度数组
indegree[i]:为以i为下标的G中节点的入度
- 辅助判断是否访问数组
visited[i]:为以i为下标的G中节点是否被访问,true为被访问,false为未被访问。
2.3 详细算法设计
1. 递归与回溯算法
递归是借用了深度优先搜素算法的特性,在每一层级的递归,都去判断每一个节点是否被访问过且入度是否为0,如果两个条件都满足,则从这个节点开始下一层级的递归。每一层级递归开始前都会先判断存储拓扑排序序列节点的栈的长度是否等于图的节点个数,如果相等,就调用函数将其输出到文件中。不等就遍历所有节点寻找符合之前两个条件的节点入栈,然后生成一个新状态:将此入栈节点的所有邻接点的入度减一,此节点在辅助判断是否访问数组中的值改为true。由这个新状态开始下一层级的递归。当以此节点开始的下面所有层级的递归结束后,将状态回溯至此节点未入栈之前的状态:将此节点的所有邻接点的入度加一,此节点在辅助判断是否访问数组中的值改为false,把此节点从栈中弹出。然后在当前层级遍历后面的节点,如有符合条件的节点,像刚刚那个节点一样开始下一层级的递归。
直到所有层级的递归完成后,全部可能得拓扑排序序列已输出到文件中,如果没有,文件中将是空白,然后得到文件中的行数,将序列数输出到文件中(空白文件将会输出0)。
- 通过文件创建图算法
对文件数据的读取是通过逐个读取每一个字符得到的。所以这对文件格式的要求十分严格。对于个数的读取,如节点个数、弧个数,是通过每个数据之间的空格判断一个数据是否读完,如果未读到空格则将之前读取到的数据乘以10再加上新读到的数据。读取到弧数之后通过一个for循环读取每一个弧的信息,每次读取以换行符结束,但读取最后一行数据时,为了迎合日常操作习惯,最后一行数据同时支持读到EOF结束。(即在字符文件中输入数据时,输完最后一行数据不必再次换行)同时注意,对于是否带权值的弧,读取代码有差异,故本版本代码只支持没有带权值的弧的读取。
注意:因文件输出方式为在尾部添加,故每次运行代码需先将cd_output.txt清空才行。
3.程序测试报告
注:本代码在vscode上测试时出现可能栈溢出问题,故在测试的时候选用的VS测试,VS测试未出现任何问题。其他编译器暂且不清楚。
测试实例(1)
程序输入 示意图
程序输出
测试实例(2)
程序输入 示意图
程序输出
测试实例(3)
程序输入 示意图
程序输出
4.结论
此处对整个程序的核心部分进行时间复杂度和空间复杂度的分析。核心部分即printALL函数和tuopu函数。
时间复杂度分析:算法使用了递归来实现深度优先搜索。对于每个节点,递归调用tuopu函数。在每次调用中,都会遍历图中所有节点,因此总体时间复杂度为O(V*V!),其中V为节点个数。
空间复杂度分析:
- 栈的空间复杂度:递归调用会使用系统调用栈,其深度取决于递归的深度。在最坏情况下,递归深度可能达到节点数,因此栈的空间复杂度为O(V)。
- 入度数组和访问数组:算法使用了两个数组来存储入度和是否访问信息,它们的空间复杂度都是O(V)。
- 其他辅助空间:除了栈、入度数组和访问数组外,还有一些常量级别的辅助空间。因此,总体空间复杂度为O(V)。
综合考虑,此算法的总体空间复杂度是O(V)。这里V表示节点数。
需要注意的是,时间复杂度中的V! 是由于算法的性质,此算法尝试了图中所有可能的拓扑排序。
在输入文件格式正确情况下,在输入文件中输入一个弧不带权值的有向图的节点数、弧数、每个弧的起始点和终止点,每个节点的数据域的信息通过此程序可以得到输入有向图的所有可能得拓扑排序序列。
5.源程序附录
#include <iostream>
#include <fstream>
#define Inisize 100 // 栈初始化大小
#define Increse 20 // 每次栈增加的大小
#define MAX_VERTEX_N 20 // 最大节点数
using namespace std;
// 自定义栈
typedef struct {
int* top;
int* base;
int size;
}myStack;
// 图的类型
typedef enum {
DG, // 有向图
DN, // 有向网
UDG, // 无向图
UDN // 无向网
}GraphKind;
// 边节点
typedef struct ArcNode
{
char adjvex; // 数据域
struct ArcNode* nextarc; // 下一个邻接点
}ArcNode;
// 节点
typedef struct
{
char data; // 数据域
ArcNode* firstarc; // 第一个邻接点
}VNode, AdjList[MAX_VERTEX_N];
// 图
typedef struct
{
AdjList vexs; // 节点数组
int vex_num; // 节点数
int arc_num; // 弧数
GraphKind kind; // 图类型
}ALGraph;
// 初始化栈
void Inistack(myStack& s) {
s.base = new int[Inisize];
s.top = s.base;
s.size = Inisize;
}
// 销毁栈
void Destory(myStack& s) {
delete s.base;
s.base = NULL;
s.top = NULL;
s.size = 0;
}
// 判断栈是否为空
bool Empty(myStack& s) {
return s.top == s.base;
}
// 获取栈的长度
int Length(myStack& s) {
return s.top - s.base;
}
// 入栈
void Push(myStack& s, int e) {
if (s.top - s.base >= s.size) {
int* a = new int[s.size + Increse];
for (int i = 0; i < s.top - s.base; i++) {
a[i] = s.base[i];
}
delete s.base;
s.base = a;
s.top = s.base + s.size;
s.size += Increse;
}
*(s.top++) = e;
}
// 出栈
int Pop(myStack& s) {
if (Empty(s)) {
exit(1);
}
return *(--s.top);
}
// 获取数据为e的节点下标
int locate(ALGraph G, char e) {
int i;
for (i = 0; i < G.vex_num; ++i) {
if (G.vexs[i].data == e) {
break;
}
}
return i;
}
// 从字符文件获取数据创建邻接表
void CreateUDN(ALGraph& G) {
ifstream ifs;
ifs.open("cd_input.txt", ios::in);
char ch;
int num = 0, index = 0, i;
// 读取节点数和弧数
while ((ch = ifs.get()) != ' ')
{
num = num * 10 + (ch - '0');
}
G.vex_num = num;
num = 0;
while ((ch = ifs.get()) != '\n')
{
num = num * 10 + (ch - '0');
}
G.arc_num = num;
num = 0;
// 初始化节点数组
while ((ch = ifs.get()) != '\n')
{
if (ch != ' ') {
G.vexs[index].data = ch;
G.vexs[index].firstarc = NULL;
++index;
}
}
// 创建弧节点
for (int k = 0; k < G.arc_num; ++k) {
char v1, v2;
while ((ch = ifs.get()) != ' ')
{
v1 = ch;
}
while ((ch = ifs.get()) != '\n')
{
if (ch == EOF) {
break;
}
v2 = ch;
}
// 将<v1, v2>添加到v1的邻接点中
ArcNode* temp = new ArcNode;
temp->adjvex = v2;
temp->nextarc = NULL;
i = locate(G, v1);
// 如果v1的邻接表为空
if (!G.vexs[i].firstarc) {
G.vexs[i].firstarc = temp;
}
else {
ArcNode* p = G.vexs[i].firstarc;
ArcNode* q = NULL;
while (p)
{
q = p;
p = p->nextarc;
}
q->nextarc = temp;
}
}
G.kind = DG;
ifs.close();
}
// 将输出拓扑排序序列输出到文件中
void to(myStack S, ALGraph G) {
ofstream ofs;
ofs.open("cd_output.txt", ios::app);
for (int i = 0; i < S.top - S.base; i++) {
ofs << G.vexs[S.base[i]].data << " ";
}
ofs << endl;
ofs.close();
}
// 获取拓扑序列的个数
void get_num() {
ofstream ofs;
ifstream ifs;
char ch;
int num = 0;
ifs.open("cd_output.txt", ios::in);
// 通过读取换行符的个数得到序列个数
while ((ch = ifs.get()) != EOF)
{
if (ch == '\n') {
++num;
}
}
ifs.close();
ofs.open("cd_output.txt", ios::app);
// 将个数输出到文件
ofs << num << endl;
ofs.close();
}
// 生成所有可能得拓扑排序序列
void tuopu(myStack& S, int* indegree, bool* visited, ALGraph G) {
ArcNode* p = NULL;
// 如果栈的长度是节点个数
if (Length(S) == G.vex_num)
{
to(S, G);
}
// 以每一个节点为起始点尝试进行拓扑排序
for (int i = 0; i < G.vex_num; i++)
{
// 如果入度为0且未被访问
if (indegree[i] == 0 && !visited[i])
{
visited[i] = true;
Push(S, i);
// 如果此节点有邻接点
if (G.vexs[i].firstarc != NULL)
{
p = G.vexs[i].firstarc;
// 将此节点的所有邻接点的入度-1
while (p != NULL)
{
indegree[locate(G, p->adjvex)]--;
p = p->nextarc;
}
}
// 递归再次进行拓扑排序
tuopu(S, indegree, visited, G);
// 此次递归完成后 将状态回溯 进行其他可能的拓扑排序
if (G.vexs[i].firstarc != NULL)
{
p = G.vexs[i].firstarc;
while (p != NULL)
{
indegree[locate(G, p->adjvex)]++;
p = p->nextarc;
}
}
visited[i] = false;
Pop(S);
}
}
}
// 获取所有的拓扑排序序列
void printALL(ALGraph G) {
// 入度数组
int* indegree = new int[G.vex_num];
myStack S;
// 是否访问数组
bool* visited = new bool[G.vex_num];
ArcNode* p = NULL;
Inistack(S);
// 初始化入度数组和是否访问数组
for (int i = 0; i < G.vex_num; ++i) {
indegree[i] = 0;
visited[i] = false;
}
for (int i = 0; i < G.vex_num; ++i) {
for (p = G.vexs[i].firstarc; p; p = p->nextarc) {
indegree[locate(G, p->adjvex)]++;
}
}
// 进行拓扑排序
tuopu(S, indegree, visited, G);
// 释放空间
delete[] indegree;
delete[] visited;
Destory(S);
}
// 释放创建图时动态申请的内存
void des(ALGraph& G) {
for (int i = 0; i < G.vex_num; ++i) {
ArcNode* p = G.vexs[i].firstarc;
ArcNode* q = NULL;
while (p)
{
q = p->nextarc;
delete p;
p = q;
}
}
}
int main(void) {
ALGraph G;
// 创建图
CreateUDN(G);
// 生成所有拓扑序列
printALL(G);
// 输出个数
get_num();
cout << "成功!!!" << endl;
des(G);
system("pause");
return 0;
}