轻松学懂图(上)——相关概念、DFS和BFS

轻松学懂图(上)——相关概念、DFS和BFS

一、概述

​ 本篇博客主要是对图相关的知识点进行总结并会附上个人的心得,让内容尽可能的通俗易懂

二、图相关知识点
  • 常见应用场景

    • 关系网络:每个顶点就像是一个人,顶点与顶点直接有一条无向边就表示有建立联系
    • 地图:定点作为城市,边作为路径
  • 相关概念

    • 出度:以这个节点为起点的边的个数,其实就是说方向向外的边
    • 入度:以这个节点为终点的边的个数,其实就是说指向自己的边
    • 权值:还记得上面的地图场景吗?权值可以表示两个地点之间路径有多长,也可以理解为开销
  • 图的分类

    • 从有向边和无向边这个角度来看,分为有向图无向图,个人觉得有向图更通用,无向图是有向图中的一个特例。
    • 从图复杂程度这个角度来看,又分为简单图、多重图。简单图即没有平行边(就是两个顶点间一来一回两个有向边),多重图又平行边或者自环(自己指向自己的边)
    • 从有无权值可分为有权图和无权图(故名思意,这里就不多解释了)
    • 然后就是比较常见且稍微特殊的几个图了:
      • 完全图(任意两个顶点之间都存在边),包含有向完全图和无向完全图
      • 连通图(任意两个顶点之间都是存在路径的,这里是对于无向图来说的),这里涉及到一个连通分量的概念,通俗的说:就像是三个村庄构成一个大的无向图,各村之间都不互通,最大的那个村就是这个图的连通分量
  • 实现方案

    1. 邻接矩阵

      看起来就像是一个矩形表格:

      v1v2v3
      v1011
      v2101
      v3110

      解释:v1,v2,v3表示顶点,坐标(v1,v2)这个等于1表示 从v1到v2 有一条边,不难看出,邻接矩阵可以用来存放有向边和无向边的信息

      优缺点:当顶点数量很多边比较少的时候,会有很多的空间用不到,即容易出现内存浪费的情况,因此,当边比较多的时候适合用邻接矩阵

    2. 邻接表

      如下图所示:

      用数组在这里插入图片描述
      来存储每个节点的出度信息,每个数组里面用链表存储了它所指向的顶点在数组中的index,这个应该很好理解。
      在这里插入图片描述

      除此之外,还有一个比较少见的逆邻接表,如下图所示:

      这个和上面的类似,只不过记录的是每个节点的入度信息

      优缺点:

      不难看出,这个邻接表相比较邻接矩阵不会出现空间冗余的情况,但是不足之处也很明显,因为数组中存储的是单链表,因此在进行查找的时候就不得不进行遍历,效率会比较低。

    图的遍历

    ​ 与线性表,树之类的数据结构在遍历的时候不太一样的是需要确定一个起点然后才可以开始遍历。常见的遍历方式有两种——深度优先搜索(DFS)和广度优先搜索(BFS)。接下来我们来看看他们分别是什么:

    1. 深度优先搜索(DFS)

      ​ 所谓深度优先即从起点出发,选择起点的一条路径出发,在到达下一个顶点后,继续选择另一条路径走,然后再继续这样下去,当走到某个顶点时,发现不可以继续”往前(之前走过的点不会选择)“走的时候,即没有路的时候就会退回到上一个节点并选择另一条路,就这样如此反复。

      如果不够形象的话,大家可以回忆一下曾经学过的树,树的后续遍历和这个过程其实很像,一直走走到头,当不能继续的时候再退回来…相信听到这里,比较细心的小伙伴可能就发现了,DFS是一个递归的过程

      下面是DFS的示例代码:

      /**
           * 深度优先遍历——递归
           *
           * @param begin	起点
           */
          @Override
          public void dfs(V begin) {
              Vertex<V, E> vertex = vertices.get(begin);
              if (vertex == null) {
                  return;
              }
              dfs(vertex, new HashSet<>());
          }
      
          /**
           * 
           * @param vertex    起点
           * @param visitedVertices   记录走过的节点
           */
          public void dfs(Vertex<V, E> vertex, Set visitedVertices) {
              System.out.println(vertex.value);
              visitedVertices.add(vertex);
      
              for (Edge<V, E> edge : vertex.outEdges) {
                  if (visitedVertices.contains(edge.to)) {
                      continue;
                  }
                  dfs(edge.to, visitedVertices);
              }
          }
      

      当然,除了递归的方式,也有非递归的,其实本质上是在用栈这一数据结构模拟递归这一过程,示例代码如下图:

      /**
           * 深度优先遍历——非递归
           *  
           * @param begin 起点
           */
          @Override
          public void dfs2(V begin) {
              Vertex<V, E> beginVertex = vertices.get(begin);
              if (beginVertex == null) {
                  return;
              }
      
              // 保存已经遍历过的顶点
              Set<Vertex<V, E>> visitedVertices = new HashSet<>();
              // 栈
              Stack<Vertex<V, E>> stack = new Stack<>();
      
              // 先访问起点
              stack.push(beginVertex);
              visitedVertices.add(beginVertex);
              System.out.println(beginVertex);
      
              // 循环,利用栈模拟递归
              while (!stack.isEmpty()) {
                  // 弹出栈顶元素,遍历它的所有的outEdges
                  Vertex<V, E> vertex = stack.pop();
      
                  for (Edge<V, E> edge : vertex.outEdges) {
                      if (visitedVertices.contains(edge.to)) {
                          // 这条边曾经走过
                          continue;
                      }
                      // 把from放进去的目的是为了能够往回找
                      stack.push(edge.from);
                      stack.push(edge.to);
                      visitedVertices.add(edge.to);
                      System.out.println(edge.to.value);
                  }
              }
          }
      

    2. 广度优先搜索(BFS)

      ​ 所谓广度优先,即从起点开始,先将距离自己距离为“1”的点(通俗的说就是一步就可以到的)先都遍历一遍,然后再遍历距离自己距离为“2”的点,以此类推下去,直至遍历完所有点即结束。其实这个和树的层次遍历特别像,显示第一层,也就是距离为1的那一层,接着是距离为2的那一层。事实上,图的广度优先搜索和树的层次遍历是很像的,我们先回忆一下,树的层次遍历是怎么实现的——用队列先进先出的特性,每遍历一个从队首出来的节点的时候,将其子节点加入到队尾,直至队列为空。图的广度优先搜索也是使用的这样一个方法,示例代码如下图所示:

      /**
           * 深度优先搜索(BFS)——队列
           * @param begin 起点
           */
          @Override
          public void bfs(V begin) {
              Vertex<V, E> beginVertex = vertices.get(begin);
              if (beginVertex == null) {
                  return;
              }
      
              // Set集合,用于存放遍历过的顶点
              Set<Vertex<V, E>> visitedVertex = new HashSet<>();
              // 队列,用于存放待遍历过顶点
              Queue<Vertex<V, E>> queue = new LinkedList<>();
              queue.offer(beginVertex);
              visitedVertex.add(beginVertex);
      
              while (!queue.isEmpty()) {
                  Vertex<V, E> vertex = queue.poll();
                  System.out.println(vertex.value);
                  // 遍历该顶点所有的outEdges
                  for (Edge edge : vertex.outEdges) {
                      // 将他指向的且未入过队的顶点入队
                      if (visitedVertex.contains(edge.to)) {
                          continue;
                      }
                      queue.add(edge.to);
                      // 入队后,加入visitedVertex集合中
                      visitedVertex.add(edge.to);
                  }
              }
          }
      
    小结

    ​ 本篇文章简述了图相关的知识点,以及DFS与BFS这两种遍历方式,内容不是很难,在理解这些是如何做的之后,其代码实现还是比较简单的。

    ​ 之后的内容将会以大家容易接受的方式去说说拓扑排序、Kruskal、Prim、Dijkstra、Bellman-ford以及Floyd算法,尽可能的用大白话让内容通俗易懂,不做搬运工

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

生命中有太多不确定

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值