拓扑排序

882 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

一、什么是拓扑排序

在图论中,拓扑排序是一个有向无环图(DAG)的所有顶点的线性序列。而且该序列必须满足下面两个条件:

  • 每个顶点只能出现一次。 即如果存在一条A到B的路径,那么A节点在B节点前面,那么B节点不能在A节点前面。
  • 只有有向无环图才有拓扑排序,如果不是DAG图的话就没有拓扑排序。

例如,下面这个图:

在这里插入图片描述

它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:

  1. 从 DAG 图中选择入度为的顶点(即没有节点指向该节点)并输出。
  2. 从图中删除这个节点以及由他发出的有向边,同时针对该节点指向的子节点的入度减一。
  3. 重复第一步和第二步,直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。

在这里插入图片描述

于是,得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。

通常,一个有向无环图可以有一个或多个拓扑排序序列。

二、实现方法

下面,我们将用两种方法来实现我们的拓扑排序:

  1. Kahn算法
  2. 基于DFS的拓扑排序算法

在代码中,函数的输入的第一个参数是节点个数,第二个参数表示两个节点连接的关系,例如[1,0]代表了有一条有向边从0指向1。

2.1 Kahn算法

Kahn的算法的思路其实就是我们上面展示的拓扑排序的实现,我们先使用一个栈保存入度为0的顶点,然后输出栈顶元素并且将和栈顶元素有关的边删除,减少和栈顶元素有关的顶点的入度数量并且把入度减少到0的顶点也入栈。

//建立图的结构,其中graphic[i]表示节点i指向的节点
	vector<vector<int>> graphic;
	//记录该节点是否被访问
	vector<int> visited;
	//栈依次记录了结果
	stack<int> s;
	vector<int> indegree;
	bool ok;

	vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
		ok = true;
		vector<int> res;
		graphic.resize(numCourses);
		visited.resize(numCourses);
		indegree.resize(numCourses);
		for (int i = 0; i < prerequisites.size(); i++)
		{
			vector<int> thispre = prerequisites[i];
			int hou = thispre[0];
			int qian = thispre[1];
			graphic[qian].push_back(hou);
			//根据图构建每个节点的入度
			indegree[hou]++;
		}
		//将入度为0的节点压入栈中
		for (int i = 0; i < indegree.size(); i++)
		{
			if (indegree[i] == 0 && visited[i] == 0)
			{
				s.push(i);
				visited[i] = 1;
			}
		}
		//如果图是一个环直接返回空表
        if (s.empty())
			return {};
		do {
			int topnode = s.top();
			visited[topnode] = 1;
			res.push_back(topnode);
			s.pop();
			for (int j = 0; j < graphic[topnode].size(); j++)
			{
				indegree[graphic[topnode][j]]--;
			}
			//将入度为0的节点压入栈中
			for (int i = 0; i < indegree.size(); i++)
			{
				if (indegree[i] == 0 && visited[i] == 0)
				{
					s.push(i);
					visited[i] = 1;
				}
			}
		} while (!s.empty());
		//判断图中是否有环
		for (int i = 0; i < visited.size(); i++)
		{
			if (visited[i] == 0)
			{
				return {};
			}
		}
		return res;
	}

2.2 基于DFS算法

对于图中的任意一个节点,它在搜索的过程中有三种状态,即:

  • 「未搜索」:我们还没有搜索到这个节点;
  • 「搜索中」:我们搜索过这个节点,但还没有回溯到该节点,即该节点还没有入栈,还有相邻的节点没有搜索完成);
  • 「已完成」:我们搜索过并且回溯过这个节点,即该节点已经入栈,并且所有该节点的相邻节点都出现在栈的更底部的位置,满足拓扑排序的要求。

通过上述的三种状态,我们就可以给出使用深度优先搜索得到拓扑排序的算法流程,在每一轮的搜索搜索开始时,我们任取一个「未搜索」的节点开始进行深度优先搜索。

我们将当前搜索的节点 uu 标记为「搜索中」,遍历该节点的每一个相邻节点 vv:

  • 如果 vv 为「未搜索」,那么我们开始搜索 vv,待搜索完成回溯到 uu;
  • 如果 vv 为「搜索中」,那么我们就找到了图中的一个环,因此是不存在拓扑排序的;
  • 如果 vv 为「已完成」,那么说明 vv 已经在栈中了,而 uu 还不在栈中,因此 uu 无论何时入栈都不会影响到 (u, v)(u,v) 之前的拓扑关系,以及不用进行任何操作。

当 uu 的所有相邻节点都为「已完成」时,我们将 uu 放入栈中,并将其标记为「已完成」。

在整个深度优先搜索的过程结束后,如果我们没有找到图中的环,那么栈中存储这所有的 nn 个节点,从栈顶到栈底的顺序即为一种拓扑排序。

//建立图的结构,其中graphic[i]表示节点i指向的节点
	vector<vector<int>> graphic;
	//记录该节点是否被访问
	vector<int> visited;
	//栈依次记录了结果
	stack<int> s;
	bool ok;

	//numCourse
	//thisnode
	void dfs(int thisnode)
	{
		visited[thisnode] = 1;//遇见了这个节点,就设为1
		for (int i = 0; i < graphic[thisnode].size(); i++)
		{
			if (visited[graphic[thisnode][i]] == 0)
			{
				dfs(graphic[thisnode][i]);
			}
			else if(visited[graphic[thisnode][i]] == 1)//如果一个节点的没有遍历完成,但在他的子节点的dfs过程中又遇到该节点,那么显然存在了环
			{
				ok = false;
			}
		}
		visited[thisnode] = 2;//这个节点的子节点都遍历完成后,设置为2
		s.push(thisnode);
	}


	vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
		ok = true;
		vector<int> res;
		graphic.resize(numCourses);
		visited.resize(numCourses);
		for (int i = 0; i < prerequisites.size(); i++)
		{
			vector<int> thispre = prerequisites[i];
			int hou = thispre[0];
			int qian = thispre[1];
			graphic[qian].push_back(hou);
		}
		for (int i = 0; i < numCourses; i++)
		{
			if (visited[i] == 0)
			{
				dfs( i);
			}
			
		}
		while(!s.empty())
		{
			res.push_back(s.top());
			s.pop();
		}
		if (ok == true)
			return res;
		else
			return {};
	}