最近写了个作业,需求是“输出有向无环图的所有拓扑序列”。经过一番折腾,终于完成。之后回想起来觉得应该写一份简单易懂的文章,帮助其他人理解。

细细分析一下,可以发现这次任务我们需要解决以下问题:

(1)如何通过邻接表创建一个图

(2)什么是拓扑排序

(3)如何对图进行拓扑排序

(4)如何输出所有的拓扑序列

对这些问题,我想先一一从算法方面先解决,之后再上代码。

(1)如何通过邻接表创建一个图?

对图有过了解的朋友们应该都知道,图大体来说,有邻接矩阵和邻接表这两种主流的储存结构。

所谓邻接矩阵(不带权值),就是假设图中有n个顶点,采用n*n的矩阵A来表示。

这样的储存结构,大家应该也都发现了,邻接矩阵的优点在于可以通过二维数组A[i][j]直接引用边(i,j),即很容易确定顶点之间的关系。但是同样,缺点也很明显。它消耗的内存空间是顶点数的平方,如果图的边较少的话,就会浪费大量空间。并且,在手动进行输入的时候,也很麻烦,所以,为了偷懒节约空间。我还是选择了邻接表。

那么什么又是邻接表呢?就是将邻接点构成链表

如图,1的邻接点有2,3,4三个,所以可以让1的邻接点指向2,3,4.这样就可以轻松的取到邻接点了。如果是有向图的话,则需要加一个表示入度的变量就行了。

那了解这样一个储存结构之后,怎样创建一个图呢?首先,我们要有一个顶点表,装着顶点。然后需要一个储存顶点信息的结构体vnode,里面装上节点的度,节点的数据和指向邻接点的第一条边。

struct vnode
{enode* firstedge;int data;int du;
};

之后,也需要一个结构体enode,它储存啥呢?它储存当前顶点邻接顶点在顶点表里的下标。还有一个enode型的指针,指向下一个邻接点。

struct enode
{int adjvex;enode* next;
};

如上图,在class graph里面的私有成员里,定义一个vnode型顶点表(数组),表中储存的就是上图的v0到v4。而vnode型顶点中enode型的指针,指向第一个邻接点,第一个邻接点的adjex则储存着邻接顶点在顶点表中的下标,enode型指针next则指向下一个邻接点。

这样之后,就可以定义类了。

class graph
{
public:graph();//无参构造函数int local(int val);//定位函数void create();//创建邻接表的图void show();void tuopu();void tuopu(vector<int> order);//bool tuopusort();
private:vnode vertex[maxsize];int numedge, numvertex;
};

如图可见,类的私有成员里定义了我所说的顶点表和顶点和边的数量。之后写一个create函数,在里面就可以开始创建图了。

很多人不知道图应该怎么输入!

其实,只要在你在纸上画出一个有向图,然后在create函数中让用户自行输入边数和顶点数,就可以开始创建了。具体代码如下:

void graph::create()
{enode* e, * p, * q;cout << "请输入图的顶点数和边数:" << endl;cin >> numvertex >> numedge;for (int i = 0; i < numvertex; i++){cout << "第" << i + 1 << "个顶点:";cin >> vertex[i].data;vertex[i].firstedge = NULL;}for (int i = 0; i < numedge; i++){cout << "请分别输入" << numedge << "条边上的信息(包括两段顶点和方向)(左指向右)"; \int v1, v2;cin >> v1 >> v2;int a = local(v1);int b = local(v2);if (vertex[a].firstedge == NULL){e = new enode;e->adjvex = b;e->next = NULL;vertex[a].firstedge = e;//e->du = 1;vertex[b].du += 1;}else{p =vertex[a].firstedge;while (p->next != NULL){p = p->next;}e = new enode;e->adjvex = b;e->next = NULL;p->next = e;//e->du = 1;vertex[b].du += 1;}}
}

其中local函数定义如下(为了返回顶点对应下标的函数):

int graph::local(int val)
{for (int i = 0; i < numvertex; i++){if (vertex[i].data == val)return i;}return -1;
}

下面解释create函数

在输入顶点数和边数之后,要开始输入顶点的值,于是构建了个for循环分别输入。但是因为不知道顶点间的关系,所以firstedge置为空,度也置为0。之后要求用户输入那numedge条边的两端分别是哪两个顶点,以此来建立顶点间的联系。因为建立的是有向图,所以我要求用户将被指的顶点放在后面,主动指向的顶点放在前面。

在用户输入顶点关系之后,就可以对被指向顶点进行入度加一的操作,并且可以将顶点指向调整,指向邻接点。

到此,就完成了图的创建。成果如下:

到此,解决了第一个问题。

(2)什么是拓扑排序

拓扑排序的定义就是由一个集合的偏序得到一个全序的过程。

听起来有些复杂,但是用算法表示就是:

①找一个入度为0的顶点v输出;

②删除v及相关弧;(将v的后继顶点减一即可)

③重复①,②直到无入度为0的顶点为止。

案例如下:

这样最后就实现了四组拓扑序列。

(3)如何对图进行拓扑排序

        知道了拓扑排序的内容,那么如何对一个构建好的图进行拓扑排序呢?

我们知道,求解方法设计到入度,因而需要保存各顶点的入度,那怎么通过入度实现拓扑排序呢?我们可以将入度为0 并且未输出的顶点放在一个结构里,需要时直接取出,当新的入度为0的顶点出现时,也要将其存放进来。对此,我们采用栈结构储存入度为0的顶点,可以通过包含stack头文件调用STL中的栈,而我选择的是自己构建一个栈。有了栈之后,接下来的操作就简单了:

   FIRST:初始化栈为空

SECOND:将AOV网中所有入度为0的顶点放入栈S中

THIRD:若S不空,则pop,并输出pop掉的顶点V

FOURTH:将V的每个后继顶点的入度减一,如果其中有减少一之后入度变成了0,则将其放入S中

 FIFTH:转到THIRD继续执行

下面分别给出伪码和可运行代码

伪码:

      Bool    toposort(graph G){get_Ind(G,Ind);stack s;int count=0;for(i=1;i<=n;i++)if ( Ind[i] == 0 )    s.push(i);while ( !s.empty() ){v=s.pop();      cout<<v;    count++;w=firstadj(G,v);while(w!=0){Ind[w]--;if ( Ind[w] == 0 )    s.push(w);w=nextadj(G,v,w);}}if ( count == n )   return TURE: else  returen FALSE;}

可运行代码:

bool graph::tuopusort()
{stack<int> S;for (int i = 0; i < numvertex; i++){if (vertex[i].du == 0)//挑出入度为0的顶点入栈S.push(i);}int count = 0;//计数判断是否为无环图while (!S.empty()){int tmp;tmp = S.top();S.pop();++count;if (vertex[tmp].firstedge != NULL){vertex[vertex[tmp].firstedge->adjvex].du--;//度为0的顶点的邻接顶点入度减一if (vertex[vertex[tmp].firstedge->adjvex].du == 0)//如果入度减一之后变成零{S.push(vertex[tmp].firstedge->adjvex);//入栈}while (vertex[tmp].firstedge->next != NULL)//把当前顶点的所有邻接顶点都遍历一遍{vertex[tmp].firstedge = vertex[tmp].firstedge->next;vertex[vertex[tmp].firstedge->adjvex].du--;if (vertex[vertex[tmp].firstedge->adjvex].du == 0)S.push(vertex[tmp].firstedge->adjvex);  }}}if (count < numvertex)//如果count小于numvertex说明为有环图,返回falsereturn false;elsereturn true;
}

(代码解释见注释)

运行结果如下:

这时候我们发现,已经输出了拓扑序列了,别得意。需求是输出所有拓扑序列...

咋办呢???

(4)如何输出所有的拓扑序列

开门见山吧,回溯法

首先先简单的说一下回溯法是啥:回溯法简单来说就是按照深度优先的顺序,穷举所有可能性的算法,但是回溯算法比暴力穷举法更高明的地方就是回溯算法可以随时判断当前状态是否符合问题的条件。一旦不符合条件,那么就退回到上一个状态,省去了继续往下探索的时间。
最基本的回溯法是在解空间中穷举所有的解。

就比如在上面求解拓扑序列时输出的这四条线就是通过回溯法。具体怎么实现呢?

首先我们要建立一个bool类型的数组visit[maxsize],干啥用呢?对应每个顶点有没有被访问过,初始化为false。

之后再想想用啥储存结果并输出呢?思来想去我觉得用vector性价比更高点,于是

void graph::tuopu(vector<int> order)

就这样定义了个输出所有拓扑序列的函数。

首先我们知道,当有一个拓扑序列完成时要直接输出,然后再进行第二个拓扑序列的处理。所以首先要设置输出的代码段。

之后遍历顶点表,找到入度为0并且没有被访问过的顶点。之后呢?肯定要将visit[i]变成true呀!然后将该顶点的data放进容器内待输出,之后就是和(3)中描述的相同的,邻接顶点入度减一的操作。

但是!在这之后,与之不同的是再次调用tuopu(order)形成递归,为的就是能够一次输出所有序列。递归完成后,则说明已经输出了拓扑序列,现在则需要开始回溯,咋回溯呢?就是要将访问过的顶点统统设置为未访问,并且将减一的度数加回来。之后将原本存入容器的顶点值pop出来,就完成了代码的编写了。

void graph::tuopu(vector<int> order)
{if (order.size() == numvertex){for (int i = 0; i < numvertex; i++){cout << order[i] << " ";}cout << endl;}for (int i = 0; i < numvertex; i++){if (vertex[i].du == 0 && !visit[i]){visit[i] = true;order.push_back(vertex[i].data);if (vertex[i].firstedge!=NULL){enode* p = vertex[i].firstedge;while (p != NULL){vertex[p->adjvex].du--;p = p->next;}}tuopu(order);if (vertex[i].firstedge != NULL){enode* q = vertex[i].firstedge;while (q != NULL){vertex[q->adjvex].du++;q = q->next;}}visit[i] = false;order.pop_back();}}}

运行结果如下:

那么到这里,就完成了任务的需求啦!!!

完整源代码如下:

stack.h

#pragma once
template<class T>
class stack
{
public:stack(int max=1000);~stack();bool empty();bool full();T top();bool push(T x);bool pop();
private:T* p;int max_len;int count;
};

stack.cpp

#include"stack.h"
#include<iostream>
using  namespace std;
template<class T>stack<T>::stack(int max)
{max_len = max;count = 0;p = new T[max_len];
}
template<class T>
stack<T>::~stack()
{delete[]p;
}
template<class T>
bool stack<T>::empty()
{return count == 0;
}
template<class T>
bool stack<T>::full()
{return count == max_len - 1;
}
template<class T>
T stack<T>::top()
{if (empty()){cout << "栈为空" << endl;return -1;}else{T x;x = *(p + count);return x;}
}
template<class T>
bool stack<T>::push(T x)
{if (full())return false;else{count++;*(p + count) = x;return true;}
}
template<class T>
bool stack<T>::pop()
{if (empty())return false;else{count--;return true;}
}

graph.h

#pragma once
#include<iostream>
#include<vector>
using namespace std;
#define elementtype int
#define maxsize 20
//邻接链表初始化图
struct enode
{int adjvex;enode* next;
};
struct vnode
{enode* firstedge;int data;int du;
};class graph
{
public:graph();//无参构造函数//graph(adjlink v[maxsize], int _current);//有参构造函数//~graph();//析构函数int local(int val);void create();//创建邻接表的图void show();void tuopu();void tuopu(vector<int> order);bool tuopusort();private:vnode vertex[maxsize];int numedge, numvertex;
};

graph.cpp

#include"graph.h"
#include<vector>
#include"stack.h"
#include"stack.cpp"
vector<int> result;
bool visit[maxsize];
int ans[maxsize];
int graph::local(int val)
{for (int i = 0; i < numvertex; i++){if (vertex[i].data == val)return i;}return -1;
}
graph::graph()
{numedge = 0, numvertex = 0;for (int i = 0; i < maxsize; i++){vertex[i].data = 0;vertex[i].du = 0;vertex[i].firstedge = NULL;}
}
void graph::create()
{enode* e, * p, * q;cout << "请输入图的顶点数和边数:" << endl;cin >> numvertex >> numedge;for (int i = 0; i < numvertex; i++){cout << "第" << i + 1 << "个顶点:";cin >> vertex[i].data;vertex[i].firstedge = NULL;}for (int i = 0; i < numedge; i++){cout << "请分别输入" << numedge << "条边上的信息(包括两段顶点和方向)(左指向右)"; \int v1, v2;cin >> v1 >> v2;int a = local(v1);int b = local(v2);if (vertex[a].firstedge == NULL){e = new enode;e->adjvex = b;e->next = NULL;vertex[a].firstedge = e;//e->du = 1;vertex[b].du += 1;}else{p =vertex[a].firstedge;while (p->next != NULL){p = p->next;}e = new enode;e->adjvex = b;e->next = NULL;p->next = e;//e->du = 1;vertex[b].du += 1;}}
}
void graph::show()
{for (int i = 0; i < numvertex; i++){cout << "第"<<i+1<<"个节点信息及后继为:" << endl;cout << vertex[i].data << "->";if (vertex[i].firstedge != NULL){cout << vertex[vertex[i].firstedge->adjvex].data << "->";while (vertex[i].firstedge->next != NULL){vertex[i].firstedge = vertex[i].firstedge->next;cout << vertex[vertex[i].firstedge->adjvex].data << "->";}}cout << "NULL";cout << endl;}cout << endl;}
void graph::tuopu()
{vector<int> order;for (int i = 0; i < numvertex; i++){visit[i] = false;}tuopu(order);
}
void graph::tuopu(vector<int> order)
{if (order.size() == numvertex){for (int i = 0; i < numvertex; i++){cout << order[i] << " ";}cout << endl;}for (int i = 0; i < numvertex; i++){if (vertex[i].du == 0 && !visit[i]){visit[i] = true;order.push_back(vertex[i].data);if (vertex[i].firstedge!=NULL){enode* p = vertex[i].firstedge;while (p != NULL){vertex[p->adjvex].du--;p = p->next;}}tuopu(order);if (vertex[i].firstedge != NULL){enode* q = vertex[i].firstedge;while (q != NULL){vertex[q->adjvex].du++;q = q->next;}}visit[i] = false;order.pop_back();}}}
bool graph::tuopusort()
{stack<int> S;for (int i = 0; i < numvertex; i++){cout << "第" << i+1 << "个点的入度为:" << vertex[i].du << endl;if (vertex[i].du == 0)S.push(i);}int count = 0;while (!S.empty()){int tmp;tmp = S.top();S.pop();cout << vertex[tmp].data << "->";++count;if (vertex[tmp].firstedge != NULL){vertex[vertex[tmp].firstedge->adjvex].du--;if (vertex[vertex[tmp].firstedge->adjvex].du == 0){int temp = vertex[tmp].firstedge->adjvex;S.push(temp);}while (vertex[tmp].firstedge->next != NULL){vertex[tmp].firstedge = vertex[tmp].firstedge->next;vertex[vertex[tmp].firstedge->adjvex].du--;if (vertex[vertex[tmp].firstedge->adjvex].du == 0)S.push(vertex[tmp].firstedge->adjvex);  }}}if (count < numvertex)return false;elsereturn true;
}

main.cpp

#include"graph.h"
void test()
{graph map;map.create();//map.show();//cout << "拓扑序列分别为:" << endl;map.tuopusort();
}
int main()
{test();system("pause");return 0;}

希望看官们能够动动小手,给鄙人留下一个赞~

使用邻接表输出所有拓扑序列(新手必看!通俗易懂!绝对详细!!)相关推荐

  1. php 纯文本输出_关于PHP的语法介绍,新手必看

    作为一款最好用的PHP集成开发工具,它具有智能PHP代码编辑器,代码质量分析,简易代码导航和搜索.那么对于刚开始接触它的新手小白来说,它的入门语法对于你们来说一定很重要,那么就请跟我一起来看看吧. P ...

  2. 新手必看的编程介绍,帮你推荐学习方案!

    新手必看的编程介绍,帮你推荐学习方案! VB是什么? ) y0 {6 G# G; j3 B VB 是Visual Basic编程语言 * B. O2 G# z) O1 |- g8 `4 ^ 编写计算机 ...

  3. python新手入门代码-新手必看:手把手教你入门 Python

    原标题:新手必看:手把手教你入门 Python 本文为 AI 研习社编译的技术博客,原标题 : Learning Python: From Zero to Hero 翻译 |永恒如新的日常校对 | 酱 ...

  4. PTA 7-124 A+B 输入输出练习 (I) ------新手必看!!

    PTA 7-124 A+B 输入输出练习 (I) ------新手必看!! 分数 10 作者 蔡尚真 单位 绍兴文理学院元培学院 假设你有两只手(这个好像不用假设 - -!),在你的面前有一堆的糖,左 ...

  5. Web.config配置文件详解(新手必看)

    Web.config配置文件详解(新手必看) 花了点时间整理了一下ASP.NET Web.config配置文件的基本使用方法.很适合新手参看,由于Web.config在使用很灵活,可以自定义一些节点. ...

  6. wdatepicker设置时间范围_Mac新手必看,这些设置可以让你的Mac更好用!

    如果你刚买了一台新的Mac,不免要对Mac做一些设置,现为你提供一些Mac常用的设置技巧,让你的Mac更加好用. Mac新手必看,这些设置可以让你的Mac更好用! - macw下载站​www.macw ...

  7. html css 命名规范,浅谈css命名规则(新手必看)

    头:header 内容:content/container 尾:footer 导航:nav 侧栏:sidebar 栏目:column 页面外围控制整体布局宽度:wrapper 左右中:left rig ...

  8. 在conda虚拟环境中配置cuda+cudnn+pytorch深度学习环境(新手必看!简单可行!)

    本人最近接触深度学习,想在服务器上配置深度学习的环境,看了很多资料后总结出来了对于新手比较友好的配置流程,创建了一个关于深度学习环境配置的专栏,包括从anaconda到cuda到pytorch的一系列 ...

  9. 影视后期制作新手必看!超多片头模板免费分享

    影视后期制作新手必看!超多片头模板免费分享 今年疫情影响,电影院到现在没有还是不能营业,自己也实业了,每天闲的无聊,经过朋友的推荐,自己闲下来学习了一些后期制作技术,学习是比较困难的,其中酸甜苦辣只有 ...

最新文章

  1. 【设计模式】四:开放-封闭原则
  2. python写入中文、用utf-16编码得出二进制字节串_在python中将unicode UTF16数据写入文件时出现问题...
  3. 初学者不建议月python吗_9.python入门
  4. 11.6 mpstat:CPU信息统计
  5. SentOS 7防火墙配置与端口增删改查的命令
  6. iOS UIDatePicker设置为中文的方法
  7. activiti启动流程实例
  8. sql 时间查询 /sql中判断更新或者插入/查询一年所有双休日
  9. c# zxing条形码设置密度_C# 中 ZXing.Net 怎样突破 条形码 多识别 数量限制
  10. 基于Spring Security Role过滤Jackson JSON输出内容
  11. 琴岛学院java书_​师生共品书 传承优秀传统文化 琴岛学院第十三届金秋读书节开幕...
  12. java 龟兔赛跑预测_Java实现 蓝桥杯VIP 基础练习 龟兔赛跑预测
  13. 《旅行青蛙》的代码揭秘,攻略,体验
  14. JavaScript 使用js修改页面元素
  15. 使用 Task.Wait()?立刻死锁(deadlock)
  16. 【620】【信息管理学基础】【01信息与信息管理】
  17. 【微信小程序付款转二维码付款】
  18. 恒等于符号html编码,latex中恒等于号≡怎么打?
  19. 长安奔奔Mini维修手册
  20. 短视频内容重复,如何伪原创处理

热门文章

  1. python设计游戏使用手机操作_如何使用python控制手机(以微信游戏跳一跳为例)
  2. 三沙第三方检测实验室建设要点科普
  3. 华为如何实施数字化转型(附PPT)
  4. C++处理点在椭圆上的问题
  5. 关于RHEL支持T10 PI的情况
  6. 计算机小白对计算机的认识
  7. [Unity实战]虚线的绘制
  8. 冒泡排序 以及利用函数升序 降序
  9. TOGAF®9认证通过人数100,000里程碑
  10. 地图跑步轨迹回放动画实现