搜索算法是利用计算机的性能来有目的的穷举一个问题解空间的部分或所有的可能情况,从而求出问题的解的一种方法。算法是作用于具体数据结构之上的,而深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构的。
图的搜索
图的存储方式主要有邻接表和邻接矩阵,但觉得邻接表更适合用来存储图。无向图和有向图都可以使用深度和广度搜索算法。无向图代码实现如下:
public class Graph {
private int v; //顶点个数
private LinkedList<Integer> arr[]; //邻接表
public Graph(int v) {
this.v = v;
// 初始化邻接表中存储LinkedList的空间
arr = new LinkedList[v];
for (int i = 0; i < v; i++) {
// 初始化每一条LinkedList
arr[i] = new LinkedList<>();
}
}
// 给邻接表添加边和度(无向图一条边存两次)
public void addEage(int s, int t) {
arr[s].add(t);
arr[t].add(s);
}
}
广度优先搜索算法
广度优先搜索(Breadth-First-Search),我们平常都简称 BFS。直观地讲,它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。广度优先搜索要依赖队列这个数据结构,和树的广度优先遍历相似。
BFS代码实现:
/**
* 广度优先搜索
* @param s
* @param t
*/
public void bfs(int s, int t) {
if (s == t)
return;
// visited用于记录已经访问的顶点,防止顶点被重复访问
boolean[] visited = new boolean[v];
// 已被访问的点记为true
visited[s] = true;
// 用于存储已被访问但是与它相连的顶点未被访问的顶点
Queue<Integer> queue = new LinkedList<>();
// 将起始顶点存入队列中
queue.add(s);
// 数组用于记录搜索路径
int[] prev = new int[v];
// 数组元素初始化为-1
for (int i = 0; i < v; ++i) {
prev[i] = -1;
}
// 循环遍历搜索
while (queue.size() != 0) {
// 将父节点从队列中去除
int w = queue.poll();
/**
* 1:获取出父节点的子节点
* 2:如果顶点未被访问,使用prev数组(当前顶点作为下标)记录搜索路径
* 2.1:如果当前顶点和要搜索的顶点一致,使用print函数输出搜索路径
* 2.2:如果不一致,将当前顶点的访问记录变为true,将子节点插入队列中
*/
for (int i = 0; i < arr[w].size(); ++i) {
int q = arr[w].get(i);
if (!visited[q]) {
prev[q] = w;
if (q == t) {
print(prev, s, t);
return;
}
visited[q] = true;
queue.add(q);
}
}
}
}
/**
* 因为路径是反向存储的(数组t下标中存储的元素是父节点存储位置的下标),需要使用递归输出路径
* @param prev
* @param s
* @param t
*/
private void print(int[] prev, int s, int t) { // 递归打印s->t的路径
if (prev[t] != -1 && t != s) {
print(prev, s, prev[t]);
}
System.out.print(t + " ");
}
广度优先搜索的时间复杂度是 O(V+E),其中,V 表示顶点的个数,E 表示边的个数。当所有的点之间都是连通的时候,E>>V
所以广度优先搜索的时间复杂度也可以简写为 O(E)。广度优先搜索的空间复杂度是顶点的个数,O(V)。
深度优先搜索算法
深度优先搜索(Depth-First-Search),简称 DFS。搜索的起始顶点是 s,终止顶点是 t,在图中寻找一条从顶点 s 到顶点 t 的路径。如果映射到迷宫那个例子,s 就是起始所在的位置,t 就是出口。使用深度递归算法,把整个搜索的路径标记出来。这里面实线箭头表示遍历,虚线箭头表示回退。从图中可以看出,深度优先搜索找出来的路径,并不是顶点 s 到顶点 t 的最短路径。
深度优先搜索用的是一种算法思想,回溯思想。 这种思想解决问题的过程,非常适合用递归来实现。深度优先搜索代码实现里,有个比较特殊的变量 found,它的作用是,当我们已经找到终止顶点 t 之后,终止递归函数。
DFS代码实现:
// 用于终止递归函数
boolean found = false;
public void dfs(int s, int t) {
found = false;
// visited用于记录已经访问的顶点,防止顶点被重复访问
boolean[] visited = new boolean[v];
// 数组用于记录搜索路径
int[] prev = new int[v];
// 数组元素初始化为-1
for (int i = 0; i < v; ++i) {
prev[i] = -1;
}
//调用递归函数
recurDfs(s, t, visited, prev);
//调用print函数输出路径
print(prev, s, t);
}
//递归函数
public void recurDfs(int w, int t, boolean[] visited, int[] prev) {
//如果终止条件成立终止递归
if (found == true) {
return;
}
//标记此顶点已被访问
visited[w] = true;
//如果访问到的顶点是要找的点跳出递归函数
if (w == t) {
found = true;
return;
}
//循环遍历邻接表,在循环中调用递归函数,直至找到t
for (int i = 0; i < arr[w].size(); i++) {
//选中子节点中的其中一个
int q = arr[w].get(i);
//判断该节点是否被访问
if (!visited[q]) {
//如果没有将该节点的父节点存储在路径数组中
prev[q] = w;
//调用递归函数
recurDfs(q, t, visited, prev);
}
}
}
因为深度优先搜索中,每条边最多被访问两次,所以时间复杂度是O(E),E代表的是边数。因为递归调用栈的最大深度不会超过顶点的个数,所以总的空间复杂度就是 O(V)。