图论(七)图的广度优先遍历BFS

前言

广度优先搜索是对图中的边进行系统性的探索来发现可以从源节点出发到达的所有节点。该算法能够计算从源节点到每个可到达的节点的最短距离(无权值)。其广度优先则体现在始终是对图进行逐层探索,当当前所在层探索完毕后才进入到邻居节点进一步探索。

一、图的广度优先遍历基本思想

  • 1、图的广度优先搜索,简称BFS。
  • 2、该遍历类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。

二、广度优先遍历算法步骤

  • 1 访问初始结点v并标记结点v为已访问。

  • 2 结点v入队列。

  • 3 当队列非空时,继续执行,否则算法结束。

  • 4 出队列,取得队头结点u。

  • 5 查找结点u的第一个邻接结点w。

  • 6 若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤:

    • 6.1 若结点w尚未被访问,则访问结点w并标记为已访问。
    • 6.2 结点w入队列。
    • 6.3 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6。

三、广度优先遍历图文演示

广度优先遍历和树的层序遍历类似。


上图经过变形

特点:

  • 1、把根节点放到队列的末尾;
  • 2、每次从队列的头部取出一个元素,查看这个元素所有的下一级元素,把它们方法队列的末尾,并把这个元素记为下一级元素的前驱;
  • 3、找到所要找到元素的结束程序;
  • 4、如果遍历整个树还没有找到,结束遍历。

四、用邻接表进行广度优先遍历

4.1 构建数据结构

public class Graph {

    //顶点个数
    private int V;

    //边的条数
    private int E;

    //领接表的底层存储结构
    private TreeSet<Integer>[] adj;

}

4.2 通过该结构定义,构造一个图(无向图)

/**
 * @Author: huangyibo
 * @Date: 2022/3/28 1:02
 * @Description: 领接表, 目前只支持无向无权图
 */

public class Graph {

    //顶点个数
    private int V;

    //边的条数
    private int E;

    //领接表的底层存储结构
    private TreeSet<Integer>[] adj;

