图系列(二)图的遍历与拓扑排序

图的遍历与拓扑排序

好了,接下来是重中之重图的遍历。图遍历是很多其他算法的基础,比如Dijkstra算法。

图的遍历

广度优先遍历使用queue,深度优先遍历使用stack。简单来说,两种方式都是将元素从容器中取出,将子节点加入进去,只是“取”的方式不太一样而已。

广度优先遍历

广度优先遍历的关键是需要借助一个队列。代码如下:

class Solution {
    List<Integer>[] adjs;
    public void bfs(int n, int[][] edges) {
        // 初始化邻接链表
        // ...
        Queue<Integer> q = new LinkedList<>();
        q.add(0); // 假设起点从0开始, 通过广度优先遍历可以遍历所有元素。
        
        while(!q.isEmpty()) {
            int i = q.poll();
            for (Integer adj : adjs[i]) {
                q.add(adj);
            }
        }
    }
}

这里做了两个假设: 1. 起点从0开始,就是说0是树的根节点;2. 从0可以遍历到所有元素,并且只有一棵树。相应的,我们需要解除这两个假设:首先,我们可以从特定的点开始,例如入度为0的点;或者,使用一个数组,存储每个点的访问状态,如果元素还没有访问,下一轮继续访问。举个栗子:

class Solution {
    List<Integer>[] adjs;
    boolean[] visited;
    public void bfs(int n, int[][] edges) {
        // 初始化邻接链表
        // ...
        visited = new boolean[n];

        Queue<Integer> q = new LinkedList<>();
        for(int i = 0 ; i < n;i++) {
            if (!visited[i]) {
                q.add(i);
                while (!q.isEmpty()) {
                    int head = q.poll();
                    visited[head] = true;
                    for (Integer adj : adjs[head]) {
                        if (!visited[adj]) {
                            q.add(adj);
                        }
                    }
                }
            }
        }
    }
}

参见leetcode题目: 课程表

深度优先遍历

深度优先遍历,主要利用递归。当然,我们不能够一直避开环的问题。这里介绍《算法与数据结构》中典型的处理方式—— 涂色。没有访问过的元素涂成白色;已经访问过的元素涂成黑色;正在访问的,在同一条递归调用中的元素涂成灰色。代码如下:

class Solution {
    private List<Integer>[] adjs;
    private Color[] colors;
    public boolean containsLoop(int n, int[][] edges) {
        // 初始化
        adjs = new List[n];
        colors = new Color[n];
        for(int i = 0; i < n; i++) {
            adjs[i] = new LinkedList<>();
            colors[i] = Color.WHITE;
        }
        for(int i = 0 ; i<edges.length;i++) {
            int[] edge = edges[i];
            int from = edge[0];
            int to = edge[1];
            adjs[from].add(to);
        }

        // 开始访问
        for (int i = 0; i < n;i++) {
            if (colors[i] == Color.WHITE) {
                if(dfsVisit(i)) return true;
            }
        }
        return false;
    }

    // 判断是否存在环,存在立即返回true
    // 不存在返回false。
    private boolean dfsVisit(int i) {
        colors[i] = Color.GRAY;
        for (Integer adj: adjs[i]) {
            if (colors[adj] == Color.GRAY) return true; // 访问到同一递归调用中已经访问过的元素。
            if (colors[adj] == Color.WHITE) { // 访问还没有访问过的元素
                if (dfsVisit(adj)) return true;
            }
        }
        // 从递归调用中返回
        colors[i] = Color.BLACK;
        return false;
    }
    private enum Color {
        WHITE, GRAY, BLACK
    }
}

       其实,蛮简单的,多些几遍就会了。

       补充一种做法,昨天看到的,这种方法是利用栈stack辅助操作,这样就不需要用递归了。

