图相关的算法

  1. 图相关算法
    1. 图的遍历
      1. 宽度优先遍历BFS
      2. 深度优先遍历DFS
    2. 图的拓扑排序
    3. 图的最小生成树算法
    4. 图的最短路径算法

图相关算法

  1. 图的概念
    1. 由点的集合和边的集合构成
    2. 虽然存在有向图和无向图的概念,
      1. 但实际上都可以用有向图来表达
      2. 无向图可以理解为两个联通点互相指向
    3. 边上可能带有权值
  2. 图的表示方法(连接与否,权重)
    1. 邻接表表示法(代价可封装上去)
      1. 从A点出发能到的直接邻居
    2. 邻接矩阵表示法(全连接)
      1. 列二维表格,表格中填写权重,如果AB不相邻,就是正无穷
    3. 其他方式
    4. 笔试面试常见结构:边的权重,从from节点指向to节点
    5. 图的结构有很多,我们确定一种自己熟悉的图结构,以不变应万变,无论来的什么结构,写一个转换类,任意图结构的描述,都向我们上述的图结构转化:
  3. 大一统图图
    1. 点结构的描述:
    2. 边结构的描述:
    3. 图结构的描述:
  4. 图算法并不难,难点在于图有很多种表示方式,表达一张图的篇幅比较大,coding容易出错。我们的套路就是
    1. 先用自己最熟练的方式,实现图结构的表达
    2. 在自己熟悉的结构上,实现所有常用的图算法作为模板
    3. 把面试题提供的图结构转化为自己熟悉的图结构,再调用模板或改写即可

图的遍历

宽度优先遍历BFS

  1. (Code01_BFS)
    1. 利用队列实现
    2. 从源节点开始依次按照宽度进队列,然后弹出
    3. 每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
    4. 直到队列变空
  2. 宽度优先的思路:实质先遍历自己,再遍历自己的下一跳节点(同一层节点的顺序无需关系),再下下跳节点…
  3. 步骤实现:我们从A点开始遍历:
    1. A进队列–> Q[A];A进入Set–> S[A]
    2. A出队:Q[],打印A;A直接邻居为BCD,都不在Set中,进入队列Q[D,C,B], 进入S[A,B,C,D]
    3. B出队:Q[D,C], B有CE三个邻居,C已经在Set中, 放入E, S[A,B,C,D,E],队列放E, Q[E,D,C]
    4. C出队,周而复始

深度优先遍历DFS

  1. Code02_DFS
    1. 利用栈实现
    2. 从源节点开始把节点按照深度放入栈,然后弹出
    3. 每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
    4. 直到栈变空
  2. 深度优先思路:表示从某个节点一直往下深入,知道没有路了,返回。我们的栈实质记录的是我们深度优先遍历的路径
  3. 步骤实现:我们从A点开始遍历:
    1. A进栈,Stack[A] 打印A。弹出A,当前弹出的节点A去枚举它的后代BCD,B没加入过栈中。压入A再压入B,Stack[B,A]。打印B
    2. 弹出B,B的直接后代邻居为CE,C再栈中而E不在栈中。重新压B,压E,Stack[E,B,A]。打印E
    3. 弹出E,E有邻居D,D不在栈中。压回E,再压D,此时Stack[D,E,B,A]。打印D
    4. 弹出D,D的直接邻居是A,A已经在栈中了。说明A-B-E-D这条路径走到了尽头。弹出D之后,当前循环结束。继续while栈不为空,重复操作

图的拓扑排序

  1. 解释:当前工作需要依赖之前的工作(需要前面工作都完成)
    1. 在图中找到所有入度为0的点输出
    2. 把所有入度为0的点在图中删掉,消除这些点的边的影响。继续找入度为0的点输出,删除,消边,周而复始
    3. 图的所有点都被删除后,依次输出的顺序就是图的拓扑排序
  2. 要求:有向图且其中没有环
  3. 应用:事件安排,编译顺序
  4. 代码
    // 有向无环图,返回拓扑排序的顺序list
    public static List<Node> sortedTopology(Graph graph) {
    
        // key:某一个node
        // value:该节点剩余的入度
        HashMap<Node, Integer> inMap = new HashMap<>();
    
        // 只有剩余入度为0的点,才能进这个队列
        Queue<Node> zeroInQueue = new LinkedList<>();
    
        // 拿到该图中所有的点集
        for (Node node : graph.nodes.values()) {
            // 初始化每个点,每个点的入度是原始节点的入度信息
            // 加入inMap
            inMap.put(node, node.in);
            // 由于是有向无环图,则必定有入度为0的起始点。放入到zeroInQueue
            if (node.in == 0) {
                zeroInQueue.add(node);
            }
        }
    
        // 拓扑排序的结果,依次加入result
        List<Node> result = new ArrayList<>();
    
        while (!zeroInQueue.isEmpty()) {
            // 该有向无环图初始入度为0的点,直接弹出放入结果集中
            Node cur = zeroInQueue.poll();
            result.add(cur);
            // 该节点的下一层邻居节点,更新:入度减1
            for (Node next : cur.nexts) {
                inMap.put(next, inMap.get(next) - 1);
                // 如果下一层存在入度变为0的节点,加入到0入度的队列中
                if (inMap.get(next) == 0) {
                    zeroInQueue.add(next);
                }
            }
        }
        return result;
    }
    

