图类算法总结(一):DFS/BFS

参考自文章:https://blog.csdn.net/ntt5667781/article/details/52743342

1、图的存储

在开始各类图算法之前,先将图的结构进行分类。
图的表示,在实际实现过程中,有以下几种基本的方式可以来表示图。
1) 邻接矩阵:对于较小或者中等规模的图的构造较为适用,因为需要V*V大小的空间。
2) 边的数组:使用一个简单的自定义edge类,还有两个变量,分别代表边的两个端点编号,实现简单,但是在求每个点的邻接点的时候实现较为困难。
3) 邻接表数组:较为常用,使用一个以顶点为索引的数组,数组每个元素都是和该顶点相邻的顶点列表,这种数组占空间相对于邻接矩阵少了很多,并且能很好的找到某个给定点的所有邻接点。

按照图中边的方向将图分成有向图和无向图:
1)无向图:图中的边没有方向。
2)有向图:图中的边有方向。
对于有向图和无向图的具体实现表示可以使用前面介绍的三种方法,两种图在表示的时候大部分的实现代码都是一致的。

普通无向图的邻接数组表示方法以及邻接矩阵表示方法的具体实现代码如下:

package graph;

import java.util.*;

public class Graph {
    private int V;
    private int E;
    private List<Integer>[] adj;
    private int[][] a;
    public Graph(int V){
        this.E = 0;
        this.V = V;
        adj = new ArrayList[V];
        a = new int[V][V];
        for(int i=0;i<V;i++){
            adj[i] = new ArrayList<Integer>();

        }
    }
    //无向图是没有方向的,所以需要在两个定点添加顶点信息
    public void addEdge(int v1,int v2){
        a[v1][v2] = 1;
        a[v2][v1] = 1;
        adj[v1].add(v2);
        adj[v2].add(v1);
        this.E++;
    }

    public int getV(){
        return this.V;
    }

    public int getE(){
        return this.E;
    }
    //邻接数组返回给定点的所有邻接点
    public List<Integer> adj(int i){
        return adj[i];
    }

    //邻接矩阵返回给定点的所有邻接点
    public List<Integer> adj1(int i){
        List<Integer> list = new ArrayList<Integer>();
        for(int j = 0;j<this.V;j++){
            if(a[i][j] == 1){
                list.add(j);
            }
        }
        return list;
    }

    public static void main(String[] args){
        Graph graph = new Graph(5);
        graph.addEdge(0,1);
        graph.addEdge(0,3);
        graph.addEdge(3,1);
        System.out.println(graph.getE());
        List<Integer> adj = graph.adj(0);
        for(int i=0;i<adj.size();i++){
            System.out.print(adj.get(i) + " ");
        }
    }
}

无权有向图的具体实现代码如下:

package graph;

import java.util.*;

public class DirectedGraph {
    private int V;  //图中的顶点数目
    private int E;  //图中的边数目
    private List<Integer>[] adj;  //邻接数组
    private int[][] a;   //邻接矩阵

    public DirectedGraph(int V) {
        this.E = 0;
        this.V = V;
        adj = new ArrayList[V];
        a=new int[V][V];
        for (int i = 0; i < V; i++)
            adj[i] = new ArrayList<>();
    }
    //由于无向图中的边是有方向的,所以添加边的时候需要只需要在起始点的邻接列表中添加顶点信息。
    public void addEdge(int v1, int v2) {
        a[v1][v2]=1;
        adj[v1].add(v2);
        E++;
    }

    public int getV() {
        return V;
    }

    public int getE() {
        return E;
    }
    //邻接数组返回给定点的所有邻接点
    public List<Integer> adj(int i) {
        return adj[i];
    }
    //邻接矩阵返回给定点的所有邻接点
    public List<Integer> adj1(int i){
        List<Integer> list=new ArrayList<Integer>();
        for(int j=0;j<V;j++){
            if(a[i][j] == 1)
                list.add(j);
        }
        return list;
    }

    public static void main(String[] args){
        DirectedGraph graph = new DirectedGraph(5);
        graph.addEdge(0,1);
        graph.addEdge(0,3);
        graph.addEdge(3,1);
        System.out.println(graph.getE());
        List<Integer> adj = graph.adj1(0);
        for(int i=0;i<adj.size();i++){
            System.out.print(adj.get(i) + " ");
        }
    }
}

2、图的遍历算法

2.1 深度优先搜索

