数据结构系列第三部分:图

1)文笔有限,如果发现博客有书写有误的地方恳请读者直言不讳,我一定会第一时间改正。
2)代码的具体实现可以参考代码中的注释,如果由于注释不清楚而不明白相应原理,可以与作者私聊。码字不易,有兴趣的小伙伴点个赞呗,大家相互学习。
3)本篇博客为数据结构系列第三部分:图,如需了解数据结构的其它部分,欢迎点击链接。

  1. 数据结构系列绪论部分:为什么需要数据结构
  2. 数据结构系列第一部分:
  3. 数据结构系列第二部分:
  4. 数据结构系列第三部分:
  5. 数据结构系列第四部分:散列
  6. 数据结构系列第五部分:递归
  7. 数据结构系列第六部分:排序
  8. 数据结构系列第七部分:查找

1 为什么需要图这种数据结构

  • 线性表局限在只有一个直接前驱和一个直接后继的关系;
  • 虽然有多个直接后继,但是也只能有一个直接前驱,即父节点;
  • 就可以完成多个直接前驱和多个直接后继的功能。
     

2 图的种类

在这里插入图片描述
图是一种数据结构,其中节点可以具有零个或多个相邻元素,同时两个节点之间的连接称为边。
 

3 图的表示方式

图的表示方式有两种,一种是利用二维数组(邻接矩阵)来表示,另一种是利用一维数组+链表(邻接表)来表示。

3.1 邻接矩阵

邻接矩阵是表示图中节点之间相邻关系的矩阵,对于 n 个节点的图而言,矩阵中的 row 和 col 表示的是 1…n 个点。
在这里插入图片描述

3.2 邻接表

邻接矩阵需要为每个节点都分配 n 个边的空间,然而事实上有很多边都是不存在的,会造成空间的一定损失。这个时候就需要邻接表,邻接表的实现只关心存在的边,不关心不存在的边,因此没有空间浪费,邻接表由数组+链表组成

在这里插入图片描述
 

4 图的实现

下面我用邻接矩阵来实现图这一数据结构。

4.1 思路

利用一个 ArrayList 集合来存储 String 顶点,使用一个二维数组 int[][] edges 来存储边。

需求:用邻接矩阵来实现如下所示的图:
在这里插入图片描述

4.2 代码实现

代码已经走通。

import java.util.ArrayList;
import java.util.Arrays;

public class GraphDemo{
    public static void main(String[] args) {
        //节点的个数
        int n=8;
        //待插入的节点
        String[] vertexs ={"1", "2", "3", "4", "5", "6", "7", "8"};
        //示例化一个图对象
        Graph graph=new Graph(8);
        //循环的添加顶点
        for(String vertex: vertexs) {
            graph.insertVertex(vertex);
        }
        //更新边的关系
        graph.insertEdge(0, 1, 1);
        graph.insertEdge(0, 2, 1);
        graph.insertEdge(1, 3, 1);
        graph.insertEdge(1, 4, 1);
        graph.insertEdge(3, 7, 1);
        graph.insertEdge(4, 7, 1);
        graph.insertEdge(2, 5, 1);
        graph.insertEdge(2, 6, 1);
        graph.insertEdge(5, 6, 1);

        //显示一把邻结矩阵
        graph.showGraph();
    }
}

class Graph {
    /**
     * 存储顶点集合
     */
    public ArrayList<String> vertexList;
    /**
     * 存储图对应的邻结矩阵
     */
    public int[][] edges;
    //表示边的数目
    public int numOfEdges;

    /**
     * 构造器
     * @param n 顶点的个数
     */

    public Graph(int n) {
        //初始化矩阵和vertexList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);
        numOfEdges = 0;
    }
    /**
     * 插入节点
     * @param vertex 节点的名称
     */

    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }
    /**
     * 插入边
     * @param v1 v1节点的在集合中的下标
     * @param v2 v2节点的在集合中的下标
     * @param weight 边的权重
     */

    public void insertEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }
    /**
     * 得到边的数目
     */
    public int getNumOfEdges() {
        return numOfEdges;
    }

    /**
     * 返回结点i(下标)对应的数据,如示例中,0->"A" 1->"B" 2->"C"
     * @param i 下标
     * @return 数组
     */
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    /**
     * 返回连接v1和v2边的的权值
     * @param v1
     * @param v2
     * @return
     */
    public int getWeight(int v1, int v2) {
        return edges[v1][v2];
    }

    /**
     * 显示图对应的矩阵,不包括节点
     */
    public void showGraph() {
        for(int[] link : edges) {
            //直接输出数组的元素
            System.err.println(Arrays.toString(link));
        }
    }
}

 