图的最小生成树算法

  1. 最小生成树解释

    1. 就是在不破坏原有图点与点的连通性基础上,让连通的边的整体权值最小。返回最小权值或者边的集合
  2. Kruskal(克鲁斯卡尔)算法:连通性借助并查集实现

    1. 先把所有边,根据权值,由小到大排序,总是从权值最小的边开始考虑,依次考察权值依次变大的边
    2. 当前的边
      1. 要么进入最小生成树的集合:如果当前的边进入最小生成树的集合中不会形成环,就要当前边
      2. 要么丢弃:如果当前的边进入最小生成树的集合中会形成环,就不要当前边
    3. 考察完所有边之后,最小生成树的集合也就得到了
    4. 代码分为两部分,一部分是并查集,一部分是Kruskal
      // K算法
      public static Set<Edge> kruskalMST(Graph graph) {
          // 先拿到并查集结构
          UnionFind unionFind = new UnionFind();
          // 该图的所有点加入到并查集结构
          unionFind.makeSets(graph.nodes.values());
          // 边按照权值从小到大**排序**,加入到小根堆
          PriorityQueue<Edge> priorityQueue =
                  new PriorityQueue<>(new EdgeComparator());
      
          for (Edge edge : graph.edges) { // M 条边
              priorityQueue.add(edge);  // O(logM)
          }
      
          Set<Edge> result = new HashSet<>();
          // 堆不为空,弹出小根堆的堆顶
          while (!priorityQueue.isEmpty()) {
              // 假设M条边,O(logM)
              Edge edge = priorityQueue.poll();
      
              // 如果该边的左右两侧不在同一个集合中
              if (!unionFind.isSameSet(edge.from, edge.to)) { // O(1)
                  // 要这条边
                  result.add(edge);
                  // 联合from和to
                  unionFind.union(edge.from, edge.to);
              }
          }
          return result;
      }
      
  3. Prim算法

    1. P算法无需并查集结构,普通set即可满足
    2. 过程(有两个东西:被解锁的点,被解锁的边)
      1. 任意指定一个出发点,譬如A,A的直接边被解锁
      2. 在A解锁的边里选择一个最小的边,该边两侧有没有新节点,如果有选择该边,没有就舍弃该边
      3. 在被选择的新节点中再解锁该节点的直接边
      4. 周而复始,直到所有点被解锁
    3. 代码
      public static class EdgeComparator implements Comparator<Edge> {
      
          @Override
          public int compare(Edge o1, Edge o2) {
              return o1.weight - o2.weight;
          }
      
      }
      
      public static Set<Edge> primMST(Graph graph) {
          // 解锁的边进入小根堆
          PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
      
          // 哪些点被解锁出来了
          HashSet<Node> nodeSet = new HashSet<>();
          // 已经考虑过的边,不要重复考虑
          Set<Edge> edgeSet = new HashSet<>();
          // 依次挑选的的边在result里
          Set<Edge> result = new HashSet<>();
          // 随便挑了一个点,进入循环处理完后直接break
          for (Node node : graph.nodes.values()) {
      
      
              // node 是开始点
              if (!nodeSet.contains(node)) {
                  // 开始节点保留
                  nodeSet.add(node);
      
                  // 开始节点的所有邻居节点全部放到小根堆
                  // 即由一个点,解锁所有相连的边
                  for (Edge edge : node.edges) {
                      // 没加入到小根堆的边,才会加入(最终,所有边都会加入)
                      if (!edgeSet.contains(edge)) {
                          edgeSet.add(edge);
                          priorityQueue.add(edge);
                      }
                  }
      
                  while (!priorityQueue.isEmpty()) {
                      // 弹出解锁的边中,最小的边
                      Edge edge = priorityQueue.poll();
                      // 可能的一个新的点,from已经被考虑了,只需要看to
                      Node toNode = edge.to;
                      // 如果发现to不在集合中,就是新的点,加入已解锁点集合,并且把边加入结果
                      if (!nodeSet.contains(toNode)) {
                          nodeSet.add(toNode);
                          result.add(edge);
                          for (Edge nextEdge : toNode.edges) {
                              // 没加过的,放入小根堆
                              if (!edgeSet.contains(nextEdge)) {
                                  edgeSet.add(nextEdge);
                                  priorityQueue.add(nextEdge);
                              }
                          }
                      }
                  }
              }
              // 直接break意味着我们不用考虑森林的情况
              // 如果不加break我们可以兼容多个无向图的森林的生成树
              // break;
          }
          return result;
      }
      

