Java数据结构之图的基本概念和算法,深度优先遍历DFS,广度优先遍历BFS(图解)


前言

千呼万唤始出来,终于开始学图喽 , 虽然学校的课程早结课了(水完了) , 话不多说,开始整活,本文用地是无向图


提示:以下是本篇文章正文内容,下面案例可供参考

一、图的基本概念

下面讲几个常用的概念,

1.图的定义

图(graph) ,是由两个集合组成的 ,其一为顶点集, 其二为边集 ,其中顶点集不允许为空 ,边集可为空, 也就是说,只有一个顶点没有边的图也可以叫做图

2.基本术语

1. 度,出/入度: 顶点的度是指与该顶点相关联的边的个数,对于有向图,度还细分
	为入度和出度, 其度=入度+出度, 一般地, 度数 / 2 = 边数
2. 路径和路径长度: 路径指的是从顶点到顶点的弧或直线, 而路径长度是指从
	某一顶点出发到另一顶点所经过的路径的条数
3.连通: 有路径即为连通,如果图中任意两个顶点间都有路径连接,就是连通图,
	而连通分量指的是图中包含的最大连通子图
4.生成树: 树中包含了图的所有顶点,和n-1条边(也就是说生成树就是缺了一条边
	,使其无法连通的图)

二、图的基本算法

讲图的算法前, 得先讲讲构建一张图需要哪些属性

我们都知道图是由边和顶点构成的,因此在构造数据结构的时候,需要一个二维数组存放边的两个顶点 (邻接矩阵) , 需要一个一维数组存放顶点, 一个布尔数组用于标记各个顶点是否已被遍历, 一个整型变量表示边的数量

1.初始化图

最方便的方式就是通过构造函数来初始化一个图, 在初始化时, 顶点个数是必须要作为参数传入的,因为一张图可以没有边,但是必须要有顶点

代码如下(示例):

	//传入顶点个数numOdVertex
    public Graph(Integer numOdVertex) {
    
    
        this.vertexs = new ArrayList<>();
        this.edges = new int[numOdVertex][numOdVertex]; //矩阵的大小等于顶点个数的平方
        this.numOdEdges = 0; //边数默认为0
        this.isVisited = new boolean[numOdVertex];
    }

2.插入顶点和边

在对图完成初始化之后, 还要对图的点边关系进行构造 , 也就是插入顶点和边的操作.

在添加顶点的时候, 我们只需要简单地向顶点数组中加入元素即可, 因为这个顶点可以没有连接边

在添加边的时候, 我们需要按顶点的索引对边进行赋权值操作 , 由于是无向图, 两端顶点索引对应的值要双面赋值(这点详见下面代码)

代码如下(示例):

	//添加边 ,三个要素,边的两个顶点(邻接矩阵的两个元素中的索引,无向图添加是双向的)
    public void insertEdge(Integer vertex01,Integer vertex02,int weight) {
    
    
        this.edges[vertex01][vertex02] = weight; //双面赋值
        this.edges[vertex02][vertex01] = weight;
        //边数++
        this.numOdEdges++;
    }

3.矩阵打印

这里有一个小技巧 ,也是第一次碰到的, 对于一个二维数组 , 我们可以将其看作是一个一维数组, 那么其中的元素其实就是一个个的一维数组, 对于一维数组, 我们完全可以通过Arrays工具类直接输出它 ,这里能上一层for循环, 代码优雅很多

	//打印图(邻接矩阵)
    public void showGraph(){
    
    
        for (int[] row:
             this.edges) {
    
    
            System.out.println(Arrays.toString(row));
        }
    }

4.返回第一个邻接结点的下标

可以参见下文广度优先算法的那张矩阵图 , 我们将矩阵按行分为了5组 , 那么每组代表一个结点, 纵向的每一列可以视为该结点的邻接点 , 那么就拿下图来说, A点只与E点有连接, 因此值为1 ,其余为0

因此在遍历的过程中 , 结点个数将作为循环的上限

//返回第一个临接结点的下标
   public Integer getFirstAdjust(int index){
    
     //传入当前当前结点的下标
       for(int i = 0 ; i < this.vertexs.size() ; i++){
    
    
           if(this.edges[index][i] > 0){
    
     //找到了第一个临接结点
               return i;
           }
       }
       return -1;
   }

5.返回第一个邻接结点的下一个结点的下标

既然要求出下一个结点的下标 , 那么我们就需要两个参数 ,一个是当前结点的下标,一个是与当前结点邻接的结点的下标, 这个函数非常重要,在下面会着重介绍它的作用,这里先给出代码

//返回第一个临接结点的下一个结点的下标
    public Integer getNextAdjust(int index01,int index02){
    
    
        for(int i = index02+1 ; i < this.vertexs.size() ; i++){
    
    
            if(edges[index01][i] > 0){
    
    
                return i;
            }
        }
        return -1;
    }

本文的重头戏来了,最麻烦的DFS & BFS

三、深度优先算法

四、广度优先算法

广度遍历类似于层级遍历 , 需要将当前结点的全部邻接结点都遍历完, 才能开始下一层遍历

我们在搭建邻接矩阵的时候, 将两个连通的顶点间的权值设为了1 ,不连通的顶点权值设为0,这样在进行广度优先遍历的时候,当遇到矩阵中值为1的位置时, 说明它和所在行所代表的顶点是连通的

在遍历过程中还需一个LinkedList作为辅助, 其原因也是在于这是广度遍历, 我们需要存下结点的邻接结点的下标, 在找到邻接结点的下标以后, 就要在邻接矩阵中,通过该下标找到该邻接结点的所在行,再对该行进行遍历,找出新的,还没被遍历过的结点 ,以此类推

在这里插入图片描述
就上图而言, 我们的第一个结点是A ,于是从第一行开始遍历, B,C,D都与A无连接 , 只有E有, 通过getFirstAdjust方法获取第一个临接结点的下标, 跳转到最后一行E行 , 从第一列开始遍历, 找到了C,再以此类推 ,与A点相邻的所有结点都被遍历完了 , 再开始下一个结点的广度优先遍历

,最后得出的广度优先遍历顺序为AECBD ,与深度遍历同

	public void bfs(){
    
    
        for(int i = 0 ; i < this.getNumOfVertex() ; i++){
    
    
            if(!isVisited[i]){
    
    
                bfs(i,isVisited);
            }
        }
    }

    private void bfs(int index,boolean[]isVisited) {
    
    
        int head; //队列头结点对应的下标
        int next; //临接结点对应的下标
        LinkedList queue = new LinkedList();
        System.out.println(this.getValueByIndex(index) + "=>");
        isVisited[index] = true;
        queue.addLast(index);
        while(!queue.isEmpty()){
    
     //队列不为空
            head = (Integer)queue.removeFirst();
            next = this.getFirstAdjust(head);
            while(next != -1){
    
    
                if(!isVisited[next]){
    
     //临接结点存在且没被访问过
                    //输出并标记为已访问
                    System.out.println(this.getValueByIndex(next));
                    isVisited[next] = true;
                    //入队
                    queue.addLast(next);
                }
                next = this.getNextAdjust(head,next);
                //this.dfs(head,isVisited);
            }
        }
    }

该处使用的url网络请求的数据。


总结

这两个算法确实非常的难理解,
今天先写这么多.

猜你喜欢

转载自blog.csdn.net/qq_45596525/article/details/110038250