1. 偏序和全序的概念
- 1.1. 偏序
设R是集合A上的一个二元关系,若R满足下列三个性质则称R为A上的偏序关系
自反性:对任意x∈A,有<x,x>∈R反对称性:对任意的x,y∈A,如果<x,y>∈R,且<y,x>∈R,则必有x=y传递性:对任意x,y,z∈A,若<x,y>∈R,<y,z>∈R,则必有<x,z>∈R通俗解释:自然数之间的"大于等于"
是一个偏序关系,例如自然数的一个子集A={1,2,3}
"大于等于"
的一个子集R={<1,1>,<2,2>,<3,3>,<3,2>,<2,1>,<3,1>}
对于自反的解释是1=1,2=2,3=3;
对于反对称性,<3,2>,<2,1>,<3,1>∈R
,但关系R中不存在元素<2,3>,<1,2><1,3>
因为2<3,1<2,1<3
,对于传递<3,2>,<2,1>∈R
即3>2,2>1
所以3>1
即<3,1>∈R
- 1.2. 全序
全序是偏序的一个子集,即集合中任意两个元素之间都有明确的"序"的关系也就是下面的性质
完全性:集合中任意两个元素之间都有明确的"序"的关系,由此可见完全性包含了自反性,任意就包含了元素和元素自己通俗解释:是偏序但不是全序,设集合A={1,2,3,b},b=2i+1
;由于b是一个复数,所以其它的三个元素都不可以和它来比较大小
- 1.3. 算法的稳定性
如果我们要对下列数组的元素按照index的大小进行排序 [{id:a,index:1},{id:b,index:1},{id:c,index:2}]
,我们设第一个为A
,第二个为B
,第三个为C
, 我们应该如何确定A和B之间的顺序呢?
A
,B
的index
值相同,但A
和B
确实是不同的元素,因此我们无法确定他们的先后顺序,即A
和B
不可比较,所以在A,B,C三个元素组成的关系不具备全序关系。但是一个偏序关系,如果我们默认,先出现的排在前面,那么我们就能比较A,B的关系了。我们排序就有C,A,B
稳定的算法:是对于存在上述情况的元素总能按照元素出现的先后顺序排列的算法
不稳定的算法:是对于上述情况,不能保证先出现的排在前面由此我们说,直接插入排序,冒泡排序,归并排序,基数排序是稳定的而shell排序,堆排序,快速排序直接选择排序是不稳定的
2. 拓扑排序
说明:本文图的构建方法及DFS算法可以参考
我们每天早上起床后穿衣的过程可以分为很多步骤,例如,穿内裤,穿裤子,穿内裤必须在穿裤子之前,同样的穿袜子必须在穿鞋子之前等等,戴手表和其它的任何一个动作之间都没有明显的关系,因此放在这个线性序列中的哪里都无所谓
- 2.1. 拓扑排序定义
对于一个有向无环图G来说,拓扑排序就是对图G中的所有节点的一次线性排序,该排序满足如下条件,如果图G中包含边(u,v),则节点u一定在v的前面,可以将拓扑排序看作是将图的所有节点在一条直线上水平排开
3. Kahn算法
- 3.1. 算法原理
对于一个有向无环AOV(顶点表示活动,边表示优先关系)图,我们重复执行以下两个步骤,直到不存在入度为0的顶点为止
(1)先择一个入度为0的顶点并输出(2)从图中删除由该节点发出的所有边
这样得到的序列就是该图的拓扑排序,如果途中有环,则输出的顶点的数目小于图中节点的数目
- 3.2. 算法描述
L一个用来存放已排序顶点的ListS一个用来存放如度为0的顶点List 当S不为空时执行循环执行以下步骤 从S中移除一个顶点(没有顺序要求,随意移除)n 将n插入到L中 循环遍历从n发出的边,对于所有的孩子节点m 移除边如果m的入度为0,则将m放入S中如果循环结束后图中还有边则说明图中有环否则L是拓扑排序的结果
- 3.3. 算法的js实现
//数据结构 邻接链表-顶点function Vertex() { if (!(this instanceof Vertex)) return new Vertex(); this.color = this.WHITE; //初始为 白色 this.pi = null; //初始为 无前驱 this.d = this.INFINITY; //初始为 无穷大 this.edges = null; //由顶点发出的所有边 this.value = null; //节点的标识 this.data = null; //节点的数据 this.incoming = 0; //节点的入度}Vertex.prototype = { constructor: Vertex, WHITE: 'white', //白色 GRAY: 'gray', //灰色 BLACK: 'black', //黑色 INFINITY: null, //d 为 null 时表示无穷大}//数据结构 邻接链表-边function Edge() { if (!(this instanceof Edge)) return new Edge(); this.index = null; //边所依附的节点的位置 this.sibling = null; this.w = null; //保存边的权值}//数据结构 图-Gfunction Graph() { if (!(this instanceof Graph)) return new Graph(); this.graph = []; this.dict = {}; //字典 用来映射标节点的识符和数组中的位置}Graph.prototype = { constructor: Graph, //这里加进来的已经具备了边的关系 addNode: function(node) { this.graph.push(node); }, getNode: function(index) { return this.graph[index]; }, //创建图的 节点 initVertex: function(vertexs) { //创建节点并初始化节点属性 value for (let value of vertexs) { let vertex = Vertex(); vertex.value = value.value; vertex.data = value.data; this.graph.push(vertex); } //初始化 字典 for (let i in this.graph) { this.dict[this.graph[i].value] = i; } }, //建立图中 边 的关系 initEdge: function(edges) { for (let field in edges) { let index = this.dict[field]; //从字典表中找出节点在 graph 中的位置 let vertex = this.graph[index]; //获取节点 vertex.edges = createLink(0, edges[field].length, edges[field], this.dict, this.graph); } }}//创建链表,返回链表的第一个节点function createLink(index, len, edges, dict, vertexs) { if (index >= len) return null; let edgeNode = Edge(); edgeNode.index = dict[edges[index].id]; //边连接的节点 用在数组中的位置表示 参照字典 vertexs[edgeNode.index].incoming = vertexs[edgeNode.index].incoming + 1; //设置节点的入度 edgeNode.w = edges[index].w; //边的权值 edgeNode.sibling = createLink(++index, len, edges, dict, vertexs); //通过递归实现 回溯 return edgeNode;}// a内裤 b袜子 c手表 d裤子 e鞋 f腰带 g衬衣 h领带 i 夹克vertexs = [{value: 'a', data: '内裤'}, {value: 'b', data: '袜子'}, {value: 'c',data: '手表'}, {value: 'd', data: '裤子'}, {value: 'e',data: '鞋'}, {value: 'f', data: '腰带'}, {value: 'g',data: '衬衣'}, {value: 'h', data: '领带'}, {value: 'i',data: '夹克'}];var edges = { a: [{id: 'd', w: 1 }, {id: 'e', w: 1 }], b: [{id: 'e', w: 1}], c: [], d: [{id: 'e', w: 1 }, {id: 'f', w: 1 }], e: [], f: [{id: 'i', w: 1}], g: [{id: 'f', w: 1 }, {id: 'h', w: 1 }], h: [{id: 'i', w: 1}], i: []}//kahn算法function kahn(g) { let s = []; //用于存放入度为0的顶点 let l = []; //用来存放已经排好序的顶点 //初始化set 将图中所有入度为0的节点加入到set中 for(let v of g.graph) { if(v.incoming==0) s.push(v); } while(s.length > 0) { let u = s.shift(); l.push(u); if (u.edges == null) continue; let sibling = u.edges; while (sibling != null) { let index = sibling.index; let n = g.getNode(index); n.incoming = n.incoming - 1; //删除边 if(n.incoming == 0) s.push(n); //入度为0的放入s sibling = sibling.sibling; } } return l;}var g = Graph();g.initVertex(vertexs);g.initEdge(edges);var r = kahn(g);console.log(r);
运行结果
4. 基于DFS的拓扑排序算法
- 4.1. 算法原理
原理:拓扑排序的次序与顶点的完成时间恰好相反
对于拓扑排序,我们要做的是保证对于任意一条边(u,v)
,节点u一定出现在节点v前面。
DFS
算法的每个节点,我们都有一个发现时间d
,一个访问时间f
,当DFS
运行完成时,对于图中的任意一条边(u,v)
,都有 u.f>v.f
,所以拓扑排序的次序与顶点的完成时间恰好相反。 当DFS
在图上运行时,探索到任意一条边(u,v)
时,u
为灰色,所以v
要么是白色,要么是黑色,如果v
是白色,则v
一定早于u
被访问,即 u.f>v.f
,当v为黑色时,此时v
已经被访问过,而u
还为灰色,即u
没有被访问,所以v依然早于u
被访问,仍有 u.f>v.f
,由此可见上述结论成立
- 4.2. js实现
基于以上结论,我们用DFS
实现拓扑排序,只需要在节点 的f被设置值即节点被访问后,将其加入一个后进先出队列中(调用unshift
方法始终向数组的头部添加新元素)L
中,当DFS
运行结束后,L
中的元素就是经过拓扑排序的结果
//数据结构 邻接链表-顶点function Vertex() { if (!(this instanceof Vertex)) return new Vertex(); this.color = this.WHITE; //初始为 白色 this.pi = null; //初始为 无前驱 this.d = this.INFINITY; //初始为 无穷大 this.edges = null; //由顶点发出的所有边 this.value = null; //节点的标识 this.data = null; //节点的数据 this.incoming = 0;}Vertex.prototype = { constructor: Vertex, WHITE: 'white', //白色 GRAY: 'gray', //灰色 BLACK: 'black', //黑色 INFINITY: null, //d 为 null 时表示无穷大}//数据结构 邻接链表-边function Edge() { if (!(this instanceof Edge)) return new Edge(); this.index = null; //边所依附的节点的位置 this.sibling = null; this.w = null; //保存边的权值}//数据结构 图-Gfunction Graph() { if (!(this instanceof Graph)) return new Graph(); this.graph = []; this.dict = {}; //字典 用来映射标节点的识符和数组中的位置}Graph.prototype = { constructor: Graph, //这里加进来的已经具备了边的关系 addNode: function(node) { this.graph.push(node); }, getNode: function(index) { return this.graph[index]; }, //创建图的 节点 initVertex: function(vertexs) { //创建节点并初始化节点属性 value for (let value of vertexs) { let vertex = Vertex(); vertex.value = value.value; vertex.data = value.data; this.graph.push(vertex); } //初始化 字典 for (let i in this.graph) { this.dict[this.graph[i].value] = i; } }, //建立图中 边 的关系 initEdge: function(edges) { for (let field in edges) { let index = this.dict[field]; //从字典表中找出节点在 graph 中的位置 let vertex = this.graph[index]; //获取节点 vertex.edges = createLink(0, edges[field].length, edges[field], this.dict, this.graph); } }}//创建链表,返回链表的第一个节点function createLink(index, len, edges, dict, vertexs) { if (index >= len) return null; let edgeNode = Edge(); edgeNode.index = dict[edges[index].id]; //边连接的节点 用在数组中的位置表示 参照字典 vertexs[edgeNode.index].incoming = vertexs[edgeNode.index].incoming + 1; //设置节点的入度 edgeNode.w = edges[index].w; //边的权值 edgeNode.sibling = createLink(++index, len, edges, dict, vertexs); //通过递归实现 回溯 return edgeNode;}// a内裤 b袜子 c手表 d裤子 e鞋 f腰带 g衬衣 h领带 i 夹克vertexs = [{value: 'a', data: '内裤'}, {value: 'b', data: '袜子'}, {value: 'c',data: '手表'}, {value: 'd', data: '裤子'}, {value: 'e',data: '鞋'}, {value: 'f', data: '腰带'}, {value: 'g',data: '衬衣'}, {value: 'h', data: '领带'}, {value: 'i',data: '夹克'}];var edges = { a: [{id: 'd', w: 1 }, {id: 'e', w: 1 }], b: [{id: 'e', w: 1}], c: [], d: [{id: 'e', w: 1 }, {id: 'f', w: 1 }], e: [], f: [{id: 'i', w: 1}], g: [{id: 'f', w: 1 }, {id: 'h', w: 1 }], h: [{id: 'i', w: 1}], i: []}function DFS(g) { let t = 0; let l =[]; for (let v of g.graph) { if (v.color == v.WHITE) DFSVisit(g, v); } function DFSVisit(g, v) { t = t + 1; v.d = t; v.color = v.GRAY; let sibling = v.edges; while (sibling != null) { let index = sibling.index; let n = g.getNode(index); if (n.color == n.WHITE) { n.pi = v; DFSVisit(g, n); //先纵向找 } sibling = sibling.sibling; //利用递归的特性来回溯 } v.color = v.BLACK; t = t + 1; v.f = t; //设置完成时间 l.unshift(v); //拓扑排序的次序与顶点的完成时间恰好相反 } console.log(l)}var g = Graph();g.initVertex(vertexs);g.initEdge(edges);DFS(g);
运行结果