图的最短路径算法

  1. Dijkstra(迪杰特斯拉)算法

    1. 求解单元点的最短路径问题:给定带权有向图G和源点v,求v到G中其他顶点的最短路径
    2. 限制条件:图G中不存在负权值的边
  2. 步骤

    1. Dijkstra算法必须指定一个源点
    2. 生成一个源点到各个点的最小距离表,一开始只有一条记录,即原点到自己的最小距离为0,源点到其他所有点的最小距离都是∞正无穷大
    3. 从距离表中拿出没拿过记录里的最小记录,通过这个点出发的边,更新源点到各个点的最小距离表,不断重复这一步
    4. 源点到所有的点记录如果都被拿过一遍,过程停止,最小距离表得到了
  3. 另一种描述:

    1. 不断运行广度优先算法找可见点,计算可见点到源点的距离长度
    2. 从当前已知的路径中选择长度最短的将其顶点加入S作为确定找到的最短路径的顶点。
  4. 代码:未优化:更新表单采用正常找最小

    // 返回的map表就是从from到表中每个key的最小距离
    // 某个点不在map中记录,则from到该点位正无穷
    public static HashMap<Node, Integer> dijkstra1(Node from) {
    
        // 从from出发到所有点的最小距离表
        HashMap<Node, Integer> distanceMap = new HashMap<>();
    
        // from到from距离为0
        distanceMap.put(from, 0);
    
        // 已经求过距离的节点,存在selectedNodes中,以后再也不碰
        HashSet<Node> selectedNodes = new HashSet<>();
    
        // from 0 得到没选择过的点中的最小距离
        Node minNode =
                getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
    
        // 得到minNode之后
        while (minNode != null) {
    
            // 把minNode对应的距离取出,此时minNode就是桥连点 就是 原点---桥连点 的距离
            int distance = distanceMap.get(minNode);
    
            // 把minNode上所有的邻边拿出来
            // 这里就是要拿到例如A到C和A到桥连点B再到C哪个距离小的距离
            for (Edge edge : minNode.edges) {
                // 某条边对应的下一跳节点toNode
                Node toNode = edge.to;
    
                // 如果关于from的distencMap中没有去toNode的记录,表示正无穷,直接添加该条
                if (!distanceMap.containsKey(toNode)) {
                    // 如果没有,直接添加
                    // from到minNode的距离 加上 minNode到当前to节点的边距离
                    distanceMap.put(toNode, distance + edge.weight);
    
                    // 如果有,看该距离是否更小,更小就更新
                } else {
                    // 老的距离,取代,新的距离
                    distanceMap.put(edge.to,
                            Math.min(distanceMap.get(toNode), distance + edge.weight));
                }
            }
            // 锁上minNode,表示from通过minNode到其他节点的最小值已经找到
            // minNode将不再使用
            selectedNodes.add(minNode);
            // 再在没有选择的节点中挑选MinNode当成from的桥接点
            minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
        }
        // 最终distanceMap全部更新,返回
        return distanceMap;
    }
    
    
    // 得到没选择过的点的最小距离
    public static Node getMinDistanceAndUnselectedNode(
            HashMap<Node, Integer> distanceMap,
            HashSet<Node> touchedNodes) {
    
        Node minNode = null;
        int minDistance = Integer.MAX_VALUE;
        // 遍历该表,确保没有被选过,并且距离最小
        for (Entry<Node, Integer> entry : distanceMap.entrySet()) {
            Node node = entry.getKey();
            int distance = entry.getValue();
            // 没有被选择过,且距离最小
            if (!touchedNodes.contains(node) && distance < minDistance) {
                minNode = node;
                minDistance = distance;
            }
        }
        // 抓出来这个点,返回
        return minNode;
    }
    
  5. 更新后的代码:更新表单采用自己写的小根堆

    1. 自定义小根堆
    2. dijkstra过程
