小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
一、什么是拓扑排序
在图论中,拓扑排序是一个有向无环图(DAG)的所有顶点的线性序列。而且该序列必须满足下面两个条件:
- 每个顶点只能出现一次。 即如果存在一条A到B的路径,那么A节点在B节点前面,那么B节点不能在A节点前面。
- 只有有向无环图才有拓扑排序,如果不是DAG图的话就没有拓扑排序。
例如,下面这个图:
它是一个 DAG 图,那么如何写出它的拓扑排序呢?这里说一种比较常用的方法:
- 从 DAG 图中选择入度为的顶点(即没有节点指向该节点)并输出。
- 从图中删除这个节点以及由他发出的有向边,同时针对该节点指向的子节点的入度减一。
- 重复第一步和第二步,直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。
于是,得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。
通常,一个有向无环图可以有一个或多个拓扑排序序列。
二、实现方法
下面,我们将用两种方法来实现我们的拓扑排序:
- Kahn算法
- 基于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 {};
}