class Solution {
    private List<Integer>[] adjs;
    private boolean[] visited;
    public void dfs(int n, int[][] edges) {
        // 初始化
        adjs = new List[n];
        visited = new boolean[n];
        for(int i = 0; i < n; i++) {
            adjs[i] = new LinkedList<>();
        }
        for(int i = 0 ; i<edges.length;i++) {
            int[] edge = edges[i];
            int from = edge[0];
            int to = edge[1];
            adjs[from].add(to);
        }


        for (int i = 0 ; i < n;i++) {
            if (!visited[i]) dfsVisited(i);
        }
    }

    private void dfsVisit(int i) {
        Stack<Integer> stack = new Stack<>();
        stack.push(i);
        while(!stack.isEmpty()) {
            Integer node = stack.pop();
            visited[node] = true;
            for (Integer adj: adjs[node]) {
                if (!visited[adj]) stack.push(adj);
            }
        }
    }
}

这样做的好处是,可以在一个方法体里面完成对每个元素的遍历。比如,如果每个点有val属性,你就可以方便地通过深度遍历进行加和操作。

拓扑排序

我还是不要偷懒,赶紧把这个部分写掉,省的哪天我自己都忘了。参见leetcode题目: 课程表 II 。

深度优先遍历的做法

相当于,处于“树”最低端的节点,排在最后面。(因为无环的图就是一系列的树。)

import java.util.LinkedList;
import java.util.List;

class Solution {
    List<Integer>[] adjs;
    Color[] colors;
    int[] ans;
    int size;
    public int[] topoSort(int n, int[][] edges) {
        adjs = new List[n];
        colors = new Color[n];
        ans = new int[n];
        size = n;
        for (int i = 0 ; i < n; i++) {
            adjs[i] = new LinkedList<>();
            colors[i] = Color.WHITE;
        }

        for (int i = 0 ; i < edges.length; i++) {
            int[] edge = edges[i];
            int pre = edge[1];
            int course = edge[0];
            adjs[pre].add(course);
        }

        for (int i = 0 ; i < n;i++) {
            if (colors[i] == Color.WHITE) {
                if (dfsVisit(i)) return new int[0];
            }
        }
        return ans;
    }

    // 判断是否存在环,如果存在返回true。
    // 当存在环时,是无法进行拓扑排序的。
    private boolean dfsVisit(int i) {
        colors[i] = Color.GRAY;
        for (int adj: adjs[i]) {
            if (colors[adj] == Color.WHITE) {
                if (dfsVisit(adj)) return true;
            }
            if (colors[adj] == Color.GRAY) return true; // 存在环
        }
        colors[i] = Color.BLACK;
        ans[--size] = i; // 这一步很关键,最先返回的是叶子节点。
        return false;
    }

    private enum Color {WHITE, GRAY, BLACK}
}

广度优先遍历的做法

这种做法的意思是,找到入度为0的点,肯定是起点,将它放在开头。就像一系列的课程,你把课程一门一门地修掉。每修掉一门课之后,更新点的入度,然后继续从入度为0的点开始。

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

class Solution {
    List<Integer>[] adjs;
    int[] indegrees;
    public int[] topoSort(int n, int[][] edges) {
        adjs = new List[n];
        indegrees = new int[n];
        for (int i = 0 ; i < n; i++) {
            adjs[i] = new LinkedList<>();
        }
        // 入度统计和邻接链表初始化。
        for (int i = 0 ; i < edges.length; i++) {
            int[] edge = edges[i];
            int pre = edge[1];
            int course = edge[0];
            adjs[pre].add(course);
            indegrees[course] += 1;
        }

        // 广度优先遍历
        Queue<Integer> q = new LinkedList<>();
        for (int i = 0 ; i < n;i++) {
            if (indegrees[i] == 0) q.add(i);
        }
        int[] ans = new int[n];
        int size = 0;
        while(!q.isEmpty()) {
            int i = q.poll(); // 最先访问入度为0的点。
            ans[size++] = i;
            for (int adj: adjs[i]) {
                indegrees[adj] -= 1; // 更新入度
                if(indegrees[adj] == 0) q.add(adj); // 添加入度为0的点
            }
        }
        return size == n? ans:new int[0];
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值