拓扑排序原理分析及js实现

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>∈R3>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之间的顺序呢?
由于ABindex值相同,但AB确实是不同的元素,因此我们无法确定他们的先后顺序,即AB不可比较,所以在A,B,C三个元素组成的关系不具备全序关系。但是一个偏序关系,如果我们默认,先出现的排在前面,那么我们就能比较A,B的关系了。我们排序就有C,A,B

稳定的算法:是对于存在上述情况的元素总能按照元素出现的先后顺序排列的算法

不稳定的算法:是对于上述情况,不能保证先出现的排在前面由此我们说,直接插入排序,冒泡排序,归并排序,基数排序是稳定的而shell排序,堆排序,快速排序直接选择排序是不稳定的

2. 拓扑排序

说明:本文图的构建方法及DFS算法可以参考 BFS,DFS 算法原理及js实现

图片描述

我们每天早上起床后穿衣的过程可以分为很多步骤,例如,穿内裤,穿裤子,穿内裤必须在穿裤子之前,同样的穿袜子必须在穿鞋子之前等等,戴手表和其它的任何一个动作之间都没有明显的关系,因此放在这个线性序列中的哪里都无所谓

  • 2.1. 拓扑排序定义

对于一个有向无环图G来说,拓扑排序就是对图G中的所有节点的一次线性排序,该排序满足如下条件,如果图G中包含边(u,v),则节点u一定在v的前面,可以将拓扑排序看作是将图的所有节点在一条直线上水平排开

3. Kahn算法

  • 3.1. 算法原理

对于一个有向无环AOV(顶点表示活动,边表示优先关系)图,我们重复执行以下两个步骤,直到不存在入度为0的顶点为止

(1)先择一个入度为0的顶点并输出

(2)从图中删除由该节点发出的所有边

这样得到的序列就是该图的拓扑排序,如果途中有环,则输出的顶点的数目小于图中节点的数目

  • 3.2. 算法描述

L一个用来存放已排序顶点的List
S一个用来存放如度为0的顶点List  
当S不为空时执行循环执行以下步骤
    从S中移除一个顶点(没有顺序要求,随意移除)n
    将n插入到L中
    循环遍历从n发出的边,对于所有的孩子节点m
        移除边<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; //保存边的权值
}

//数据结构 图-G
function 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; //保存边的权值
}

//数据结构 图-G
function 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);

运行结果

图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值