2021年大厂Java岗面试必问,数据结构之图(内含Java代码实现)

深度优先搜索类似i走迷宫,一条路走到黑,如果发现这条路走不通,就在前一个路口继续向前走。就像下面这样(图片节选自《算法第四版》)

那么算法中,我们需要解决什么问题呢?我们可以通过adj函数得到结点的相邻结点,但是如果我们如何保证结点已经被我们访问过了,我们就需要一个标志mark,这个标志代表着这个结点是否已经被访问过。(HashSet这种数据结构也可以做到这种事情)。步骤如下:

  • 将被访问的结点标记为已访问

  • 递归地访问它的所有没有被标记过的邻居结点


/**

 * 无向图的深度优先搜索

 * @author xiaohui

 */

public class DepthFirstSearch {

    private boolean[] marked;

    private int count;



    public DepthFirstSearch(UndirGraph graph,int s){

        marked = new boolean[graph.V()];

        dfs(graph,s);

    }



    private void dfs(UndirGraph graph, int s) {

        marked[s] = true;

        count++;

        for (int v:graph.adj(s)){

            if (!marked[v]){

                dfs(graph,v);

            }

        }

    }



    public boolean getMarked(int w) {

        return marked[w];

    }



    public int getCount() {

        return count;

    }

}

大家可以有上面的代码可以i很简单的知道,获得与s相同的结点,只需要对dfs进行递归即可,并将结点的marked标志设置为true即可。现在我们就可以完善search函数了。


Iterable<Integer> search(int s) {

    DepthFirstSearch dfs = new DepthFirstSearch(this,s);

    List list = new ArrayList(dfs.getCount());

    for (int i=0;i<this.V();i++) {

        if (dfs.getMarked(i)){

            list.add(i);

        }

    }

    return list;

}

在上面的深度优先搜索的算法,其实还有一个应用,那就是寻找路径的问题,也就是说,通过深度优先算法,我们可以知道A结点和X结点是否存在一条路径,如果有,则输出路径。


/**

 * @author xiaohui

 * 通过深度优先搜索寻找路径

 */

public class DepthFirstSearchPath {



    private boolean[] marked;

    /**

     * 从起点到一个顶点的已知路径上面的最后一个顶点,例如:

     * 0-3-4-5-6 则 edgeTo[6] = 5

     */

    private int[] edgeTo;

    /**

     * 起点

     */

    private final int s;



    /**

     * 在graph中找出起点为s的路径

     * @param graph

     * @param s

     */

    public DepthFirstSearchPath(Graph graph,int s) {

        marked = new boolean[graph.V()];

        this.s = s;

        edgeTo = new int[graph.V()];

        dfs(graph,s);

    }



    private void dfs(Graph graph, int s) {

        marked[s] = true;



        for (int v:graph.adj(s)){

            if (!marked[v]){

                edgeTo[v] = s;

                dfs(graph,v);

            }

        }

    }



    /**

     * v的顶点是否可达,也就是说是否存在s到v的路径

     * @param v

     * @return

     */

    public boolean hasPathTo(int v){

        return marked[v];

    }



    /**

     * 返回s到v的路径

     * @param v

     * @return

     */

    public Iterable<Integer> pathTo(int v){



        if (!hasPathTo(v)){

            return null;

        }

        Stack<Integer> path = new Stack<>();

        for (int x = v;x!=s;x = edgeTo[x]){

            path.push(x);

        }

        path.push(s);

        return path;



}

在上面的算法中, 我们首先进行深度优先遍历将每个结点是否被遍历保存到marked[]数组中,然后,在edgeTo[]数组我们保存了进行深度遍历中被遍历结点的上一个结点,示意图如下图所示(图片节选自《算法》):

现在我们可以补全上文中的一些函数了。


/**

 * 是否存在S结点到V结点的路径

 * @param s

 * @param v

 * @return

 */

@Override

boolean hasPathTo(int s, int v) {

    DepthFirstSearchPath dfsPath = new DepthFirstSearchPath(this,s);

    return dfsPath.hasPathTo(v);

}

/**

 * 找出s到v结点的路径

 * @param s

 * @param v

 * @return

 */

@Override

Iterable<Integer> pathTo(int s, int v) {

    DepthFirstSearchPath dfsPath = new DepthFirstSearchPath(this,s);

    return dfsPath.pathTo(v);

}

通过深度优先搜索,我们可以得到s结点的路径,那么深度优先搜索还有什么用法呢?其中有一个用法就是寻找出一幅图的所有连通分量。


public class CC {

    private boolean[] marked;

    /**

     * id代表结点所属的连通分量为哪一个,例如:

     * id[1] =0,id[3]=1

     * 代表1结点属于0连通分量,3结点属于1连通分量

     */

    private int[] id;

    /**

     * count代表连通分量的表示,0,1……

     */

    private int count;



    public CC(Graph graph) {

        marked = new boolean[graph.V()];

        id = new int[graph.V()];

        for (int s=0;s<graph.V();s++){

            if (!marked[s]){

                count++;

                dfs(graph,s);

            }

        }

    }



    private void dfs(Graph graph,int v) {

        marked[v] = true;

        id[v] = count;

        for (int w:graph.adj(v)) {

            if (!marked[w]){

                dfs(graph,w);

            }

        }

    }



    /**

     * v和w是否属于同一连通分量

     * @param v

     * @param w

     * @return

     */

    public boolean connected(int v,int w){

        return id[v]==id[w];

    }



    /**

     * 获得连通分量的数量

     * @return

     */

    public int getCount() {

        return count;

    }



    /**

     * 结点属于哪一个连通分量

     * @param w

     * @return

     */

    public int id(int w){

        return id[w];

    }



} 

在下图中,有三个连通分量。

说完深度优先搜索,我们可以来说一说广度优先搜索算法了。在前面的深度优先搜索中,我们将深度优先搜索算法比喻成迷宫,它可以带我们从一个结点走到另外一个结点(也就是寻找路径问题),但是如果我们需要去解决_最短路径_的问题,使用深度优先搜索能不能解决呢?答案是不能,我们可以想一想,使用深度优先搜索,我们是一条道走到“黑”,有可能离开始结点最近的结点反而还有可能最后遍历。但是广度优先遍历却可以解决这个问题。

广度优先遍历

广度优先的算法在迷宫中类似这样:我们先遍历开始结点的相邻结点并将结点,然后按照与起点的距离的顺序来遍历所有的顶点。在前面的深度优先遍历中,我们使用了隐式的栈【LIFO】(递归)来进行保存结点,而在广度优先遍历中,我们将使用显式的队列(FIFO)来保存结点。

进行广度优先遍历的算法步骤如下:

先将起点加入队列,然后重复以下步骤:

  • 取队列中的下一个顶点v并标记它

  • 将与v相邻的所有未被标记过的结点加入队列


package graph.undir;



import java.util.LinkedList;

import java.util.Queue;

import java.util.Stack;



/**

 * @author xiaohui

 * 广度优先遍历

 */

public class BreadthFirstSearch {

    private boolean[] marked;

    private final int s;

    private int[] e
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值