// 自定义小根堆结构
// 需要提供add元素的方法,和update元素的方法
// 需要提供ignore方法,表示我们已经找到from到某节点的最短路径
// 再出现from到该节点的其他路径距离,我们直接忽略
public static class NodeHeap {
    private Node[] nodes; // 实际的堆结构
    // key 某一个node, value 上面堆中的位置
    // 如果节点曾经进过堆,现在不在堆上,则node对应-1
    // 用来找需要ignore的节点
    private HashMap<Node, Integer> heapIndexMap;
    // key 某一个节点, value 从源节点出发到该节点的目前最小距离
    private HashMap<Node, Integer> distanceMap;
    private int size; // 堆上有多少个点

    public NodeHeap(int size) {
        nodes = new Node[size];
        heapIndexMap = new HashMap<>();
        distanceMap = new HashMap<>();
        size = 0;
    }

    // 该堆是否空
    public boolean isEmpty() {
        return size == 0;
    }

    // 有一个点叫node,现在发现了一个从源节点出发到达node的距离为distance
    // 判断要不要更新,如果需要的话,就更新
    public void addOrUpdateOrIgnore(Node node, int distance) {
        // 如果该节点在堆上,就看是否需要更新
        if (inHeap(node)) {
            distanceMap.put(node, Math.min(distanceMap.get(node), distance));
            // 该节点进堆,判断是否需要调整
            insertHeapify(node, heapIndexMap.get(node));
        }
        // 如果没有进入过堆。新建,进堆
        if (!isEntered(node)) {
            nodes[size] = node;
            heapIndexMap.put(node, size);
            distanceMap.put(node, distance);
            insertHeapify(node, size++);
        }
        // 如果不在堆上,且进来过堆上,什么也不做,ignore
    }

    // 弹出from到堆顶节点的元素,获取到该元素的最小距离,再调整堆结构
    public NodeRecord pop() {
        NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));
        // 把最后一个元素放在堆顶,进行heapify
        swap(0, size - 1);
        heapIndexMap.put(nodes[size - 1], -1);
        distanceMap.remove(nodes[size - 1]);
        // free C++同学还要把原本堆顶节点析构,对java同学不必
        nodes[size - 1] = null;
        heapify(0, --size);
        return nodeRecord;
    }

    private void insertHeapify(Node node, int index) {
        while (distanceMap.get(nodes[index])
                < distanceMap.get(nodes[(index - 1) / 2])) {
            swap(index, (index - 1) / 2);
            index = (index - 1) / 2;
        }
    }

    private void heapify(int index, int size) {
        int left = index * 2 + 1;
        while (left < size) {
            int smallest = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])
                    ? left + 1
                    : left;
            smallest = distanceMap.get(nodes[smallest])
                    < distanceMap.get(nodes[index]) ? smallest : index;
            if (smallest == index) {
                break;
            }
            swap(smallest, index);
            index = smallest;
            left = index * 2 + 1;
        }
    }

    // 判断node是否进来过堆
    private boolean isEntered(Node node) {
        return heapIndexMap.containsKey(node);
    }

    // 判断某个节点是否在堆上
    private boolean inHeap(Node node) {
        return isEntered(node) && heapIndexMap.get(node) != -1;
    }

    private void swap(int index1, int index2) {
        heapIndexMap.put(nodes[index1], index2);
        heapIndexMap.put(nodes[index2], index1);
        Node tmp = nodes[index1];
        nodes[index1] = nodes[index2];
        nodes[index2] = tmp;
    }
}
// 使用自定义小根堆,改进后的dijkstra算法
// 从from出发,所有from能到达的节点,生成到达每个节点的最小路径记录并返回
public static HashMap<Node, Integer> dijkstra2(Node from, int size) {
    // 申请堆
    NodeHeap nodeHeap = new NodeHeap(size);
    // 在堆上添加from节点到from节点距离为0
    nodeHeap.addOrUpdateOrIgnore(from, 0);
    // 最终的结果集
    HashMap<Node, Integer> result = new HashMap<>();
    while (!nodeHeap.isEmpty()) {
        // 每次在小根堆弹出堆顶元素
        NodeRecord record = nodeHeap.pop();
        // 拿出的节点
        Node cur = record.node;
        // from到该节点的距离
        int distance = record.distance;
        // 以此为桥接点,找是否有更小的距离到该节点的其他to节点
        // addOrUpdateOrIgnore该方法保证
        // 如果from到to的节点没有,就add
        // 如果有,看是否需要Ignore,如果不需要Ignore且更小,就Update
        for (Edge edge : cur.edges) {
            nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
        }
        result.put(cur, distance);
    }
    return result;
}

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,可以邮件至 963614756@qq.com。