5 图的遍历

图的遍历就是依次访问所有的节点,其分为深度优先遍历和广度优先遍历。

5.1 图的深度优先遍历

5.1.1 定义

一个图有很多的节点,如果遍历这些节点,需要一些特定策略,首先介绍的是深度优先遍历策略(Depth First Search)

深深度优先遍历的策略就是:首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点, 可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。

可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。显然,深度优先搜索是一个递归的过程。

5.1.2 算法步骤

  1. 访问初始节点 i,并标记节点 i 为已访问;
  2. 查找节点 i 的第一个邻接节点 w;(“第几个” 的定义是根据构建图的数组中各元素的关系来决定;“邻接节点” 的定义是这两个节点之间有边相连来决定)
    • 如果 w 不存在,则回到第一步,从 i 的下一个节点继续;
    • 如果 w 存在,则继续后续的第3步;
  3. 判断:
    • 如果 w 未被访问过,则对 w 进行深度优先遍历递归;(即把 w 当做另一个 i,然后进行步骤 1 和步骤 2);
    • 如果 w 已经被访问了,则查找节点 i 的 w 邻接节点的下一个邻接结点,转到步骤 2;

5.1.3 实例与代码实现

实例:利用深度优先遍历实现下面 图 的遍历。
在这里插入图片描述
代码实现

import java.util.ArrayList;
import java.util.Arrays;

public class DFSGraphDemo {
    public static void main(String[] args) {
        //节点的个数
        int n = 8;
        //待插入的节点
        String[] vertexs = {"1", "2", "3", "4", "5", "6", "7", "8"};
        //示例化一个图对象
        DFSGraph graph = new DFSGraph(8);
        //循环的添加顶点
        for (String vertex : vertexs) {
            graph.insertVertex(vertex);
        }
        //更新边的关系
        graph.insertEdge(0, 1, 1);
        graph.insertEdge(0, 2, 1);
        graph.insertEdge(1, 3, 1);
        graph.insertEdge(1, 4, 1);
        graph.insertEdge(3, 7, 1);
        graph.insertEdge(4, 7, 1);
        graph.insertEdge(2, 5, 1);
        graph.insertEdge(2, 6, 1);
        graph.insertEdge(5, 6, 1);

        //显示一把邻结矩阵
        graph.showGraph();
        //深度优先遍历
        //1->2->4->8->5->3->6->7
        graph.dfs();
    }
}

class DFSGraph {
    /**
     * 存储顶点集合
     */
    public ArrayList<String> vertexList;
    /**
     * 存储图对应的邻结矩阵
     */
    public int[][] edges;
    //表示边的数目
    public int numOfEdges;
    //定义给数组boolean[], 记录某个结点是否被访问
    private boolean[] isVisited;

    /**
     * 构造器
     *
     * @param n 顶点的个数
     */
    public DFSGraph(int n) {
        //初始化矩阵和vertexList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);
        numOfEdges = 0;
    }

    /**
     * 插入节点
     *
     * @param vertex 节点的名称
     */

    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    /**
     * 插入边
     *
     * @param v1     v1节点的在集合中的下标
     * @param v2     v2节点的在集合中的下标
     * @param weight 边的权重
     */

