图的遍历与拓扑排序
好了,接下来是重中之重图的遍历。图遍历是很多其他算法的基础,比如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];
}
}