深度优先搜索类似i走迷宫,一条路走到黑,如果发现这条路走不通,就在前一个路口继续向前走。就像下面这样(图片节选自《算法第四版》)
那么算法中,我们需要解决什么问题呢?我们可以通过adj函数得到结点的相邻结点,但是如果我们如何保证结点已经被我们访问过了,我们就需要一个标志mark,这个标志代表着这个结点是否已经被访问过。(HashSet这种数据结构也可以做到这种事情)。步骤如下:
-
将被访问的结点标记为已访问
-
递归地访问它的所有没有被标记过的邻居结点
/**
* 无向图的深度优先搜索
* @author xiaohui
*/
public class DepthFirstSearch {
private boolean[] marked;
private int count;
public DepthFirstSearch(UndirGraph graph,int s){
marked = new boolean[graph.V()];
dfs(graph,s);
}
private void dfs(UndirGraph graph, int s) {
marked[s] = true;
count++;
for (int v:graph.adj(s)){
if (!marked[v]){
dfs(graph,v);
}
}
}
public boolean getMarked(int w) {
return marked[w];
}
public int getCount() {
return count;
}
}
大家可以有上面的代码可以i很简单的知道,获得与s相同的结点,只需要对dfs进行递归即可,并将结点的marked标志设置为true即可。现在我们就可以完善search函数了。
Iterable<Integer> search(int s) {
DepthFirstSearch dfs = new DepthFirstSearch(this,s);
List list = new ArrayList(dfs.getCount());
for (int i=0;i<this.V();i++) {
if (dfs.getMarked(i)){
list.add(i);
}
}
return list;
}
在上面的深度优先搜索的算法,其实还有一个应用,那就是寻找路径的问题,也就是说,通过深度优先算法,我们可以知道A结点和X结点是否存在一条路径,如果有,则输出路径。
/**
* @author xiaohui
* 通过深度优先搜索寻找路径
*/
public class DepthFirstSearchPath {
private boolean[] marked;
/**
* 从起点到一个顶点的已知路径上面的最后一个顶点,例如:
* 0-3-4-5-6 则 edgeTo[6] = 5
*/
private int[] edgeTo;
/**
* 起点
*/
private final int s;
/**
* 在graph中找出起点为s的路径
* @param graph
* @param s
*/
public DepthFirstSearchPath(Graph graph,int s) {
marked = new boolean[graph.V()];
this.s = s;
edgeTo = new int[graph.V()];
dfs(graph,s);
}
private void dfs(Graph graph, int s) {
marked[s] = true;
for (int v:graph.adj(s)){
if (!marked[v]){
edgeTo[v] = s;
dfs(graph,v);
}
}
}
/**
* v的顶点是否可达,也就是说是否存在s到v的路径
* @param v
* @return
*/
public boolean hasPathTo(int v){
return marked[v];
}
/**
* 返回s到v的路径
* @param v
* @return
*/
public Iterable<Integer> pathTo(int v){
if (!hasPathTo(v)){
return null;
}
Stack<Integer> path = new Stack<>();
for (int x = v;x!=s;x = edgeTo[x]){
path.push(x);
}
path.push(s);
return path;
}
在上面的算法中, 我们首先进行深度优先遍历将每个结点是否被遍历保存到marked[]数组中,然后,在edgeTo[]数组我们保存了进行深度遍历中被遍历结点的上一个结点,示意图如下图所示(图片节选自《算法》):
现在我们可以补全上文中的一些函数了。
/**
* 是否存在S结点到V结点的路径
* @param s
* @param v
* @return
*/
@Override
boolean hasPathTo(int s, int v) {
DepthFirstSearchPath dfsPath = new DepthFirstSearchPath(this,s);
return dfsPath.hasPathTo(v);
}
/**
* 找出s到v结点的路径
* @param s
* @param v
* @return
*/
@Override
Iterable<Integer> pathTo(int s, int v) {
DepthFirstSearchPath dfsPath = new DepthFirstSearchPath(this,s);
return dfsPath.pathTo(v);
}
通过深度优先搜索,我们可以得到s结点的路径,那么深度优先搜索还有什么用法呢?其中有一个用法就是寻找出一幅图的所有连通分量。
public class CC {
private boolean[] marked;
/**
* id代表结点所属的连通分量为哪一个,例如:
* id[1] =0,id[3]=1
* 代表1结点属于0连通分量,3结点属于1连通分量
*/
private int[] id;
/**
* count代表连通分量的表示,0,1……
*/
private int count;
public CC(Graph graph) {
marked = new boolean[graph.V()];
id = new int[graph.V()];
for (int s=0;s<graph.V();s++){
if (!marked[s]){
count++;
dfs(graph,s);
}
}
}
private void dfs(Graph graph,int v) {
marked[v] = true;
id[v] = count;
for (int w:graph.adj(v)) {
if (!marked[w]){
dfs(graph,w);
}
}
}
/**
* v和w是否属于同一连通分量
* @param v
* @param w
* @return
*/
public boolean connected(int v,int w){
return id[v]==id[w];
}
/**
* 获得连通分量的数量
* @return
*/
public int getCount() {
return count;
}
/**
* 结点属于哪一个连通分量
* @param w
* @return
*/
public int id(int w){
return id[w];
}
}
在下图中,有三个连通分量。
说完深度优先搜索,我们可以来说一说广度优先搜索算法了。在前面的深度优先搜索中,我们将深度优先搜索算法比喻成迷宫,它可以带我们从一个结点走到另外一个结点(也就是寻找路径问题),但是如果我们需要去解决_最短路径_的问题,使用深度优先搜索能不能解决呢?答案是不能,我们可以想一想,使用深度优先搜索,我们是一条道走到“黑”,有可能离开始结点最近的结点反而还有可能最后遍历。但是广度优先遍历却可以解决这个问题。
广度优先遍历
广度优先的算法在迷宫中类似这样:我们先遍历开始结点的相邻结点并将结点,然后按照与起点的距离的顺序来遍历所有的顶点。在前面的深度优先遍历中,我们使用了隐式的栈【LIFO】(递归)来进行保存结点,而在广度优先遍历中,我们将使用显式的队列(FIFO)来保存结点。
进行广度优先遍历的算法步骤如下:
先将起点加入队列,然后重复以下步骤:
-
取队列中的下一个顶点v并标记它
-
将与v相邻的所有未被标记过的结点加入队列
package graph.undir;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
/**
* @author xiaohui
* 广度优先遍历
*/
public class BreadthFirstSearch {
private boolean[] marked;
private final int s;
private int[] e