1. 概述
拓扑排序是将一个有向无环图的所有顶点进行线性序列。且该序列必须满足下面两个条件:
每个顶点出现且只出现一次
若存在一条从
顶点A
到顶点B
的路径,那么在序列中顶点A
出现在顶点B
的前面
拓扑排序并不会改变图的结构,它只是在图结构中找到一条有先后顺序的路径。当图结构为有向无环图时,才有拓扑排序一说
所谓先后顺序的路径,例如:当我们学习一套视觉班的课程时
首先要具备
OpenGL基础
和3D数学
的基础,才能学习OpenGL ES
的学科在掌握
OpenGL ES
之后,才能进一步完成GPUImage
的学习当前面的学科掌握后,最终才能完成
Metal
的学习
示例中,想要完成整体课程学习,我们就要按照各个学科的先后顺序,层层递进,依次学习。而这个学习路径,就是一个具有先后顺序的路径
在⼀个表示⼯程的有向图中,⽤顶点表示活动,⽤弧表示活动之间的优先关系。这样有向图为顶点表示活动的⽹,我们称之为AOV
⽹(Activity On Vertex Network
)
在有向图中,边被称之为“弧”
图结构不一定是线性的,但我们按照任务执行过程中的先后关系,最终要输出一个线性排序
拓扑排序只是在任务执行过程中,选择的策略不同,它并不是唯一的答案。例如:以下两种路径:
C1 → C2 → C3 → C4 → C5
C2 → C1 → C3 → C4 → C5
2. 拓扑序列
设G = (V, E)
是⼀个具有n
个顶点的有向图,V
中的顶点序列V1
、V2
…Vn
。若满⾜从顶点Vi
到Vj
有⼀条路径,则在顶点序列Vi
必须在Vj
之前,则我们称这样的顶点序列为拓扑序列
所谓拓扑排序,其实就是对⼀个有向图构造拓扑序列的过程
拓扑序列构造过程会产⽣两个结果:
如果此⽹中的全部顶点被输出,则说明它不存在环(回路)的
AOV
⽹如果输出的顶点数少了,哪怕仅少了⼀个,也说明这个⽹存在环(回路),不是
AOV
⽹
3. 算法解析
对一张图进行拓扑排序时,我们无法预知该图是否可以完整拓扑排序。我们会根据执行结构,判断该图是否具备拓扑排序的条件
判断逻辑:当所有顶点都能按照先后顺序输出,该图为无环的AOV
网,它符合拓扑排序的条件
由于拓扑排序选择的策略不同,它的答案并不是唯一的
3.1 存储方式
在开始拓扑排序的算法之前,我们还要确定AOV
网图的存储方式。因为在算法的设计过程中,我们会进行一些删除操作,所以采用邻接表存储更具优势
3.2 数据结构
当一个顶点没有前驱结点,也就是入度为0
时,就可以将它作为起始顶点
- 入度:指有向图中某点作为图中边的终点的次数之和
所以,我们根据上述逻辑,可以设计出邻接表顶点的数据结构:
in
:存储顶点的入度data
:顶点域,存储顶点信息firstedge
:指向边表头结点
3.3 算法思路
从AOV
⽹中选择⼀个⼊度为0
的顶点输出,然后从顶点表中删去此顶点,并删除以此顶点为尾的弧。继续重复此步骤,直到输出全部顶点或AOV
⽹中不存在⼊度为0
的顶点为⽌
这个算法实现过程中,我们需要借助另⼀个数据结构:栈,帮助我们规避在每次查找时,都要遍历AOV
图中的顶点表,查找是否存在⼊度为0
的顶点
创建⼀个栈,⽤来存储⼊度为
0
的顶点序号遍历
AOV
图中顶点表,判断⼊度为0
的顶点全部⼊栈
4. 算法分析
以此图为例:
将案例中的AOV
网图,采用邻接表存储后的结果:
【初始化】将入度为0
的顶点对应的索引值入栈。将count
值初始为0
,用于统计输出的顶点个数
【第一次执行】遍历stack
栈,栈顶gettop = 3
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V3
连接的顶点V2
、V13
,并将其⼊度-1
,则V2
、V13
的⼊度值都为1
。 它的⽬的是为了将V3
顶点的弧删除
【第二次执行】 遍历stack
栈,栈顶gettop = 1
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V1
连接的顶点V2
、V4
、V8
,并将其⼊度-1
,则V2
的⼊度为0
,V4
、V8
的⼊度值都为1
。它的⽬的是为了将V1
顶点的弧删除。因为V2
的⼊度为0
,则V2
⼊栈
【第三次执行】 遍历stack
栈,栈顶gettop = 2
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V2
连接的顶点V5
、V6
、V9
,并将其⼊度-1
,则V6
的⼊度为0
,V5
的⼊度值为2
,V9
的⼊度值为1
。它的⽬的是为了将V2
顶点的弧删除。因为V6
的⼊度为0
,则V6
⼊栈
【第四次执行】 遍历stack
栈,栈顶gettop = 6
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V6
连接的顶点V5
,并将其⼊度-1
,则V5
的⼊度为1
。它的⽬的是为了将V6
顶点的弧删除
【第五次执行】 遍历stack
栈,栈顶gettop = 0
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V0
连接的顶点V4
、V5
、V11
,并将其⼊度-1
,则V4
、V5
的⼊度都为0
,V11
的⼊度为1
。它的⽬的是为了将V0
顶点的弧删除。因为V4
、V5
的⼊度为0
,则V4
、V5
⼊栈
【第六次执行】 遍历stack
栈,栈顶gettop = 4
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V4
连接的顶点V7
,并将其⼊度-1
,则V7
的⼊度为1
。它的⽬的是为了将V4
顶点的弧删除
【第七次执行】 遍历stack
栈,栈顶gettop = 5
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V5
连接的顶点V8
、V12
,并将其⼊度-1
,则V8
、V12
的⼊度都为1
。它的⽬的是为了将V5
顶点的弧删除。因为V8
、V12
的⼊度为0
,则V8
、V12
⼊栈
【第八次执行】 遍历stack
栈,栈顶gettop = 8
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V8
连接的顶点V7
,并将其⼊度-1
,则V7
的⼊度为0
。它的⽬的是为了将V8
顶点的弧删除。因为V7
的⼊度为0
,则V7
⼊栈
【第九次执行】 遍历stack
栈,栈顶gettop = 7
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,由于V7
没有连接的顶点,继续下一轮循环
【第十次执行】 遍历stack
栈,栈顶gettop = 12
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V12
连接的顶点V9
,并将其⼊度-1
,则V9
的⼊度为0
。它的⽬的是为了将V12
顶点的弧删除。因为V9
的⼊度为0
,则V9
⼊栈
【第十一次执行】 遍历stack
栈,栈顶gettop = 9
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V9
连接的顶点V10
、V11
,并将其⼊度-1
,则V10
、V11
的⼊度都为0
。它的⽬的是为了将V9
顶点的弧删除。因为V10
、V11
的⼊度为0
,则V10
、V11
⼊栈
【第十二次执行】 遍历stack
栈,栈顶gettop = 10
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,找到对应的V10
连接的顶点V13
,并将其⼊度-1
,则V13
的⼊度为0
。它的⽬的是为了将V10
顶点的弧删除。因为V13
的⼊度为0
,则V13
⼊栈
【第十三次执行】 遍历stack
栈,栈顶gettop = 13
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,由于V13
没有连接的顶点,继续下一轮循环
【第十四次执行】 遍历stack
栈,栈顶gettop = 11
出栈,并count++
循环针对栈顶gettop
顶点对应的弧链表进⾏遍历,由于V11
没有连接的顶点,继续下一轮循环
【停止遍历】此时stack
栈为空栈,停止遍历,程序执行结束
5. 代码实现
创建AovGraphLinked
类,定义边表中的顶点结构,顶点表中的顶点结构,以及AOV
网图的数据结构
class AovGraphLinked {
fileprivate class Node {
var adj_vex_index : Int?;
var data : String?;
var next : Node?;
}
fileprivate class VNode {
var inDegree : Int;
var data : String?;
var firstEdge : Node?;
init(){
self.inDegree = 0;
}
}
fileprivate class Graph {
var adjList : [VNode]?;
var arc_num : Int;
var node_num : Int;
init(){
self.arc_num = 0;
self.node_num = 0;
}
}
fileprivate var graph : Graph;
init(){
self.graph = Graph();
}
}
实现创建方法:
class AovGraphLinked {
func create(nodes : [String], arcs : [[Int]]) {
self.graph.arc_num = arcs.count;
self.graph.node_num = nodes.count;
self.graph.adjList = [VNode]();
for data in nodes {
let vNode = VNode();
vNode.data = data;
self.graph.adjList?.append(vNode);
}
for arc in arcs {
let i = arc[0];
let j = arc[1];
let node = Node();
node.adj_vex_index = j;
node.data = self.graph.adjList![j].data;
self.graph.adjList![j].inDegree += 1;
let vNode = self.graph.adjList![i];
node.next = vNode.firstEdge;
vNode.firstEdge = node;
}
}
}
实现打印方法:
class AovGraphLinked {
func traverse() -> String {
var str : String = "";
for vNode in self.graph.adjList! {
var node = vNode.firstEdge;
str += "入度:\(vNode.inDegree),\(vNode.data!)"
while(node != nil){
str += " -> \(node!.data!)";
node = node?.next;
}
str += "\n";
}
return str;
}
}
实现拓扑排序方法:
class AovGraphLinked {
fileprivate var stack : LinearStack<Int>?;
func topologicalSort() -> String {
var str : String = "";
self.stack = LinearStack<Int>.init(capacity: self.aovGraph.adjList!.count);
// 1、将入度为0的顶点对应的索引值入栈
for i in 0 ..< self.aovGraph.adjList!.count {
let aovNode = self.aovGraph.adjList![i];
if(aovNode.inDegree > 0){
continue;
}
self.stack!.push(element: i);
}
var count = 0;
// 2、遍历stack
while(!self.stack!.isEmpty()) {
// 将数据依次出栈,并且count++
let i = self.stack!.pop()!;
count += 1;
// 通过索引在顶点表中获取顶点
let aovNode = self.aovGraph.adjList![i];
str += "\(aovNode.data!) -> ";
// 获取顶点指向边表中的头结点
var node = aovNode.firstEdge;
// 遍历边表中的顶点
while(node != nil){
// 将顶点的入度-1
let aovNode = self.aovGraph.adjList![node!.adj_vex_index!];
aovNode.inDegree -= 1;
// 如果-1后顶点的入度为0,将其索引值入栈
if(aovNode.inDegree == 0){
self.stack!.push(element: node!.adj_vex_index!);
}
node = node?.next;
}
}
// 3、判断输出的顶点是否为正确的拓扑排序
if(count < self.aovGraph.node_num){
str = "执行失败,图结构非AOV网图";
}
return str;
}
}
按照示例中的AOV
网图逻辑,使用代码创建邻接表,并打印拓扑排序的结果:
let nodes = ["V0", "V1", "V2", "V3", "V4", "V5", "V6", "V7", "V8", "V9", "V10", "V11", "V12", "V13"];
let arcs = [[0, 4], [0, 5], [0, 11],
[1, 2], [1, 4], [1, 8],
[2, 5], [2, 6], [2, 9],
[3, 2], [3, 13],
[4, 7],
[5, 8], [5, 12],
[6, 5],
[8, 7],
[9, 10], [9, 11],
[10, 13],
[12, 9]];
let aovGraphLinked = AovGraphLinked();
aovGraphLinked.create(nodes: nodes, arcs: arcs);
print(aovGraphLinked.traverse());
print("拓扑排序:\(aovGraphLinked.topologicalSort())");
-------------------------
//输出以下内容:
入度:0,V0 -> V11 -> V5 -> V4
入度:0,V1 -> V8 -> V4 -> V2
入度:2,V2 -> V9 -> V6 -> V5
入度:0,V3 -> V13 -> V2
入度:2,V4 -> V7
入度:3,V5 -> V12 -> V8
入度:1,V6 -> V5
入度:2,V7
入度:2,V8 -> V7
入度:2,V9 -> V11 -> V10
入度:1,V10 -> V13
入度:2,V11
入度:1,V12 -> V9
入度:2,V13
拓扑排序:V3 -> V1 -> V2 -> V6 -> V0 -> V4 -> V5 -> V8 -> V7 -> V12 -> V9 -> V10 -> V13 -> V11 ->
总结
概述:
拓扑排序是将一个有向无环图的所有顶点进行线性序列。且该序列必须满足下面两个条件:
每个顶点出现且只出现一次
若存在一条从
顶点A
到顶点B
的路径,那么在序列中顶点A
出现在顶点B
的前面
拓扑排序并不会改变图的结构,它只是在图结构中找到一条有先后顺序的路径。当图结构为有向无环图时,才有拓扑排序一说
拓扑序列:
设
G = (V, E)
是⼀个具有n
个顶点的有向图,V
中的顶点序列V1
、V2
…Vn
。若满⾜从顶点Vi
到Vj
有⼀条路径,则在顶点序列Vi
必须在Vj
之前,则我们称这样的顶点序列为拓扑序列拓扑序列构造过程会产⽣两个结果:
如果此⽹中的全部顶点被输出,则说明它不存在环(回路)的
AOV
⽹如果输出的顶点数少了,哪怕仅少了⼀个,也说明这个⽹存在环(回路),不是
AOV
⽹
算法解析:
当所有顶点都能按照先后顺序输出,该图为无环的
AOV
网,它符合拓扑排序的条件由于拓扑排序选择的策略不同,它的答案并不是唯一的
在算法的设计过程中,我们会进行一些删除操作,所以采用邻接表存储更具优势
当一个顶点没有前驱结点,也就是入度为
0
时,就可以将它作为起始顶点- 入度:指有向图中某点作为图中边的终点的次数之和
算法思路:
从
AOV
⽹中选择⼀个⼊度为0
的顶点输出,然后从顶点表中删去此顶点,并删除以此顶点为尾的弧。继续重复此步骤,直到输出全部顶点或AOV
⽹中不存在⼊度为0
的顶点为⽌这个算法实现过程中,我们需要借助另⼀个数据结构:栈,帮助我们规避在每次查找时,都要遍历
AOV
图中的顶点表,查找是否存在⼊度为0
的顶点创建⼀个栈,⽤来存储⼊度为
0
的顶点序号遍历
AOV
图中顶点表,判断⼊度为0
的顶点全部⼊栈