    public void insertEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }

    /**
     * 返回结点的个数
     *
     * @return
     */
    public int getNumOfVertex() {
        return vertexList.size();
    }

    /**
     * 得到边的数目
     */
    public int getNumOfEdges() {
        return numOfEdges;
    }

    /**
     * 返回结点i(下标)对应的数据,如示例中,0->"A" 1->"B" 2->"C"
     *
     * @param i 下标
     * @return 数组
     */
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    /**
     * 返回连接v1和v2边的的权值
     *
     * @param v1
     * @param v2
     * @return
     */
    public int getWeight(int v1, int v2) {
        return edges[v1][v2];
    }

    /**
     * 显示图对应的矩阵,不包括节点
     */
    public void showGraph() {
        for (int[] link : edges) {
            //直接输出数组的元素
            System.err.println(Arrays.toString(link));
        }
    }

    //下面是深度优先遍历加上的

    /**
     * 得到第一个邻接结点的下标 w
     *
     * @param index
     * @return 如果存在就返回对应的下标,否则返回-1
     */
    public int getFirstNeighbor(int index) {
        for (int j = 0; j < vertexList.size(); j++) {
            if (edges[index][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    /**
     * 根据前一个邻接结点的下标来获取下一个邻接结点
     *
     * @param v1
     * @param v2
     * @return
     */
    public int getNextNeighbor(int v1, int v2) {
        for (int j = v2 + 1; j < vertexList.size(); j++) {
            if (edges[v1][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    private void dfs(boolean[] isVisited, int i) {
        //首先我们访问该结点,输出
        System.out.print(getValueByIndex(i) + "->");
        //将结点设置为已经访问
        isVisited[i] = true;
        //查找结点i的第一个邻接结点w
        int w = getFirstNeighbor(i);
        //说明有
        while (w != -1) {
            if (!isVisited[w]) {
                dfs(isVisited, w);
            }
            //如果w结点已经被访问过
            w = getNextNeighbor(i, w);
        }
    }

    //对dfs 进行一个重载, 遍历我们所有的结点,并进行 dfs
    public void dfs() {
        isVisited = new boolean[vertexList.size()];
        //遍历所有的结点,进行dfs[回溯]
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                dfs(isVisited, i);
            }
        }
    }
}

5.2 图的广度优先遍历

5.2.1 定义

图的广度优先搜索(Broad First Search) 类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的节点的顺序,以便按这个顺序来访问这些节点的邻接结点。

5.2.2 算法步骤

  1. 访问初始结点 v 并标记结点 v 为已访问;
  2. 结点 v 入队列;
  3. 当队列非空时,继续执行,否则当前三步算法结束;
  4. 出队列,取得队头结点 u;
  5. 查找结点 u 的第一个邻接结点 w ;
  6. 若结点 u 的邻接结点 w 不存在,则转到步骤 3;否则循环执行以下三个步骤:
    6.1 若结点 w 尚未被访问,则访问结点 w 并标记为已访问;
    6.2 结点 w 入队列 ;
    6.3 查找结点 u 的继 w 邻接结点后的下一个邻接结点 w,转到步骤 6。

5.2.3 实例与代码实现

实例:利用深度优先遍历实现下面 图 的遍历。
在这里插入图片描述
代码实现

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

public class BFSGraphDemo {
    public static void main(String[] args) {
        //节点的个数
        int n = 8;
        //待插入的节点
        String[] vertexs = {"1", "2", "3", "4", "5", "6", "7", "8"};
        //示例化一个图对象
        BFSGraph graph = new BFSGraph(8);
        //循环的添加顶点
        for (String vertex : vertexs) {
            graph.insertVertex(vertex);
        }
        //更新边的关系
        graph.insertEdge(0, 1, 1);
        graph.insertEdge(0, 2, 1);
        graph.insertEdge(1, 3, 1);
        graph.insertEdge(1, 4, 1);
        graph.insertEdge(3, 7, 1);
        graph.insertEdge(4, 7, 1);
        graph.insertEdge(2, 5, 1);
        graph.insertEdge(2, 6, 1);
        graph.insertEdge(5, 6, 1);

        //显示一把邻结矩阵
        graph.showGraph();
        //深度优先遍历
        //1=>2=>3=>4=>5=>6=>7=>8
        graph.bfs();
    }
}

class BFSGraph {
    /**
     * 存储顶点集合
     */
    public ArrayList<String> vertexList;
    /**
     * 存储图对应的邻结矩阵
     */
    public int[][] edges;
    //表示边的数目
    public int numOfEdges;
    //定义给数组boolean[], 记录某个结点是否被访问
    private boolean[] isVisited;

    /**
     * 构造器
     *
     * @param n 顶点的个数
     */
    public BFSGraph(int n) {
        //初始化矩阵和vertexList
        edges = new int[n][n];
        vertexList = new ArrayList<String>(n);
        numOfEdges = 0;
    }

    /**
     * 插入节点
     *
     * @param vertex 节点的名称
     */

    public void insertVertex(String vertex) {
        vertexList.add(vertex);
    }

    /**
     * 插入边
     *
     * @param v1     v1节点的在集合中的下标
     * @param v2     v2节点的在集合中的下标
     * @param weight 边的权重
     */

    public void insertEdge(int v1, int v2, int weight) {
        edges[v1][v2] = weight;
        edges[v2][v1] = weight;
        numOfEdges++;
    }

    /**
     * 返回结点的个数
     *
     * @return
     */
    public int getNumOfVertex() {
        return vertexList.size();
    }

    /**
     * 得到边的数目
     */
    public int getNumOfEdges() {
        return numOfEdges;
    }

    /**
     * 返回结点i(下标)对应的数据,如示例中,0->"A" 1->"B" 2->"C"
     *
     * @param i 下标
     * @return 数组
     */
    public String getValueByIndex(int i) {
        return vertexList.get(i);
    }

    /**
     * 返回连接v1和v2边的的权值
     *
     * @param v1
     * @param v2
     * @return
     */
    public int getWeight(int v1, int v2) {
        return edges[v1][v2];
    }

    /**
     * 显示图对应的矩阵,不包括节点
     */
    public void showGraph() {
        for (int[] link : edges) {
            //直接输出数组的元素
            System.err.println(Arrays.toString(link));
        }
    }

    //下面是深度优先遍历加上的

    /**
     * 得到第一个邻接结点的下标 w
     *
     * @param index
     * @return 如果存在就返回对应的下标,否则返回-1
     */
    public int getFirstNeighbor(int index) {
        for (int j = 0; j < vertexList.size(); j++) {
            if (edges[index][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    /**
     * 根据前一个邻接结点的下标来获取下一个邻接结点
     *
     * @param v1
     * @param v2
     * @return
     */
    public int getNextNeighbor(int v1, int v2) {
        for (int j = v2 + 1; j < vertexList.size(); j++) {
            if (edges[v1][j] > 0) {
                return j;
            }
        }
        return -1;
    }

    /**
     * 对一个结点进行广度优先遍历的方法
     *
     * @param isVisited
     * @param i
     */
    private void bfs(boolean[] isVisited, int i) {
        int u; // 表示队列的头结点对应下标
        int w; // 邻接结点w
        //队列,记录结点访问的顺序。用LinkedList来表示队列。
        LinkedList queue = new LinkedList();
        //访问结点,输出结点信息
        System.out.print(getValueByIndex(i) + "=>");
        //标记为已访问
        isVisited[i] = true;
        //将结点加入队列
        queue.addLast(i);

        while (!queue.isEmpty()) {
            //取出队列的头结点下标
            u = (Integer) queue.removeFirst();
            //得到第一个邻接结点的下标 w
            w = getFirstNeighbor(u);
            while (w != -1) {//找到
                //是否访问过
                if (!isVisited[w]) {
                    System.out.print(getValueByIndex(w) + "=>");
                    //标记已经访问
                    isVisited[w] = true;
                    //入队
                    queue.addLast(w);
                }
                //以u为前驱点,找w后面的下一个邻结点
                w = getNextNeighbor(u, w); //体现出我们的广度优先
            }
        }

    }

    //遍历所有的结点,都进行广度优先搜索
    public void bfs() {
        isVisited = new boolean[vertexList.size()];
        for (int i = 0; i < getNumOfVertex(); i++) {
            if (!isVisited[i]) {
                bfs(isVisited, i);
            }
        }
    }
}

5.3 深度与广度优先遍历的比较

我用一个矩阵图来介绍两者的区别。如下图所示,其为某个图的邻接矩阵a[][ ]表示,为便于理解,接下来我用矩阵的元素位置来说明。
Alt

深度优先搜索:

  1. 首先是 A,这个时候如矩阵所示,会发现其右边第一个元素 a[0][1]1,则说明其邻接节点存在,则用 B 来当首节点,因为 B 是首节点了,所以看第二行,a[1][0]访问过,所以跳过,a[1][1]0,跳过,a[1][2]1,所以说明其邻接节点存在,则用C来当首节点,所以看第三行,a[2][0]a[2][1]访问过了,跳过,后面的都为0,所以第三行不存在邻接节点。所以回到第一步,用A的第二个邻接节点C来继续,会发现不行……
  2. 然后开始用B作为首节点,也就是第二行开始,会发现a[1][3]1,则用D来当首节点,然后不行,然后用用B的第二个邻接节点E来继续,然后不行。
  3. 然后开始用C作为首节点,不行,以此类推,直到结束。
  4. ……

广度优先搜索:

  1. 首先是A,发现其邻接矩阵B存在,然后发现B后面C存在。
  2. 然后是B,分别发现DE
  3. ……

结合邻接矩阵会比较简单。

碎碎念:
数据结构学习完毕,起始于寒假,终止于寒假,Leetcode开始刷起来。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值