版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qqchenjian318/article/details/71126837
前言
在上一篇博客中,我们学习了搜索算法的第一种:深度优先搜索,这篇博客就让我们一起来学习另一种大家都经常听见的搜索算法:广度优先搜索。看名字就知道,他们两者之间肯定有什么不可告人的秘密。广度优先搜索,又叫宽度优先搜索,英文名:Breadth First Search。属于一种盲目搜寻方法,目的是系统的展开并且搜索图中所有的点,以找寻结果。我们还是以上篇博客后面提到的那个迷宫地图的问题来详细介绍一下该种算法。
广度优先搜索和深度优先搜索的区别
在上篇博客中搜索算法(一)深度优先搜索,我们提到了地图迷宫,那么深度优先搜索的思路就是,先找一个方向,然后一直往下走,直到走不通的时候再回到这里。但是广度优先搜索就不是这样了,比如初始点(1,1),他一步可以走到的点有(2,1)(1,2),这两个点一步能走到的点有(2,2)(1,3)。。。这样直到达到女朋友所在的点。核心点就是,每一步都找到当前所有能到达的点,在网络爬虫的实现中,如是运用了广度优先搜索,那么他就是先将某个一节点的所有同级节点搜索完成,再对这些节点的下一个节点进行搜索,更像一层一层的去进行搜索,而深度优先搜索,就是以纵深来完成搜索的。
解决地图迷宫
具体的题目和规则,请看上篇博客。
广度优先搜索分析
ok,让我们来具体分析一下,首先呢我们可以循环的地方是,每一步的通过一步所能达到的位置,这部分是可以重复的,比如初始点
(1,1),一步所能达到的点有(2,1)(1,2),然后第二步,从(2,1)(1,2)点开始,分别计算这两个点,一步所能达到的点有(2,2)
(1,1) 和(1,1)(2,2)(1,3)。也就是说我们算法的核心已经能够确定下来了。然后我们看第二步能达到的点,发现(1,1)点是我们已
经走过的,而我们是不需要重复走的,所以去除掉已经走过的点。还有呢,出现了重复点(2,2),而重复的点,我们也只需要记录一次就行
了,所以需要去重。既然是一个递归循环,那么终止的条件是什么呢?那肯定是能达到的点,已经包含了女朋友所在的点了。
现在,我们已经有了循环的主体,也有了终止循环的条件,那么,让我们开始编写代码吧。
首先,让我们编写一个Point类,来表示坐标上的一个点
public class Point {
public int x;
public int y;
}
然后,来编写循环的主体
循环的主体其实就是,根据上一步能达到的坐标点,来计算这一步所有能达到的有效的坐标点,以及当前是第几步
public void extentFS(Point[] points,int step){
step++;//首先将步数加1,然后下面代码就是下一步能达到的所有点
//初始化一个数组用于存放下一步能到达的点,最多个数和地图上的点个数相同
Point[] newPoints = new Point[20];
int count = 0 ;//初始化一个值,用于存放点的时候用
for (int i = 0; i < points.length; i++) {
Point point = points[i];
//计算该点,按上右下左的顺序一步之内能达到的位置,不包括已经走过的点
for (int j = 0; j < aaa.length; j++) {
Point nextPoint = new Point();
nextPoint.x = point.x+aaa[j][0];
nextPoint.y = point.y + aaa[j][1];
//将当前点,标记为已经走过的点
path[nextPoint.x][nextPoint.y] = 1;
//将当前点,存入下一步能到达点的数组中
count++;
newPoints[count] = nextPoint;
}
}
//继续下一步的搜索
extentFS(newPoints,step);
}
上述代码中,我们已经大致写出了循环的主体,首先,传入参数points:表示上一步的所有点,step,表示上一步是第几步。
我们通过遍历上一步的所有点,分别计算该点所能达到的坐标位置,和上面算法一样,用aaa数组来辅助计算,上下左右的点的坐标,然
后将能达到的点存入用于下一步循环的数组newPoints。
但是,我们也需要加上很多限制条件,比如判断是否是障碍物,判断是否越界,判断是否已经走过了。
判断是否已经走过了,以及是否是障碍物
if (path[nextPoint.x][nextPoint.y] == 0 && map[nextPoint.x][nextPoint.y] == 0){
//判断点是否已经走过了,已经是否是障碍物
}
判断是否越界
if (nextPoint.x > 4 || nextPoint.y > 5 || nextPoint.x < 1 || nextPoint.y < 1){
continue;
}
另外,我们每一步循环中,因为不知道下一步能达到的点的个数,所以new了一个25长度的数组,那么肯定在下一步循环的时候,会出现
空指针的问题,我们也需要进行排除(当然,这里也可以用其他数据结构来做,动态增加长度,就不需要判断空指针了)。
if (point == null)
continue;
然后在进行下一步循环之前,我们需要检查,是否已经找到了女朋友
//遍历得到的下一步能到达点的数组,判断是否已经达到了女朋友的位置
for (Point newPoint : newPoints) {
if (newPoint == null)
continue;
if (newPoint.x == q && newPoint.y == p){
//说明已经到了女朋友所在的点
Log.i("hero","====找到女朋友啦------一共用了--"+step+"步");
return;
}
}
所以完整的代码如下
首先初始化
//地图迷宫
private int[][] aaa = {{0,-1},{1,0},{0,1},{-1,0}};//每步的搜索顺序
private int[][] map = new int[5][6];//初始化地图,其他地方为了方便是从1开始的,那么地图就多一个便于计算
private int[][] path = new int[5][6];//已经走过的路径点
private int q = 3;
private int p = 4;
private int min = 0;
private int count = 0;
public void mapMaze(){
map[3][1] = 1;//初始化地图的障碍物
map[3][3] = 1;
map[2][4] = 1;
map[4][5] = 1;
// depthFS(1,1,0);
Point point = new Point();
point.x = 1;
point.y = 1;
Point[] startPoint = new Point[1];
startPoint[0] = point;
breadthFS(startPoint,0);
// Log.i("hero","---找到女朋友啦,最短路线为--"+min+"步");
}
然后,是算法的核心循环
public void breadthFS(Point[] points,int step){
step++;//首先将步数加1,然后下面代码就是下一步能达到的所有点
//初始化一个数组用于存放下一步能到达的点,最多个数和地图上的点个数相同
Point[] newPoints = new Point[20];
//第一步,遍历当前的坐标点,然后分别计算每个点下一步能达到的位置,
int count = 0 ;//初始化一个值,用于存放点的时候用
for (int i = 0; i < points.length; i++) {
Point point = points[i];
if (point == null)
continue;
//计算该点,按上右下左的顺序一步之内能达到的位置,不包括已经走过的点
for (int j = 0; j < aaa.length; j++) {
Point nextPoint = new Point();
nextPoint.x = point.x+aaa[j][0];
nextPoint.y = point.y + aaa[j][1];
//检查该点是否越界
if (nextPoint.x > 4 || nextPoint.y > 5 || nextPoint.x < 1 || nextPoint.y < 1){
continue;
}
//查看该点是否已经走过了 并且不是障碍物
if (path[nextPoint.x][nextPoint.y] == 0 && map[nextPoint.x][nextPoint.y] == 0){
//说明该点还没走过,并且不是障碍物
//将当前点,标记为已经走过的点
path[nextPoint.x][nextPoint.y] = 1;
//将当前点,存入下一步能到达点的数组中
count++;
newPoints[count] = nextPoint;
}
}
}
//遍历得到的下一步能到达点的数组,判断是否已经达到了女朋友的位置
for (Point newPoint : newPoints) {
if (newPoint == null)
continue;
if (newPoint.x == q && newPoint.y == p){
//说明已经到了女朋友所在的点
Log.i("hero","====找到女朋友啦------一共用了--"+step+"步");
return;
}
}
//否则的话,就是还没到女朋友所在的地方,继续下一步的搜索
breadthFS(newPoints,step);
}
代码的执行结果如下
当然,该算法只是计算了找到女朋友所需要的最小步数,并没有要求打印出路线,所以也有一些可以扩展的地方,比如可以通过Point里
面增加参数,来记录该点的上一个点,就可以完成轨迹的记录等等。
首先我们上面是通过递归来实现的,每次递归都会创建一个长度为25的数组,这样做其实是非常消耗内存资源的。那么有没有什么方法可
以避免这样呢?其实我们可以通过队列(queue)和一个while循环来实现算法,我们先将(1,1)点放入队列中,然后开始找下一步能达到
的点(1,2)(2,1)并且也将它们放入队列中,当while本次循环查找完当前点,所有能达到的有效点,并且将它们放入队列中,就将当前
点移出队列,根据队列先进先出的原则,while的下次循环,会开始查下我们之前存入的有效点,一步所能达到的所有有效点,直到我们找
到我
们的女朋所在的点为止。
具体代码如下
初始化地图等
//地图迷宫
private int[][] aaa = {{0,-1},{1,0},{0,1},{-1,0}};//每步的搜索顺序
private int[][] map = new int[5][6];//初始化地图,其他地方为了方便是从1开始的,那么地图就多一个便于计算
private int[][] path = new int[5][6];//已经走过的路径点
private int q = 3;
private int p = 4;
private int min = 0;
private int count = 0;
public void mapMaze(){
map[3][1] = 1;//初始化地图的障碍物
map[3][3] = 1;
map[2][4] = 1;
map[4][5] = 1;
// depthFS(1,1,0);
int i = breadthFSBasic();
if (i == -1){
Log.i("hero","---地图找了个遍,都没发现女朋友---");
}else {
Log.i("hero","---找到女朋友啦,最少需要--"+i+"步");
}
// breadthFS(startPoint,0);
// Log.i("hero","---找到女朋友啦,最短路线为--"+min+"步");
}
算法的主体
其实上述代码,就是广度优先搜索最基本的样子。
public int breadthFSBasic(){
Point point = new Point();
point.x = 1;
point.y = 1;
point.step = 0;
Queue<Point> queue = new LinkedList<>();
queue.add(point);
//如果队列不为空,则继续
while (!queue.isEmpty()){
Point remove = queue.remove();
for (int i = 0; i < aaa.length; i++) {
Point newPoint = new Point();
newPoint.x = remove.x+aaa[i][0];
newPoint.y = remove.y+aaa[i][1];
newPoint.step = remove.step + 1;
//检查是否越界
if (newPoint.x < 1 || newPoint.y < 1 || newPoint.x>4 || newPoint.y > 5)
continue;
//检查是否是已经走过点或者是障碍物
if (path[newPoint.x][newPoint.y] == 0 && map[newPoint.x][newPoint.y] == 0){
path[newPoint.x][newPoint.y] = 1;
//判断是否是女朋友
if (newPoint.x ==q && newPoint.y == p){
// Log.i("hero","---找到女朋友了---一共用了"+"步");
return newPoint.step;
}
queue.add(newPoint);
}
}
}
return -1;
}
总结
当然,本篇文章和上篇文章都只是简单的对深度优先搜索和广度优先搜索进行了一些学习。而且用了一个比较实际的运用地图迷宫来实际
用这两种算法来解决问题。其实这两种算法都是图的搜索算法中非常基础的两种。我们可能在面试或者笔试中更多的被问到的是在二叉树
遍历中的使用。通过这两篇文章加上二叉树的一些特性,相信我们已经可以通过这两种算法完成二叉树的遍历了。具体的实现,就需要大
家多多编码练习了。
因个人水平有限,文章中难免有错误和不足之处,请各位多多指正。
下期预告,在介绍了两种最基础的搜索算法之后,我们将要学习的是几种求最短路径的算法。咦,是不是感觉地图迷宫其实也是最短路径?
但是我们要说的跟地图迷宫并不太相同。我们将系统的学习Floyed-Warshell、Dijkstra、Bellman-Ford等几种求解最短路径的算法。