算法:深度和广度优先搜索,找出社交网络中的三度好友关系

什么是"搜索"算法?

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

图上的搜索算法,最直接的理解就是,在图种找出从一个顶点出发,到另一个顶点的路径。具体方法有很多,比如最简单最直接的深度优先、广度优先搜索,还有A*、IDA*等启发式搜索算法。深度优先搜索算法和广度优先搜索算法,既可以用在无向图,也可以用在有向图上。

下面以邻接表表示的无向图为例,学习深度优先搜索算法和广度优先搜索算法。其无向图实现如下:

package com.company;

import java.util.Arrays;
import java.util.LinkedList;

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<>();
        }
    }

    /**
     * 添加边
     *
     * @param s 顶点
     * @param t 顶点
     */
    public void addEdge(int s, int t){ // 无向图一条边存两次
        adj[s].add(t);
        adj[t].add(s);
    }


     void printGraph() {
        for (int i = 0; i < v; i++) {
            System.out.println("Adjacency list of vertex " + i);
            System.out.print("head");
            for (Integer pCrawl : adj[i]) {
                System.out.print(" -> " + pCrawl);
            }
            System.out.println("\n");
        }
    }



    public static void main(String[] args) {
        Graph graph = new Graph(8);
        graph.addEdge(0,1);
        graph.addEdge(0,1);
        graph.addEdge(0,3);
        graph.addEdge(1,2);
        graph.addEdge(1,4);
        graph.addEdge(2,5);
        graph.addEdge(4,5);
        graph.addEdge(4,6);
        graph.addEdge(5,7);
        graph.addEdge(6,7);
        graph.printGraph();
    }
}

广度优先搜索(BFS)

代码实现如下,我们搜索一条从 s 到 t 的路径。实际上,这样求得的路径就是从 s到 t 的最短路径:

    /*
    * @param s 起始顶点
    * @param s 终止顶点
    * */
    public void bfs(int s, int t){
        if(s == t){
            return;
        }

        /*
        * visited是用来记录已经被访问的顶点,用来避免顶点被重复访问。
        * 如果顶点 q 被访问,那相应的 visited[q] 会被设置为 true。
        * */
        boolean [] visited = new boolean[v]; //v为顶点个数
        visited[s] = true; // 起始顶点(已经被访问过)设为true

        /*
        * queue是一个队列,用来存储已经被访问,但相连的顶点还没有被访问
        * 的顶点。因为广度优先搜索是逐层访问的,也就是说,我们只有把第k层
        * 的顶点都访问完成之后,才能访问第k+1层的顶点。当我们访问到第k层
        * 的顶点的时候,我们需要把第k层的顶点记录下来,稍后才能通过第k层
        * 的顶点来找第k+1层的顶点。所以,我们用这个队列来实现记录的功能
        * */
        Queue<Integer> queue = new LinkedList<>();
        queue.add(s);
        /*
        * prev用来记录搜索路径。当我们从顶点s开始,广度优先搜索到订单t之
        * 后,prev数组中存储的就是搜索的路径。不过,这个路径是反向存储的。
        * prev[w]存储的是,订单w是从哪个前驱顶点遍历过来的。比如,我们
        * 通过顶点2的邻接表访问到顶点3,那prev[3]就等于2.为了正向打印
        * 出路径,我们需要递归的来打印,也就是调用print()函数
        * */
        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);
                if(!visited[q]){
                    prev[q] = w;
                    if(q == t){
                        print(prev, s, t);
                        return;
                    }
                }
                visited[q] = true;
                queue.add(q);
            }
        }
    }

在这里插入图片描述
在这里插入图片描述
掌握了广优先搜索算法的原理,我们来看下,广度优先搜索的时间、空间复杂度是多少呢?

  • 最坏情况下,终止顶点t离起始顶点s很远,需要遍历完整个图才能找到。这个时候,每个顶点都要进出一遍队列,每个边也都会被访问一次。所以,广度优先搜索的时间复杂度是O(V+E),其中V表示顶点的个数,E表示边的个数。当然,对于一个连通图来说,也就是说一个图中的所有顶点都是连通的,,E 肯定要大于等于 V-1,所以,广度优先搜索的时间复杂度也可以简写为 O(E)
  • 广度优先搜索的空间消耗主要在几个辅助变量 visited 数组、queue 队列、prev 数组上。这三个存储空间的大小都不会超过顶点的个数,所以空间复杂度是 O(V)。

深度优先搜索(DFS)

如何在图中应用深度优先搜索,来找某个顶点到另一个顶点的路径。

如下图。搜索的起始顶点是s,终止顶点是t。我们希望在图中寻址一条从顶点s到顶点t的路径。如果映射到迷宫那个例子,s 就是你起始所在的位置,t 就是出口。

下图为深度递归算法找到的整个搜索的路径。这里面实线箭头表示遍历,虚线箭头表示回退。从图中我们可以看出,深度优先搜索找出来的路径,并不是顶点 s 到顶点 t 的最短路径

在这里插入图片描述
实际上,深度优先搜索用的是一种比较著名的算法思想,回溯思想。这种思想解决问题的过程,非常适合用递归来实现。

我们可以看出,深度优先搜索代码实现也用到了prev、visited变量以及print函数,它们跟广度优先搜索代码实现的作用是一样的。不过,深度优先搜索代码实现里面,有关比较特殊的变量found,它的作用是,当我们已经找到终止顶点t之后,我们就不再递归的继续查找了

    boolean found = false;
    private void recurDfs(int w, int t, boolean[]vistied, int[] prev){
        if(found == true){
            return;
        }
        vistied[w] = true;
        if(w == t){
            found = true;
            return;
        }
        for (int i = 0; i < adj[w].size(); i++) {
            int q = adj[w].get(i);
            if(!vistied[q]){
                prev[q] = w;
                recurDfs(q, t, vistied, prev);
            }
        }
    }


    public void dfs(int s, int t){
        found = false;
        boolean[] vistied = new boolean[v];
        int []prev = new int[v];
        for (int i = 0; i < v; i++) {
            prev[i] = -1;
        }

        recurDfs(s, t, vistied, prev);
        print(prev, s, t);
    }


我们来看,深度度优先搜索的时间、空间复杂度是多少呢?

  • 从上面图可以看出,每条边最多会被访问两次,一次是遍历,一次是回溯。所以,时间复杂度是O(E),E表示边的个数
  • 深度优先搜索算法的消耗内存主要是 visited、prev 数组和递归调用栈。visited、prev 数组的大小跟顶点的个数 V 成正比,递归调用栈的最大深度不会超过顶点的个数,所以总的空间复杂度就是 O(V)

如何找出社交网络中的三度好友关系?

社交网络可以用图来表示。这个问题就非常适合用图的广度优先搜索算法来解决。因为广度优先搜索是层层往外推进的。首先,遍历与起始顶点最近的一层顶点,也就是用户的一度好友,然后再遍历与用户距离的边数为 2 的顶点,也就是二度好友关系,以及与用户距离的边数为 3 的顶点,也就是三度好友关系。

小结

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

  • 广度优先搜索,通俗的理解就是,地毯式层次推进,从起始顶点开始,依次往外遍历。广度优先搜索需要借助队列来实现,遍历得到的路径就是起始顶点到终止顶点的路径。
  • 深度优先搜索,利用的是回溯的思想,非常适合用递归来实现。也就是说,深度优先搜索是借助栈来实现的。

在执行效率方面,深度优先和广度优先搜索的时间复杂度都是 O(E),空间复杂度是O(V)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值