介绍两种比较基础的图遍历算法,广度优先搜索和深度优先搜索。
1)深度优先搜索:这是一种典型的递归算法用来搜索图(遍历所有的顶点);
思想:从图的某个顶点i开始,将顶点i标记为已访问顶点,并将访问顶点i的邻接列表中没有被标记的顶点j,将顶点j标记为已访问,并在访问顶点j的邻接列表中未被标记的顶点k依次深度遍历下去,直到某个点的所有邻接列表中的点都被标记为已访问后,返回上层。重复以上过程直到图中的所有顶点都被标记为已访问。
深度优先遍历和树的先序访问非常类似,尽可能深的去访问节点。深度优先遍历的大致过程(递归版本):
a)在访问一个节点的时候,将其设置为已访问。
b)递归的访问被标记顶点的邻接列表中没有被标记的所有顶点
(非递归版本):
图的非递归遍历我们借助栈来实现。
a)如果栈为空,则退出程序,否则,访问栈顶节点,但不弹出栈点节点。
b)如果栈顶节点的所有直接邻接点都已访问过,则弹出栈顶节点,否则,将该栈顶节点的未访问的其中一个邻接点压入栈,同时,标记该邻接点为已访问,继续步骤a。
该算法访问顶点的顺序是和图的表示有关的,而不只是和图的结构或者是算法有关。

深度优先探索是个简单的递归算法(当然借助栈也可以实现非递归的版本),但是却能有效的处理很多和图有关的任务,比如:
a) 连通性:ex:给定的两个顶点是否联通 or 这个图有几个联通子图。
b) 单点路径:给定一幅图和一个固定的起点,寻找从s到达给定点的路径是否存在,若存在,找出这条路径。

寻找路径:
为了实现这个功能,需要在上面实现的深度优先搜索中中增加实例变量edgeTo[],它相当于绳索的功能,这个数组可以找到从每个与起始点联通的顶点回到起始点的路径(具体实现的思路非常巧妙: 从边v-w第一次访问w的时候,将edgeTo[w]的值跟新为v来记住这条道路,换句话说,v-w是从s到w的路径上最后一条已知的边,这样搜索结果就是一条以起始点为根结点的树,也就是edgeTo[]是个有父链接表示的树。)

深度优先搜索的递归实现版本和非递归版本

package graph;

import java.util.*;

public class DepthFirstSearch {
    //用来记录顶点的标记状态 true表示为已访问,false表示为未被访问。
    private boolean[] marked;
    private int count;
    //用来记录顶点索引所对应的父结点,假设遍历是从s到达的t那么edgeTo[s所对的所用]=t;
    private int[] edgeTo;
    //起始点
    private int s;
    private Stack<Integer> stack = new Stack<Integer>();

    public DepthFirstSearch(Graph G,int s){
        marked = new boolean[G.getV()];
        edgeTo = new int[G.getV()];
        this.s = s;
        stack.push(s);
        dfs(G,s);
    }

    //递归形式实现
    public void dfs(Graph G,int s){
        marked[s] = true;
        count ++;
        for(int temp:G.adj(s)){
            if(!marked[temp]){
                edgeTo[temp] = s;
                dfs(G,temp);
            }
        }
    }

    //非递归形式实现
    public void dfs(Graph G){
        while(!stack.isEmpty()){
            s = stack.peek();
            boolean needPop = true;
            marked[s] = true;
            count++;
            for(int temp:G.adj(s)){
                if(!marked[temp]){
                    needPop = false;
                    stack.push(temp);
                    edgeTo[temp]=s;
                    break;
                }
            }
            if(needPop)
                stack.pop();
        }
    }

    public boolean hasPathTo(int v){
        return marked[v];
    }
    //得到到达v的一个路径
    public List<Integer> pathTo(int v){
        if(!hasPathTo(v))
            return null;
        List<Integer> list = new ArrayList<Integer>();
        list.add(v);
        v = edgeTo[v];
        while(v!=s) {
            list.add(v);
            v = edgeTo[v];
        }
        list.add(s);
        Collections.reverse(list);
        return list;
    }

    public int count(){
        return count;
    }

    public static void main(String[] args){
        int V = 5;
        Graph G=new Graph(V);
        G.addEdge(0,1);
        G.addEdge(0,2);
        G.addEdge(1,3);
        G.addEdge(3,4);
        int s=0;
        DepthFirstSearch dfs=new  DepthFirstSearch(G,s);
        for(int v=0;v<G.getV();v++){
            if(dfs.hasPathTo(v))
                for(int x:dfs.pathTo(v))
                    if(x==s)
                        System.out.print(x);
                    else
                        System.out.print("-"+x);
            System.out.println();
        }
    }
}

DFS其实还可以解决很多在无向图中的基础性问题,比如:

计算图中的连通子图的数量

package graph;

public class ConnectComponent {
    private boolean[] marked;
    private int[] id;
    private int count;

