图-深度和广度优先搜索-最短路径算法-java实现

什么是“搜索”算法?

我们知道,算法是作用于具体数据结构之上的,深度优先搜索算法和广度优先搜索算法都是基于“图”这种数据结构的。这是因为,图这种数据结构的表达能力很强,大部分涉及搜索的场景都可以抽象成“图”。

图上的搜索算法,最直接的理解就是,在图中找出从一个顶点出发,到另一个顶点的路径。具体方法有很多,比如今天要讲的两种最简单、最“暴力”的深度优先、广度优先搜索,还有 A*、IDA* 等启发式搜索算法。

广度优先搜索(BFS)

广度优先搜索(Breadth-First-Search),我们平常都简称 BFS。直观地讲,它其实就是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索。理解起来并不难。为了方便理解我们先看一下二叉树广度优先遍历,广度优先遍历也叫层序遍历,先遍历第一层(节点 1),再遍历第二层(节点 2,3,4),第三层(5,6,7,8),第四层(9,10)。以下是二叉树广度优先遍历示意图,你可以看下

 深度优先遍历用的是栈,而广度优先遍历要用队列来实现,我们以下图二叉树为例来看看如何用队列来实现广度优先遍历。

 以上就是二叉树的广度优先遍历的算法动态展示。

下面我们来探讨一下如何实现图的“广度优先遍历算法”的编程实现

我们准备如下"无向图"

 首先,上图左边的是一个简单的无向图,右上方是一个表格,描述了每个节点与之相连的所有邻节点。右下方是一个队列q。还有新增一个prev数组用来计算节点与起始节点之间的距离。首先把起始节点(假设0为起始节点)压入队列中,设置prev[0]=0。标记0节点的visited状态为true,那么此时0节点就已经被遍历了,接下来不能立马让0节点出队列,因为0节点还有一群“跟随”他的邻节点还没入队列呢!所谓的广度优先,就是尽量把节点与之相邻的所有临节点都遍历完再往“深”出去遍历,因此,0节点不仅不能立马出队,还要把他的所有的“邻居节点”都拉进队列里来,他才能安心的离开队列。因为他的邻节点不仅仅是他一个人的邻节点,也有可能是其他节点的邻节点,因此,有可能其他的邻节点已经先把他的部分邻居节点拉入队列中了,所以“拉邻居节点入队列”要排除已经入队列的“邻居节点”,那又如何判断一个节点是否已经进入队列了呢?别忘记了!我们还有一个visited数组记录着呢,任意一个节点只要进入了队列,其visited状态就会置为true。所以,按照这个思路,0节点在出队列之前要把他所有的“邻居节点”中符合条件的节点一起拉入队列中,其中0节点的邻居节点是1,2,5,6,这四个节点的visited状态都为false,所以把他们拉进队伍,并把visited状态置为true,把prev[1],prev[2],prev[5],prev[6]都置为0,表示这4个节点都是从0节点遍历过来的。这样,0节点才完成了他应该完成的所有任务,可以允许出列了

接下来,队列中的首节点是1号节点,轮到1号节点完成任务了,visited[0]=true,但0号节点已经入队过了,所以没有邻居节点可以被1节点拉进队列了,没有新的节点入队,因此此时不用维护prev这个表了,1节点可以允许出列了。

只要队列不为空,那么队列就可以按照上面的规则一直执行下去,直到队列为空,此时图中所有的节点就都已经入队,并完成了相应的数据维护工作,图的“广度优先遍历”就已经完成了。剩余的部分我用下面动画来演示
 

里面有三个重要的辅助变量 visited、queue、prev。

visited 是用来记录已经被访问的顶点,用来避免顶点被重复访问。如果顶点 q 被访问,那相应的 visited[q]会被设置为 true。

queue 是一个队列,用来存储已经被访问、但相连的顶点还没有被访问的顶点。因为广度优先搜索是逐层访问的,也就是说,我们只有把第 k 层的顶点都访问完成之后,才能访问第 k+1 层的顶点。当我们访问到第 k 层的顶点的时候,我们需要把第 k 层的顶点记录下来,稍后才能通过第 k 层的顶点来找第 k+1 层的顶点。所以,我们用这个队列来实现记录的功能。

prev 用来记录搜索路径。当我们从顶点 s 开始,广度优先搜索到顶点 t 后,prev 数组中存储的就是搜索的路径。不过,这个路径是反向存储的。prev[w]存储的是,顶点 w 是从哪个前驱顶点遍历过来的。为了正向打印出路径,我们需要递归地来打印,你可以看下 print() 函数的实现方式。

java代码实现"广度优先搜索算法"

 图的代码实现

import java.util.LinkedList;
/**
 * Created with IntelliJ IDEA.
 * 无向 图
 * @Auther: njp
 * @Date: 2021/11/05/15:28
 * @Description:
 */
