图的最小路径

301 阅读4分钟

数据结构中的图, 和平常画画的图是不一样的, 数据结构中的图一般是指顶点和顶点之间通过连接构成的图, 如下图所示, 图的核心就是 顶点顶点之间相连的边

image.png

那么我们要怎么来表示图呢, 主要就是用集合来存储所有的顶点, 再用集合来存储顶点所相连的顶点, 就是边. 下面的代码就是表示图的构建, 我们就简单的用0, 1, 2, 3, 4来表示顶点

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

  /**
   * 往无向图中添加边(即两个点间建立连接
   * 就两个点中间,s对应的边加上j, j对应的边加上s
   * 没有考虑到重复的情况
   * @param s
   * @param j
   */
  public void add(int s, int j) {
    adj[s].add(j);
    adj[j].add(s);
  }
 
}

遍历

了解了图的结构, 那么要怎么样才能遍历图呢, 说到遍历一般有有两种,深度优先和广度优先.接下来让我们通过这两种遍历方式来打印从图中的起始顶点到结束顶点的路径.

广度优先遍历

通过广度优先遍历来打印从起始顶点到结束顶点的路径.广度优先遍历是指从起始顶点开始, 先遍历跟这个顶点相连的顶点, 之后再依次广度优先遍历刚才相连的的顶点, 直到遍历到目标节点. 广度优先遍历一般是借助队列(FIFO先进先出), 具体实现的话有几个很巧妙的地方

  • 通过boolean[] visited 来表示当前顶点是否被访问过, true为已访问, false为未访问
  • 通过int[] prevs 来表示距离最近的顶点
  • 通过Queue来存储经过的顶点
  • 遍历到目标顶点时, 通过prevs, 往前递归, 获取和他相连的节点 由于广度优先遍历, 每次遍历都是最近的一层, 并且访问过的顶点后续就不会再被访问, 这就使得prevs数组里顶点的前置顶点, 都是距离原节点最近的一个值, 也就说通过广度优先遍历, 最后打印出来的路径就是最小路径

image.png 代码:

/**
   * Breadth-First-Search 深度优先遍历
   * 借助队列,
   * @param s
   * @param t
   */
  public void bfs(int s, int t) {
    // 顶点是否被访问到
    boolean[] visited = new boolean[v];
    visited[s] = true;
    // 顶点最先被哪个前置顶点访问
    int[] prevs = new int[v];
    Arrays.fill(prevs, -1); // 默认顶点都是未被访问的
    Queue<Integer> queue = new LinkedList<>();
    queue.add(s);
    while (!queue.isEmpty()) {
      int w = queue.poll();
      for (int i = 0; i < adj[w].size(); ++i) {
        int q = adj[w].get(i);
        if (!visited[q]) { // q没有被访问过
          prevs[q] = w; // q 节点的前一个访问是w
          if (q == t) {
            printRoad(prevs, q, t);
            return;
          }
          visited[q] = true;
          queue.add(q);
        }
      }
    }
  }

  /**
   * 打印从 s顶点到 t节点的路径
   * @param prevs
   * @param s
   * @param t
   */
  private void printRoad(int[] prevs, int s, int t) {
    // 从t节点开始,从前开始访问到s
    if (prevs[t] != -1 && s != t) { // 递归,每次都离s节点越近
      printRoad(prevs, s, prevs[t]);
    }
    System.out.print(t + " "); // 递归结束后 打印当前节点
  }

深度优先遍历

通过深度优先遍历来打印从起始顶点到结束顶点的路径. 深度优先遍历就是指从一顶点开始, 再找到下面的一个顶点, 再往下找下面的顶点, 类似于迷宫, 通过一条岔路, 一直往前走,直到走到头, 再从开始的另一个岔道往下走... 遍历的图如下

image.png

通过递归来进行遍历, 有点类似于回溯(因为到达顶点的末尾又会重新从岔道的另一头开始遍历), 其他的注意点和广度优先遍历类似, 使用Visited数组记录是否顶点被访问过, Prevs数组记录当前顶点的前一个节点 再讲一下递归, 遍历到当前节点, 再接着往下遍历和当前节点相连的节点, 一层一层遍历, 就跟上图类似

/**
   * Depth-First-Search 深度优先遍历, 通过递归实现
   * 借助栈
   * @param s
   * @param t
   */
  public void dfs(int s, int t) {
    found = false;
    int[] prevs = new int[v];
    boolean[] visited = new boolean[v];
    Arrays.fill(prevs, -1);
    recurDfs(s, t, prevs, visited);
    printRoad(prevs, s, t);
  }

  /**
   * 递归
   * @param w
   * @param t
   * @param prevs
   * @param visited
   */
  private void recurDfs(int w, int t, int[] prevs, boolean[] visited) {
    if (found == true) return;
    visited[w] = true;
    if (w == t) {
      found = true;
      return;
    }
    for (int i = 0; i < adj[w].size(); ++i) {
      int q = adj[w].get(i);
      if (!visited[q]) {
        prevs[w] = q; // 重点, 设置当前顶点的前一个顶点
        recurDfs(q, t, prevs, visited);
      }
    }
  }

  /**
   * 打印从 s顶点到 t节点的路径
   * @param prevs
   * @param s
   * @param t
   */
  private void printRoad(int[] prevs, int s, int t) {
    // 从t节点开始,从前开始访问到s
    if (prevs[t] != -1 && s != t) { // 递归,每次都离s节点越近
      printRoad(prevs, s, prevs[t]);
    }
    System.out.print(t + " "); // 递归结束后 打印当前节点
  }

总结

广度优先就跟地毯式搜索一样, 一层一层的铺进, 通过队列来实现
深度优先就好比在迷宫找出口一样, 在不知道的情况下, 一直往前, 直到没有路可以走, 在从之前开始的另一个岔路再一直往前走, 有点像不撞南墙不回头一样, 通过栈来实现