    public ConnectComponent(Graph G){
        marked = new boolean[G.getV()];
        id = new int[G.getV()];
        for(int i=0;i<G.getV();i++){
            if(!marked[i]){
                dfs(G,i);
                count++;
            }
        }
    }

    public void dfs(Graph G,int s){
        marked[s] = true;
        id[s] = count;
        for(int temp : G.adj(s)){
            if(!marked[temp]){
                dfs(G,temp);
            }
        }
    }

    public boolean connected(int v,int w){
        return id[v] == id[w];
    }

    public int id(int v){
        return id[v];
    }

    public int getCount(){
        return count;
    }

    public static void main(String[] args){
        int V = 5;
        Graph G=new Graph(V);
        G.addEdge(0,1);
        G.addEdge(0,2);
        G.addEdge(3,4);
        int s=0;
        ConnectComponent graph = new ConnectComponent(G);
        System.out.println(graph.getCount());
        System.out.println(graph.connected(0,2));
        System.out.println(graph.connected(0,4));
    }
}

检测图中是否有环

package graph;

public class CycleDetect {
    private boolean[] marked;
    private boolean flag;

    public CycleDetect(Graph G){
        marked = new boolean[G.getV()];
        for(int s=0;s<G.getV();s++){
            if(!marked[s]){
                dfs(G,s,s);
            }
        }
    }

    public void dfs(Graph G,int s,int initial){
        marked[s] = true;
        for(int temp:G.adj(s)){
            if(!marked[temp]){
                dfs(G,temp,initial);
            } else{
                if(temp == initial){
                    flag = true;
                    return;
                }
            }
        }
    }

    public boolean hasCycle(){
        return flag;
    }
}

二分图问题
即能否用两种颜色给这个图进行着色

package graph;

public class IsBiagraph {
    private boolean[] marked;
    private boolean[] color;
    private boolean flag = true;

    public IsBiagraph(Graph G){
        marked = new boolean[G.getV()];
        color = new boolean[G.getV()];

        for(int s = 0;s<G.getV();s++){
            if(!marked[s]){
                dfs(G,s);
            }
        }
    }

    public void dfs(Graph G,int s){
        marked[s] = true;
        for(int temp:G.adj(s)){
            if(!marked[temp]){
                color[temp] = !color[s];
                dfs(G,temp);
            }
            else{
                if(color[temp]==color[s])
                    flag = false;
            }
        }

    }

    public boolean isBiagragh(){
        return flag;
    }
}

有关二分图的博客参考:https://blog.csdn.net/x_y_q_/article/details/51920683

2.2 广度优先搜索

前面说过,深度优先搜索得到的路径不仅取决于图的结构,还取决于图的表示以及递归调用的性质,但是如果要求最短的路径(给定图G和起始点s寻找给定点v和s间是否存在路径,如果存在,找出最短的路径),那么使用前面的DFS算法并不能解决该问题,所以出现了广度优先搜索BFS来实现这个目的,广度优先搜索也是其他算法的基础。
在程序中,搜索一幅图的时候会遇到有很多条边都需要被遍历的情况,我们会选择其中一条并将其他边留到以后再继续搜索,在DFS中使用栈结构,使用LIFO的规则来描述,从有待搜索的通道中选取最晚遇到的那个通道,然而在BFS算法中,我们希望按照与起点的距离来遍历所有的顶点,使用FIFO(队列)来进行搜索,也就是搜索最先遇到的那个通道。
BFS:使用一个队列来保存所有已经被标记过的但是其邻接点还未被检查过的顶点,现将顶点加入队列中,然后重复下面的操作,直至队列为空:
1)取队列中的下一个顶点v并标记它
2)将与v相邻的所有的未被标记的顶点加入队列中。

广度优先算法

package graph;

import java.util.*;

public class BreadFirstSearch {
    private boolean[] marked;
    private int[] edgeTo;
    //起点
    private int s;

    public BreadFirstSearch(Graph G,int s){
        marked = new boolean[G.getV()];
        edgeTo = new int[G.getV()];
        this.s = s;
        bfs(G,s);
    }

    public void bfs(Graph G,int s){
        Queue<Integer> queue = new LinkedList<Integer>();
        marked[s] = true;
        queue.offer(s);
        while(!queue.isEmpty()){
            s = queue.poll();
            for(int temp:G.adj(s)){
                if(!marked[temp]){
                    marked[temp] = true;
                    edgeTo[temp] = s;
                    queue.offer(temp);
                }
            }
        }
    }

    public boolean hasPathTo(int v){
        return marked[v];
    }

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

推荐阅读更多精彩内容