    public Graph(String filename){
        File file = new File(filename);
        try {
            Scanner scanner = new Scanner(file);
            V = scanner.nextInt();
            if(V < 0){
                throw new IllegalArgumentException("V must be non-negative");
            }
            adj = new TreeSet[V];
            //初始化领接表
            for (int i = 0; i < V; i++) {
                adj[i] = new TreeSet<>();
            }

            E = scanner.nextInt();
            if(E < 0){
                throw new IllegalArgumentException("E must be non-negative");
            }
            for (int i = 0; i < E; i++) {
                int a = scanner.nextInt();
                //校验顶点a是否合法
                validateVertex(a);

                int b = scanner.nextInt();
                //校验顶点b是否合法
                validateVertex(b);

                //校验是否是自环边
                if(a == b){
                    throw new IllegalArgumentException("Self Loop is Detected!");
                }
                //校验是否是平行边
                if(adj[a].contains(b)){
                    throw new IllegalArgumentException("Parallel Edges are Detected!");
                }
                adj[a].add(b);
                adj[b].add(a);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 校验顶点是否合法
     * @param v
     */
    private void validateVertex(int v){
        if(v < 0 || v >= V){
            throw new IllegalArgumentException("vertex " + v + " is invalid");
        }
    }

    /**
     * 获取顶点个数
     * @return
     */
    public int V(){
        return V;
    }

    /**
     * 获取边的条数
     * @return
     */
    public int E(){
        return E;
    }

    /**
     * 图中是否存在v到w的边
     * @param v
     * @param w
     * @return
     */
    public boolean hasEdge(int v, int w){
        //校验顶点v是否合法
        validateVertex(v);
        //校验顶点w是否合法
        validateVertex(w);
        return adj[v].contains(w);
    }

    /**
     * 返回和v相邻的顶点
     * @param v
     * @return
     */
    public Iterable<Integer> adj(int v){
        //校验顶点v是否合法
        validateVertex(v);
        return adj[v];
    }

    /**
     * 返回顶点v的度
     * 顶点v的度(Degree)是指在图中与v相关联的边的条数
     * @param v
     * @return
     */
    public int degree(int v){
        //校验顶点v是否合法
        validateVertex(v);
        return adj[v].size();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("V = %d, E = %d\n", V, E));
        for (int v = 0; v < V; v++) {
            sb.append(String.format("%d : ", v));
            for (Integer w : adj[v]) {
                sb.append(String.format("%d ", w));
            }
            sb.append("\n");
        }
        return sb.toString();
    }
}

4.3 邻接表的广度优先算法

/**
 * @Author: huangyibo
 * @Date: 2022/4/9 18:26
 * @Description: 图的广度优先遍历
 */

public class GraphBFS {

    private Graph G;

    /**
     * 图的顶点是否已经被遍历过
     */
    private boolean[] visited;

    /**
     * 图的广度优先遍历结果集
     */
    private List<Integer> order = new ArrayList<>();

    public GraphBFS(Graph G){
        this.G = G;
        visited = new boolean[G.V()];
        //循环所有顶点, 防止一个图出现多个连通图(连通分量)的情况
        for (int v = 0; v < G.V(); v++) {
            if(!visited[v]){
                bfs(v);
            }
        }
    }

    /**
     * 图的广度优先遍历
     * @param source
     */
    private void bfs(int source){
        Queue<Integer> queue = new LinkedList<>();
        //将源结点加入队列
        queue.add(source);
        //标记为已访问
        visited[source] = true;
        while(!queue.isEmpty()){
            Integer v = queue.remove();
            //当前出队顶点添加到图的广度优先遍历结果集
            order.add(v);
            //遍历顶点V的相邻顶点
            for (Integer w : G.adj((v))) {
                //如果没有遍历过
                if(!visited[w]){
                    //顶点w先入队
                    queue.add(w);
                    //标记w为已访问
                    visited[w] = true;
                }
            }
        }
    }

    /**
     * 图的广度优先遍历结果集
     * @return
     */
    public List<Integer> order(){
        return order;
    }

    public static void main(String[] args) {
        Graph graph = new Graph("src/main/resources/g1.txt");
        GraphBFS graphBFS = new GraphBFS(graph);
        System.out.println(graphBFS.order());
    }
}

g1.txt

7 6
0 1
0 2
1 3
1 4
2 3
2 6

五、基于广度优先遍历的应用

5.1 求解单源路径问题

/**
 * @Author: huangyibo
 * @Date: 2022/4/9 22:08
 * @Description: 基于图的广度优先遍历、求解单源路径问题
 */

public class SingleSourcePath {

    private Graph G;

    /**
     * 源节点
     */
    private int source;

    /**
     * 图的顶点是否已经被遍历过
     */
    private boolean[] visited;

    /**
     * 存储的是当前访问节点的前一个节点的值
     */
    private int[] pre;

    public SingleSourcePath(Graph G, int source){
        this.G = G;
        this.source = source;
        visited = new boolean[G.V()];
        pre = new int[G.V()];
        for (int i = 0; i < G.V(); i++) {
            pre[i] = -1;
        }
        bfs(source);
    }

    /**
     * 图的广度优先遍历
     * @param source
     */
    private void bfs(int source){
        Queue<Integer> queue = new LinkedList<>();
        //将源结点加入队列
        queue.add(source);
        //标记为已访问
        visited[source] = true;
        //源节点的pre节点为自己
        pre[source] = source;
        while(!queue.isEmpty()){
            Integer v = queue.remove();
            //遍历顶点V的相邻顶点
            for (Integer w : G.adj((v))) {
                //如果没有遍历过
                if(!visited[w]){
                    //顶点w先入队
                    queue.add(w);
                    //标记w为已访问
                    visited[w] = true;
                    //标记w顶点的pre顶点为v
                    pre[w] = v;
                }
            }
        }
    }

    /**
     * 判断源s到顶点target是否可达
     * @param target
     * @return
     */
    public boolean isConnectedTo(int target){
        G.validateVertex(target);
        return visited[target];
    }

    /**
     * 源s到顶点target的路径
     * @param target
     * @return
     */
    public List<Integer> path(int target){
        List<Integer> result = new ArrayList<>();
        if(!isConnectedTo(target)){
            //源s到顶点target不可达, 直接返回空集合
            return result;
        }
        int cur = target;
        //如果当前顶点不是源顶点
        while(cur != source){
            //路径添加当前顶点
            result.add(cur);
            //当前顶点的pre顶点赋值为当前顶点,便于继续循环
            cur = pre[cur];
        }
        //将当前顶点添加到路径中
        result.add(source);
        //路径反转后,即为源顶点到目标顶点的路径
        Collections.reverse(result);
        return result;
    }

    public static void main(String[] args) {
        Graph graph = new Graph("src/main/resources/g1.txt");
        SingleSourcePath ssPath = new SingleSourcePath(graph, 0);
        System.out.println("0 -> 6 : " + ssPath.path(6));
        System.out.println("0 -> 5 : " + ssPath.path(5));
    }
}

5.2 求解无向无权图最短路径问题

/**
 * @Author: huangyibo
 * @Date: 2022/4/9 22:08
 * @Description: 基于图的广度优先遍历、求解无向无权图最短路径问题
 */

public class USSPath {

    private Graph G;

    /**
     * 源节点
     */
    private int source;

    /**
     * 图的顶点是否已经被遍历过
     */
    private boolean[] visited;

    /**
     * 存储的是当前访问节点的前一个节点的值
     */
    private int[] pre;

    /**
     * 存储的是从源点到各目标顶点的路径的长度
     */
    private int[] dis;

    public USSPath(Graph G, int source){
        this.G = G;
        this.source = source;
        visited = new boolean[G.V()];
        pre = new int[G.V()];
        dis = new int[G.V()];
        for (int i = 0; i < G.V(); i++) {
            pre[i] = -1;
            dis[i] = -1;
        }
        bfs(source);
    }

    /**
     * 图的广度优先遍历
     * @param source
     */
    private void bfs(int source){
        Queue<Integer> queue = new LinkedList<>();
        //将源结点加入队列
        queue.add(source);
        //标记为已访问
        visited[source] = true;
        //源节点的pre节点为自己
        pre[source] = source;
        //从源点source到source的距离为 0
        dis[source] = 0;
        while(!queue.isEmpty()){
            Integer v = queue.remove();
            //遍历顶点V的相邻顶点
            for (Integer w : G.adj((v))) {
                //如果没有遍历过
                if(!visited[w]){
                    //顶点w先入队
                    queue.add(w);
                    //标记w为已访问
                    visited[w] = true;
                    //标记w顶点的pre顶点为v
                    pre[w] = v;
                    //标记源点到w顶点的路径长度
                    dis[w] = dis[v] + 1;
                }
            }
        }
    }

    /**
     * 判断源s到顶点target是否可达
     * @param target
     * @return
     */
    public boolean isConnectedTo(int target){
        G.validateVertex(target);
        return visited[target];
    }

    /**
     * 源s到顶点target的路径
     * @param target
     * @return
     */
    public List<Integer> path(int target){
        List<Integer> result = new ArrayList<>();
        if(!isConnectedTo(target)){
            //源s到顶点target不可达, 直接返回空集合
            return result;
        }
        int cur = target;
        //如果当前顶点不是源顶点
        while(cur != source){
            //路径添加当前顶点
            result.add(cur);
            //当前顶点的pre顶点赋值为当前顶点,便于继续循环
            cur = pre[cur];
        }
        //将当前顶点添加到路径中
        result.add(source);
        //路径反转后,即为源顶点到目标顶点的路径
        Collections.reverse(result);
        return result;
    }

    /**
     * 从源点到目标顶点的最短路径的长度
     * @param target
     * @return
     */
    public int dis(int target){
        G.validateVertex(target);
        return dis[target];
    }

    public static void main(String[] args) {
        Graph graph = new Graph("src/main/resources/g1.txt");
        USSPath ssPath = new USSPath(graph, 0);
        System.out.println("0 -> 6 : " + ssPath.path(6));
        System.out.println("0 -> 6 : " + ssPath.dis(6));
        System.out.println("0 -> 5 : " + ssPath.path(5));
        System.out.println("0 -> 6 : " + ssPath.dis(5));
    }
}

参考:
https://zhuanlan.zhihu.com/p/138073414

https://blog.csdn.net/chengqiuming/article/details/115304221

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,864评论 6 494
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,175评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,401评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,170评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,276评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,364评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,401评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,179评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,604评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,902评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,070评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,751评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,380评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,077评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,312评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,924评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,957评论 2 351

推荐阅读更多精彩内容