在施工流程图或产品生产的流程图中,根据施工项目的顺序和产品生产顺序排序的过程,即拓扑序列。有向无环图的拓扑序列指的是:图中v1到v2有一条边,那么在拓扑序列中v1在v2之前,对于所有的顶点和边都适用。下面介绍两种求拓扑序列的方法:
(1)在有向图中选择一个没有前驱的顶点输出。
(2)从图中删除该顶点和所有以它为尾的弧。
重复上述过程,直至全部的顶点输出,或者当前图中不存在无前驱的顶点为止。后一种情况说明该图是带环的。用这种方法可以判断有向图是不是有向无环图。我们采用邻接表作为有向图的存储结构,我们设置一个indegree数组,数组中存储当前节点的入度数。入度为零的顶点即为没有前驱的顶点,为了避免重复检测入度为零的顶点,可另设置一个栈暂时存所有入度为零的顶点。
对于有n个顶点和e条边的有向图而言,建立各个顶点入度的时间复杂度为o(e);建立零入度顶点栈的时间复杂度为o(n);在有向无环图中,则每个顶点入栈一次,出栈一次,入度减一的操作在while循环中总共执行了e次所以总的时间复杂度为o(n + e)。
#include<iostream>
#include<vector>
#include<fstream>
#include<time.h>
#include<stack>
using namespace std;
stack<int> s;//定义一个栈
void readGraph();//读取文件,存储图
void findIndegree();//初始化每个顶点的入度
void topSort();//拓扑排序的函数
int nodeNum;//图中顶点数
int edgeNum;//图中边数
vector<vector<int>> mGraph;//图的存储结构
int *indegree;//存储每个节点的入度数
//从文件中读取数据,存储在图中
void readGraph()
{
fstream fin("E:\\myData\\cit-Patents.txt");
fin>>nodeNum>>edgeNum;
mGraph.resize(nodeNum);
indegree = new int[nodeNum]();
int num1, num2, node;
for(int i = 0; i < nodeNum; ++i)
{
fin>>num1>>num2;
mGraph[i].reserve(num2);
for(int j = 0; j < num2; ++j)
{
fin>>node;
mGraph[i].push_back(node);
}
}
fin.close();
}
//初始化每个顶点的入度
void findIndegree()
{
for(int i = 0; i < nodeNum; ++i)
{
int count = mGraph[i].size();
for(int j = 0; j < count; ++j)
{
indegree[mGraph[i][j]]++;
}
}
}
//拓扑排序
void topSort()
{
for(int i = 0; i < nodeNum; ++i)
{
if(!indegree[i])
{
s.push(i);
}
}
int num = 0;//记录入栈元素的个数
int v = 0;//设置栈顶元素
int count = 0;//当前顶点邻接点的个数
int val = 0;//当前元素的邻接点的值
while(!s.empty())//判断栈是否为空
{
v = s.top();//取出栈顶元素
s.pop();//出栈
count = mGraph[v].size();
for(int i = 0; i < count; ++i)
{
val = mGraph[v][i];
if(!(--indegree[val]))//当前元素的入度减一
{
s.push(val);//如果入度为零则入栈
}
}
num++;//顶点数加加
}
if(num < nodeNum)
{
cout<<"有向有环图"<<endl;
}
}
int main(void)
{
clock_t start, finish;
start = clock();
readGraph();
findIndegree();
topSort();
finish = clock();
cout<<"拓扑排序的时间:"<<(finish - start) / CLOCKS_PER_SEC * 1000<<endl;
system("pause");
return 0;
}
实验结果:单位(ms)
数据集为citeseerx
图中顶点的个数:1457057
图中边的条数:3002252
拓扑排序的时间:0
数据集为cit-Patents
图中顶点的个数:3774768
图中边的条数:16518947
拓扑排序的时间:2000
数据集为twitter
图中顶点的个数:18121168
图中边的条数:18359487
拓扑排序的时间:4000
数据集为web-uk
图中顶点的个数:22753644
图中边的条数:38184039
拓扑排序的时间:0
总结:时间主要消耗在stack<int> s中空间的动态开辟的过程。
方法二:
当有向图为无环时,也可以利用深度优先遍历进行拓扑排序,因为图中无环,则由图中某个顶点出发进行深度优先遍历时,最先退出DFS函数的顶点即出度为零的顶点,是拓扑有序序列中最后一个顶点,由此,按退出DFS的先后顺序记录下顶点的序列即为逆向的拓扑有序序列。时间复杂度和方法一的时间复杂度相同。
#include<iostream>
#include<vector>
#include<fstream>
#include<time.h>
#include<stack>
using namespace std;
void readGraph();//读取文件,存储图
void topSort();//拓扑排序
void DFS(int v);//从v顶点进行深度优先遍历
int nodeNum;//图中顶点数
int edgeNum;//图中边数
vector<vector<int>> mGraph;//图的存储结构
int *arr;//存储元素的拓扑序列
int num;
bool *visited;
//从文件中读取数据,存储在图中
void readGraph()
{
fstream fin("E:\\myData\\1M-3.txt");
//fstream fin("E:\\myData\\cit-Patents.txt");
fin>>nodeNum>>edgeNum;
arr = new int[nodeNum]();
num = nodeNum - 1;
mGraph.resize(nodeNum);
visited = new bool[nodeNum]();
int num1, num2, node;
for(int i = 0; i < nodeNum; ++i)
{
fin>>num1>>num2;
mGraph[i].reserve(num2);
for(int j = 0; j < num2; ++j)
{
fin>>node;
mGraph[i].push_back(node);
}
}
fin.close();
}
void topSort()
{
for(int i = 0; i < nodeNum; ++i)
{
if(!visited[i])
{
DFS(i);
}
}
}
void DFS(int v)
{
visited[v] = true;
int count = mGraph[v].size();
for(int i = 0; i < count; ++i)
{
if(!visited[mGraph[v][i]])
{
DFS(mGraph[v][i]);
}
}
arr[num--] = v;//存储元素的拓扑序列
}
int main(void)
{
readGraph();
clock_t start, finish;
start = clock();
DFSTraverse();
finish = clock();
cout<<"拓扑排序的时间:"<<(finish - start) / CLOCKS_PER_SEC * 1000<<endl;
system("pause");
return 0;
}
实验结果:单位(ms)
数据集为citeseerx
图中顶点的个数:1457057
图中边的条数:3002252
拓扑排序的时间:0
数据集为cit-Patents
图中顶点的个数:3774768
图中边的条数:16518947
拓扑排序的时间:0
数据集为twitter
图中顶点的个数:18121168
图中边的条数:18359487
拓扑排序的时间:0
数据集为web-uk
图中顶点的个数:22753644
图中边的条数:38184039
拓扑排序的时间:0
比较:方法一比方法二耗时,是因为方法一中创建stack<int> s对象时,s的空间是根据栈中元素的个数动态开辟的,这部分的时间比较耗时。拓扑排序的时间复杂度为o(n + e),基本在0ms之内就可以排序完成。