public class Graph {
    //顶点的个数
    private int v;
    //邻接表
    private LinkedList<Integer> adj[];

    //构造
    public Graph(int v){
        this.v=v;
        //构建一个数组,大小为图的顶点个数,每个元素保存与这个顶点有关系的其他顶点的信息(索引,下标)
        adj = new LinkedList[v];
        for (int i = 0; i < v; i++) {
            adj[i]=new LinkedList<>();
        }
    }
    public void addEdge(int s, int t){
        //无向图一条边存两次
        adj[s].add(t);
        adj[t].add(s);

    }
    
    public int getV() {
        return v;
    }

    public LinkedList<Integer>[] getAdj() {
        return adj;
    }
}

广度优先搜索代码实现

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

/**
 * Created with IntelliJ IDEA.
 * 广度优先搜索遍历(无向图)
 *
 * @Auther: njp
 * @Date: 2021/11/05/15:23
 * @Description:
 */
public class BFS {

    public Graph creatGraph() {
        int v = 7;
        Graph graph = new Graph(v);
        //添加指定的边用作测试
        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(0, 5);
        graph.addEdge(0, 6);
        graph.addEdge(5, 3);
        graph.addEdge(5, 4);
        graph.addEdge(3, 4);
        graph.addEdge(4, 6);
        return graph;
    }

    /**
     * @param graph 无向图
     * @param s     路径起点
     * @param t     路径终点
     */
    public void bfs(Graph graph, int s, int t) {
        int v = graph.getV();
        LinkedList<Integer>[] adj = graph.getAdj();
        if (s == t) return;
        //用来记录已经被访问的顶点,用来避免顶点被重复访问,如果顶点index被访问那相应的visited[index]会被设置为true
        boolean[] visited = new boolean[v];
        //保存已经访问过的,但是与其相连的顶点没有被访问的
        //创建队列有疑问的可以看下LinkedList的源码,LinkedList实现了Deque,而Deque又继承自Queue
        Queue<Integer> queue = new LinkedList<>();
        queue.add(s);
        //用来记录路径,路径是反向存储的(用来记录每个节点的上一节点编号)
        int[] prev = new int[v];
        //初始化辅助数组
        for (int i = 0; i < v; ++i) {
            prev[i] = -1;
        }
        while (queue.size() != 0) {
            //取出队列头部
            int w = queue.poll();
            //遍历每个节点上的链表
            for (int i = 0; i < adj[w].size(); ++i) {
                int q = adj[w].get(i);
                //查看q顶点是否被访问
                if (!visited[q]) {
                    //意思就是,q的上一个顶点是w,也就是说prev会储存当前顶点的上一个顶点位置
                    prev[q] = w;
                    //找到目标退出打印
                    if (q == t) {                
                        print(prev, s, t);
                        return;
                    }
                    //设置被访问状态
                    visited[q] = true;
                    //入队
                    queue.add(q);
                }
            }
        }
    }
    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 + " ");
    }


    public static void main(String[] args) {
        Queue<Integer> queue = new LinkedList<>();
        queue.add(1);
        queue.add(2);
        int w = queue.poll();
        int size = queue.size();

        BFS bfs = new BFS();
        Graph graph = bfs.creatGraph();
        bfs.bfs(graph, 0, 3);
    }


}

代码注释很详细,如果有不明白的地方可以在IDE中debug一步一步去理解

打印结果:

 掌握了广度优先搜索算法的原理,我们来看下,广度优先搜索的时间、空间复杂度是多少呢?

最坏情况下,终止顶点 t 离起始顶点 s 很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次,所以,广度优先搜索的时间复杂度是 O(V+E),其中,V 表示顶点的个数,E 表示边的个数。当然,对于一个连通图来说,也就是说一个图中的所有顶点都是连通的,E 肯定要大于等于 V-1,所以,广度优先搜索的时间复杂度也可以简写为 O(E)。

广度优先搜索的空间消耗主要在几个辅助变量 visited 数组、queue 队列、prev 数组上。这三个存储空间的大小都不会超过顶点的个数,所以空间复杂度是 O(V)。

总结

广度优先搜索和深度优先搜索是图上的两种最常用、最基本的搜索算法,比起其他高级的搜索算法,比如 A*、IDA* 等,要简单粗暴,没有什么优化,所以,也被叫作暴力搜索算法。所以,这两种搜索算法仅适用于状态空间不大,也就是说图不大的搜索。

广度优先搜索,通俗的理解就是,地毯式层层推进,从起始顶点开始,依次往外遍历。广度优先搜索需要借助队列来实现,遍历得到的路径就是,起始顶点到终止顶点的最短路径。深度优先搜索用的是回溯思想,非常适合用递归实现。换种说法,深度优先搜索是借助栈来实现的。在执行效率方面,深度优先和广度优先搜索的时间复杂度都是 O(E),空间复杂度是 O(V)。

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值