【数据结构与算法 | 图篇】拓扑排序

1. 概念

拓扑排序是是一种针对有向无环图进行的排序方法。它将图中所有顶点排成一个线性序列,使得对于图中的每一条有向边(u, v),顶点u在序列中都出现在顶点v之前。

适用范围:

  • 拓扑排序只适用于有向无环图。

结果非唯一:

  • 对于同一个图,可能存在多个合法的拓扑排序序列。

不存在拓扑排序的情况:

  • 如果图中含有环,则无法进行拓扑排序,因为环内的节点无法确定先后。

2. 思路1(基于队列):

初始化:

找到所有入度为0的节点,并将它们加入到队列。

迭代过程:

从队列中取出一个节点并输出;对于该节点指向的所有邻接节点,减少它们的入度值。如果某个邻接节点的入度变为0,则将其入队。

结束条件:

队列为空时停止。如果输出的节点数量等于图中总节点数,则拓扑排序成功;否则说明图中有环。

3. Java代码实现

@Test
    public void test() {
        Vertex v1 = new Vertex("网页基础");
        Vertex v2 = new Vertex("JAVA基础");
        Vertex v3 = new Vertex("数据库");
        Vertex v4 = new Vertex("JAVA WEB");
        Vertex v5 = new Vertex("Spring框架");
        Vertex v6 = new Vertex("微服务框架");
        Vertex v7 = new Vertex("实战项目");
        // JDK版本太低,用不了of方法
        v1.edges = new ArrayList<>();
        v1.edges.add(new Edge(v4));

        v2.edges = new ArrayList<>();
        v2.edges.add(new Edge(v4));

        v3.edges = new ArrayList<>();
        v3.edges.add(new Edge(v5));

        v4.edges = new ArrayList<>();
        v4.edges.add(new Edge(v5));

        v5.edges = new ArrayList<>();
        v5.edges.add(new Edge(v6));

        v6.edges = new ArrayList<>();
        v6.edges.add(new Edge(v7));

        v7.edges = new ArrayList<>();

        // 拓扑排序:
        List<Vertex> list = new ArrayList<>();
        list.add(v1);
        list.add(v2);
        list.add(v3);
        list.add(v4);
        list.add(v5);
        list.add(v6);
        for(Vertex v : list){
            for(Edge i : v.edges){
                i.linked.InDegree++;
            }
        }
        Deque<Vertex> queue = new LinkedList<>();
        // 将入度为0的节点入队
        for (Vertex v : list){
            if(v.InDegree == 0){
                queue.offer(v);
            }
        }
        while(!queue.isEmpty()){
            Vertex poll = queue.poll();
            System.out.print(poll.name + "  ");
            for(Edge i : poll.edges){
                i.linked.InDegree--;
                if(i.linked.InDegree == 0){
                    queue.offer(i.linked);
                }
            }
        }
    }


public class Vertex {
    // 顶点的名字,用来区分顶点
    String name;
    // 与该顶点有关的边的集合
    List<Edge> edges;
    // 判断是否已经被遍历
    boolean visited = false;
    // 入度
    int InDegree;
    public Vertex(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

节点情况如下:

de5a8ff3ac5d48ef95a9cc4c07fe9966.png

4. 有环的情况判断1:

如果输出的节点数量等于图中总节点数,则拓扑排序成功;否则说明图中有环.

举个例子:假如"微服务框架"和"实战项目"两个节点间存在环,思想是如果入度为0就输出。网页基础和java基础先输出,然后是数据库和java web,然后是Spring框架,Spring框架被输出后,微服务框架的入度减1,此时微服务框架的入度为1,且删不了,所以连带 着微服务框架和实战也就都输出不了。此时输出的节点数肯定是小于图中总的节点数。

5. 思路2(基于队列)

通过深度优先搜索遍历图中的所有节点,如果遇到一个没有被访问过的节点,就递归的访问它的所有邻接节点,然后将该节点添加到结果列表的开头。

6. 代码实现

@Test
    public void test() {
        Vertex v1 = new Vertex("网页基础");
        Vertex v2 = new Vertex("JAVA基础");
        Vertex v3 = new Vertex("数据库");
        Vertex v4 = new Vertex("JAVA WEB");
        Vertex v5 = new Vertex("Spring框架");
        Vertex v6 = new Vertex("微服务框架");
        Vertex v7 = new Vertex("实战项目");
        // JDK版本太低,用不了of方法
        v1.edges = new ArrayList<>();
        v1.edges.add(new Edge(v4));

        v2.edges = new ArrayList<>();
        v2.edges.add(new Edge(v4));

        v3.edges = new ArrayList<>();
        v3.edges.add(new Edge(v5));

        v4.edges = new ArrayList<>();
        v4.edges.add(new Edge(v5));

        v5.edges = new ArrayList<>();
        v5.edges.add(new Edge(v6));

        v6.edges = new ArrayList<>();
        v6.edges.add(new Edge(v7));

        v7.edges = new ArrayList<>();

        List<Vertex> list = new ArrayList<>();
        list.add(v1);
        list.add(v2);
        list.add(v3);
        list.add(v4);
        list.add(v5);
        list.add(v6);
        list.add(v7);
        Deque<String> stack = new LinkedList<>();
        for(Vertex v : list){
            dfs(v, stack);
        }
        System.out.println(stack);
    }
    public static void dfs(Vertex v, Deque<String> stack){
        // 发现这个节点已经被拓扑排序遍历过,返回
        if(v.status == 2){
            return;
        }
        // 发现这个节点未放入栈中,此时遍历到了,发现有环
        if(v.status == 1){
            throw new RuntimeException("出现环");
        }
        // v.status = 1表示这个节点正在处理中
        v.status = 1;
        for(Edge i : v.edges){
            dfs(i.linked, stack);
        }
        v.status = 2;
        stack.push(v.name);
    }


public class Vertex {
    // 顶点的名字,用来区分顶点
    String name;
    // 与该顶点有关的边的集合
    List<Edge> edges;
    // 判断是否已经被遍历
    boolean visited = false;
    // 入度
    int InDegree;
    // 0表示未访问,1表示访问中,2表示在拓扑排序
    int status;
    public Vertex(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

7. 有环情况的判断2:

基于队列实现判断有环的这个情况就更简单了。

还是假如"微服务框架"和"实战项目"两个节点间存在环,此时一直递归,"微服务框架"的状态被标记为了1,然后继续递归它的邻接点,也就是遍历"实战项目"该节点,如果正常情况下,可以更新状态放入栈以后可以开始返回了,但如果有环(有从"微服务框架"指向"实战项目"的有向边,也有"实战项目"指向"微服务框架"的有向边),此时"实战项目"该节点存在邻接点"微服务框架",递归"微服务框架"该顶点,发现这个顶点的状态是1(并没有处理完拓扑排序),说明有环.

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值