0103深度优先搜索和单点连通-无向图-数据结构和算法(Java)

1.1 走迷宫

简单的迷宫,如下图1.1-1所示:

在这里插入图片描述

探索迷宫而不迷路,我们需要:

  • 选择一条没有标记过的通道,在你走过的路上铺一条绳子;
  • 标记所有你第一次路过的路口和通道;
  • 当来到一个标记过的路口时(用绳子)回退到上一个路口;
  • 当回退的路口已没有可走的通道时继续回退。

绳子可以保证总能找到一条出路,标记能保证你不会两次经过同一条通道或者路口。我们把上迷宫,用等价的图来代替,如下图1.1-2所示:在这里插入图片描述

1.2 图的深度优先搜索实现

图的遍历算法非递归代码如下:

package com.gaogzhen.datastructure.graph.undirected;

import com.gaogzhen.datastructure.stack.Stack;
import edu.princeton.cs.algs4.Graph;

import java.util.Iterator;

/**
 * 单点连通性
 * @author: Administrator
 * @createTime: 2023/03/03 19:58
 */
public class DepthFirstSearch {
    /**
     * 顶点是否标记
     */
    private boolean[] marked;

    /**
     * 与指定顶点连通的顶点总数
     */
    private int count;

    /**
     * 图
    */
    private Graph graph;

    /**
     * 起点
     */
    private int s;

    public DepthFirstSearch(Graph graph, int s) {
        this.graph = graph;
        this.s = s;
        check(s);
        marked = new boolean[graph.V()];
        long start = System.currentTimeMillis();
        dfs();
        // dfsDel();
        long end = System.currentTimeMillis();
        System.out.println("用时:" + (end - start));
    }

    /**
     * 搜索图g中与起点v连通的所有顶点
     */
    private void dfs() {
        // 栈记录搜索路径
        Stack<Iterator<Integer>> path = new Stack<>();
        // marked[v] = true;
        if (!marked[s]) {
            // 起点未标记,标记计数加1
            // 起点默认没标记,可以不加是否标记判断
            marked[s] = true;
            count++;
            Iterable<Integer> iterable = graph.adj(s);
            Iterator<Integer> it;
            if (iterable != null && (it = iterable.iterator()) != null){
                // 顶点对应的邻接表迭代器存入栈
                path.push(it);
            }
        }
        while (!path.isEmpty()) {
            Iterator<Integer> it = path.pop();
            int x;
            while (it.hasNext()) {
                // 邻接表迭代器有元素,获取元素
                x = it.next();
                if (!marked[x]) {
                    // 顶点未被标记,标记计数+1
                    marked[x] = true;
                    count++;
                    if (it.hasNext()) {
                        // 邻接表迭代器有元素重新入栈
                        path.push(it);
                    }
                    // 深度优先原则,当前迭代器入栈,新标记顶点的邻接表迭代器入栈,下次循环优先访问
                    Iterable<Integer> iterable = graph.adj(x);
                    if (iterable != null && (it = iterable.iterator()) != null){
                        path.push(it);
                    }
                    break;
                }
            }

        }
    }

    private void  dfsDel() {
        // 支持后入先出和任意删除
        java.util.Stack<Integer> stack = new java.util.Stack<>();
        stack.push(s);
        while (!stack.isEmpty()) {
            int v = stack.peek();
            if (!marked[v]) {
                marked[v] = true;
                count++;
                for (int w : graph.adj(v)) {
                    // 访问同层顶点
                    if (!marked[w]) {
                        // 没被标记过顶点,入栈
                        if (stack.contains(w)) {
                            // 没被标记,但是已经入栈,说明之前是同层压入,需要移除重新压入
                            // 这样符合深度优先原则
                            stack.removeElement(w);
                        }
                        stack.push(w);
                    }
                }
            }
            else {
                // 顶点v的邻接顶点都已访问完毕,弹出顶点v,回溯
                stack.pop();
            }
        }
    }

    /**
     * 检测索引是否在范围之内
     * @param i 给定索引
     */
    private void check(int i) {
        if (i < 0 || i > graph.V() - 1) {
            throw new IndexOutOfBoundsException("索引越界异常");
        }
    }

    /**
     * 判断起点是否与给定顶点x连通
     * @param x 给定顶点
     * @return
     */
    public boolean marked(int x) {
        check(x);
        return marked[x];
    }

    /**
     * 返回图中与顶点想连通的顶点数
     * @return
     */
    public int count() {
        return count;
    }

}


/**===========*/

import com.gaogzhen.datastructure.stack.Stack;
import edu.princeton.cs.algs4.Graph;

import java.util.Iterator;

/**
 * 单点连通性
 * @author: Administrator
 * @createTime: 2023/03/03 19:58
 */
public class DepthFirstSearch {
    /**
     * 顶点是否标记
     */
    private boolean[] marked;

    /**
     * 与指定顶点连通的顶点总数
     */
    private int count;

    /**
     * 图
    */
    private Graph graph;

    /**
     * 起点
     */
    private int s;

    public DepthFirstSearch(Graph graph, int s) {
        this.graph = graph;
        this.s = s;
        check(s);
        marked = new boolean[graph.V()];
        dfs();
    }

    /**
     * 搜索图g中与起点v连通的所有顶点
     */
    private void dfs() {
        Stack<Entry<Integer, Iterator<Integer>>> path = new Stack<>();
        // marked[v] = true;
        if (!marked[s]) {
            marked[s] = true;
            count++;
            Iterable<Integer> iterable = graph.adj(s);
            Iterator<Integer> it;
            if (iterable != null && (it = iterable.iterator()) != null){
                path.push(new Entry<>(s, it));
            }
        }
        while (!path.isEmpty()) {
            Entry<Integer, Iterator<Integer>> entry = path.pop();
            int x;
            Iterator<Integer> it = entry.getValue();
            Integer f = entry.getKey();
            while (it.hasNext()) {
                x = it.next();
                if (!marked[x]) {
                    marked[x] = true;
                    count++;
                    if (it.hasNext()) {
                        path.push(entry);
                    }
                    Iterable<Integer> iterable = graph.adj(x);
                    if (iterable != null && (it = iterable.iterator()) != null){
                        path.push(new Entry<>(x, it));
                    }
                    break;
                }
            }

        }
    }

    /**
     * 检测索引是否在范围之内
     * @param i 给定索引
     */
    private void check(int i) {
        if (i < 0 || i > graph.V() - 1) {
            throw new IndexOutOfBoundsException("索引越界异常");
        }
    }

    /**
     * 判断起点是否与给定顶点x连通
     * @param x 给定顶点
     * @return
     */
    public boolean marked(int x) {
        check(x);
        return marked[x];
    }

    /**
     * 返回图中与顶点想连通的顶点数
     * @return
     */
    public int count() {
        return count;
    }

}

测试代码:

public static void testDepth() {
    String path = "H:\\gaogzhen\\java\\projects\\algorithm\\asserts\\maze.txt";
    In in = new In(path);
    Graph graph = new Graph(in);
    int s = 0;
    DepthFirstSearch depthFirstSearch = new DepthFirstSearch(graph, s);

    int t = 5;
    System.out.println(depthFirstSearch.marked(t));
    System.out.println(depthFirstSearch.count());
}
// 测试结果
true
6

1.3 算法分析及性能

知识点

  • 深度优先搜索非递归实现,主要借助栈来代替递归调用栈帧结构,可以节省内存占用和提高运行效率。

算法分析:dfs()方法为该算法实现的主要方法,方法源代码已给出,这里不再赘述整体流程,着重分析下以下关键问题。

  • 该非递归dfs方法如何保证深度优先?
    • 首先我把起点对应的邻接(连通)顶点集合迭代器压入栈中
    • 外层循环开始
      • 弹出栈顶元素,获取顶点对应的邻接顶点集合迭代器
      • 内层循环判断该迭代器有下一个元素即还有邻接顶点,取出一个邻接顶点。
        • 判断该邻接顶点没有被标记过
          • 标记数组对应顶点索引标记
          • 连通顶点计数+1
          • 判断迭代器还有元素,重新压入栈中
          • break跳出内层循环
    • 总结:只要邻接顶点(更深一层的顶点)没被标记过,标记之后同层迭代器压入栈中,去访问更深一层的顶点;而不是继续访问同层的顶点。
  • 该dfs方法如果保证同层(同一个顶点的邻接顶点)访问全部访问完毕且只访问一次?
    • 每个顶点只访问一次是标记数组marked[]索引和顶点一一对应,默认都是false未标记;标记之后不会在压入栈中,自然不会在标记一次
    • 访问同层元素是通过迭代器完成的,while配合迭代hasNext,next()方法保证全部访问一边且不会重复访问。
  • 深度优先算法性能如何?见下面的命题及证明。
    • 这里根据上面的算法简单分析
    • 完成循环判断栈不为空,那么只有未被标记的顶点及其邻接表(迭代器)会放入栈中;也就是说所有的顶点及其邻接表都会被放入栈中且不会重复
    • 内层判断迭代器有元素那么所有的邻接表会被遍历一边,邻接表代表对应顶点的度数
    • 结论深度优先搜索标记与起点连通的所有顶点所需的时间和顶点的度数之和成正比

命题A。深度优先搜索标记与起点连通的所有顶点所需的时间和顶点的度数之和成正比。

证明:首先,我们要证明这个算法能标记所有与起点s连通的所有顶点(且不会标记其他顶点)。算法仅通过边来寻找顶点,所以每个被标记的顶点都与s连通;反证法证明标记了所有与s连通的顶点,假设某个没有被标记的顶点w与s连通。因为s作为起点是被标记的,由s到w的任意一条路径中至少有一条边连接的两个顶点分别被标记过河没有被标记过,例如v-x。根据算法,在标记了v后比如发现x,因此这样的边不存在。

每个顶点都只会被标记一次保证了时间上限(检查标记的耗时和度数成正比)。

详细搜索轨迹,可参考算法第四版341页。

1. 4 单点连通性

连通性。给定一幅图,回答“两个给定的顶点是否连通”?或者图中有多少个连通子图等类似问题。

问题“两个给定的顶点是否连通?”等价于“两个给定的顶点间是否存在一条路径?”,也可以叫做路经检测问题。深度优先搜索解决了这个问题。

递归方法参考《算法第四版?或者书提供的jar包。

后记

如果小伙伴什么问题或者指教,欢迎交流。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/algorithm

参考链接:

[1][美]Robert Sedgewich,[美]Kevin Wayne著;谢路云译.算法:第4版[M].北京:人民邮电出版社,2012.10.P338-P342.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

gaog2zh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值