11.图相关算法

简介: 11.图相关算法

 

  • 1)由点的集合和边的集合构成
  • 2)虽然存在有向图和无向图的概念,但实际上都可以用有向图来表达
  • 3)边上可能带有权值

图结构的表达

  • 1)邻接表法
  • 2)邻接矩阵法
  • 3)除此之外还有其他众多的方式

图的面试题如何搞定

  • 1图的算法都不算难,只不过coding 的代价比较高
  • 2)先用自己最熟练的方式,实现图结构的表达
  • 3)在自己熟悉的结构上,实现所有常用的图算法作为模板
  • 4)把面试题提供的图结构转化为自己熟悉的图结构,再调用模板或改写即可

用代码实现图(点–>边–>图)

package com.harrison.class11;
import java.util.ArrayList;
// 点结构的描述
public class Node {
  public int value;// 编号
  public int in;// 入度 进到我这个点的边的数量
  public int out;// 出度 从我这个点出去的边的数量
  public ArrayList<Node> nexts;// 直接邻居 只算出度
  public ArrayList<Edge> edges;// 边 从我这个点出发有几条边组成的边的数据结构 放在edges里面
  public  Node(int value) {
    this.value=value;
    in=0;
    out=0;
    nexts=new ArrayList<>();
    edges=new ArrayList<>();
  }
}
package com.harrison.class11;
public class Edge {
  public int weight;
  public Node from;
  public Node to;
  public Edge(int weight, Node from, Node to) {
    this.weight = weight;
    this.from = from;
    this.to = to;
  }
}
package com.harrison.class11;
import java.util.HashMap;
import java.util.HashSet;
public class Graph {
  public HashMap<Integer,Node> nodes;// key是编号,value是实现的点
  public HashSet<Edge> edges;
  public Graph() {
    nodes=new HashMap<>();
    edges=new HashSet<>();
  }
}
package com.harrison.class11;
public class GrahpGenerator {
  // matrix 所有的边
  // N*3 的矩阵
  // [weight, from节点上面的值,to节点上面的值]
  // [ 5 , 0 , 7]
  // [ 3 , 0, 1]
  public static Graph createGraph(int[][] matrix) {
    Graph graph=new Graph();
    for(int i=0; i<matrix.length; i++) {
      //拿到每一条边, matrix[i]
      int weight=matrix[i][0];
      int from=matrix[i][1];
      int to=matrix[i][2];
      if(!graph.nodes.containsKey(from)) {
        graph.nodes.put(from, new Node(from));
      }
      if(!graph.nodes.containsKey(to)) {
        graph.nodes.put(to, new Node(to));
      }
      Node fromNode=graph.nodes.get(from);
      Node toNode=graph.nodes.get(to);
      Edge newEdge=new Edge(weight,fromNode,toNode);
      fromNode.nexts.add(toNode);
        fromNode.out++;
        toNode.in++;
        fromNode.edges.add(newEdge);
        graph.edges.add(newEdge);
    }
    return graph;
  }
}

图的宽度优先&深度优先遍历

宽度优先遍历
  • 1)利用队列实现
  • 2)从源节点开始依次按照宽度进队列,然后弹出
  • 3)每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
  • 4)直到队列变空
package com.harrison.class11;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
public class Code01_BFS {
  // 从node出发,进行宽度优先遍历,只需要用到点结构
  public static void bfs(Node node) {
    if(node==null) {
      return;
    }
    Queue<Node> queue=new LinkedList<>();
    // 在二叉树进行宽度优先遍历时不会形成环
    // 因为图会形成环,所有必须有个set来确保每个结点只进一次队列
    HashSet<Node> set=new HashSet<>();
    queue.add(node);
    set.add(node);
    while(!queue.isEmpty()) {
      Node cur=queue.poll();
      System.out.println(cur.value);
      for(Node next: cur.nexts) {
        if(!set.contains(next)) {
          set.add(next);
          queue.add(next);
        }
      }
    }
  }
}

深度优先遍历

  • 1)利用栈实现
  • 2)从源节点开始把节点按照深度放入栈,然后弹出
  • 3)每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
  • 4)直到栈变空
package com.harrison.class11;
import java.util.HashSet;
import java.util.Stack;
public class Code02_DFS {
  public static void dfs(Node node) {
    if(node==null) {
      return;
    }
    //栈的作用:从栈底到栈顶记录的是深度优先遍历的路径
    Stack<Node> stack=new Stack<>();
    HashSet<Node> set=new HashSet<>();
    stack.add(node);
    set.add(node);
    System.out.println(node.value);
    while(!stack.isEmpty()) {
      Node cur=stack.pop();
      for(Node next: cur.nexts) {
        if(!set.contains(next)) {
          stack.add(cur);
          stack.add(next);
          set.add(next);
          System.out.println(next.value);
          break;
        }
      }
    }
  }
}

图的拓扑排序算法

一定是有向无环图

  • 1)在图中找到所有入度为0的点输出
  • 2)把所有入度为0的点在图中删掉,继续找入度为0的点输出,周而复始
  • 3)图的所有点都被删除后,依次输出的顺序就是拓扑排序
  • 要求:有向图且其中没有环
  • 应用:事件安排、编译顺序
package com.harrison.class11;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
public class Code03_TopologySort {
  // directed graph and no loop
  public static List<Node> sortedTopology(Graph graph){
    // key 某个点  value 剩余的入度
    HashMap<Node,Integer> inMap=new HashMap<>();
    //只有剩余入度为0的点,才能进入该队列
    Queue<Node> zeroInQueue=new LinkedList<>();
    for(Node node: graph.nodes.values()) {
      inMap.put(node, node.in);
      if(node.in==0) {
        zeroInQueue.add(node);
      }
    }
    List<Node> result=new LinkedList<>();
    while(!zeroInQueue.isEmpty()) {
      Node cur=zeroInQueue.poll();
      result.add(cur);
      for(Node next:cur.nexts) {
        inMap.put(next, inMap.get(next)-1);
        if(inMap.get(next)==0) {
          zeroInQueue.add(next);
        }
      }
    }
    return result;
  }
}

最小生成树算法

Kruskal

不能破环连通性

  • 1)总是从权值最小的边开始考虑,依次考察权值依次变大的边
  • 2)当前的边要么进入最小生成树的集合,要么丢弃
  • 3)如果当前的边进入最小生成树的集合中不会形成环,就要当前边,
  • 4)如果当前的边进入最小生成树的集合中会形成环,就不要当前边
  • 5)考察完所有边之后,最小生成树的集合也得到了
package com.harrison.class11;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.Stack;
public class Code04_Kruskal {
  // Union-Find set 并查集
  public static class UnionFind{
    // key 某一个结点  value key结点往上的结点
    public HashMap<Node, Node> fatherMap;
    // key 某一个集合的代表结点 value key所在集合的结点个数
    public HashMap<Node, Integer> sizeMap;
    public UnionFind() {
      fatherMap = new HashMap<Node,Node>();
      sizeMap = new HashMap<Node,Integer>();
    }
    public void makeSets(Collection<Node> nodes) {
      fatherMap.clear();
      sizeMap.clear();
      for(Node node:nodes) {
        fatherMap.put(node,node);
        sizeMap.put(node,1);
      }
    }
    public Node findFather(Node n) {
      Stack<Node> path = new Stack<>();
      while (n != fatherMap.get(n)) {
        path.push(n);
        n = fatherMap.get(n);
      }
      while (!path.isEmpty()) {
        fatherMap.put(path.pop(), n);
      }
      return n;
    }
    public boolean isSameSet(Node a,Node b) {
      return findFather(a) == findFather(b);
    }
    public void union(Node a,Node b) {
      Node aDai = findFather(a);
      Node bDai = findFather(b);
      if (aDai != bDai) {
        int aSetSize = sizeMap.get(aDai);
        int bSetSize = sizeMap.get(bDai);
        Node big = aSetSize >= bSetSize ? aDai : bDai;
        Node small = big == aDai ? bDai : aDai;
        fatherMap.put(small, big);
        sizeMap.put(big, aSetSize + bSetSize);
        sizeMap.remove(small);
      }
    }
  }
  // 将所有的边按 边的权值大小 从小到大排序
  public static class EdgeComparator implements Comparator<Edge>{
    @Override
    public int compare(Edge o1, Edge o2) {
      // TODO Auto-generated method stub
      return o1.weight-o2.weight;
    }
  }
  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条边
      Edge edge=priorityQueue.poll(); // O(logM)
      if(!unionFind.isSameSet(edge.from, edge.to)) { //O(1)
        result.add(edge);
        unionFind.union(edge.from, edge.to);
      }
    }
    return result;
  }
}
Prim
  • 1)随便选一个结点解锁,相邻的边也全部解锁,然后选一条权值最小的边
  • 2)新解锁的边两侧有新结点,则把新节点给解锁,并把新解锁的边考虑在最小生成树里面;新解锁的边两侧没有新结点,则不将新解锁的边考虑在最小生成树里面
  • 3)新解锁的结点所有相邻的边被解锁,已经被考虑在最小生成树里的边不重复解锁,然后在所有相邻的边里选择一条权值最小的边,重复步骤 2)周而复始,一直到把所有的点都解锁完
package com.harrison.class11;
import java.util.Comparator;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
// undirected graph only
public class Code05_Prim {
  public static class EdgeComparator implements Comparator<Edge>{
    @Override
    public int compare(Edge o1, Edge o2) {
      // TODO Auto-generated method stub
      return o1.weight-o2.weight;
    }
  }
  public static Set<Edge> primMST(Graph graph){
    // 被解锁的边进入小根堆
    PriorityQueue<Edge> priorityQueue=new PriorityQueue<>(new EdgeComparator());
    // 哪些点被解锁出来了
    HashSet<Node> nodeSet=new HashSet<>();
    // 已经考虑的边不要重复考虑
    HashSet<Edge> edgeSet=new HashSet<>();
    // 依次挑选的边放在result里面
    Set<Edge> result=new HashSet<>();
    // 随便挑选一个点 for循环只是为了防止森林,但一般不会出现无向图的森林
    // 所以for循环也可以不要
    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();
          // 新解锁的边两侧可能有新结点(也就是说可能的一个新的点)
          Node toNode=edge.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;
    }
    return result;
  } 
}

Dijkstra

给你一个出发点,求出发点到所有结点的最短距离是多少?如果无法到达某个点,则到这个点的距离是正无穷(一般出现在有向图里面)。

不能有权值为负数的边!!!

package com.harrison.class11;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
// no negative weight
public class Code06_Dijkstra {
  public static HashMap<Node, Integer> dijkstra1(Node from) {
    /**
     * from 从from点出发到所有点的最小距离 key:从from出发到达key value:从from出发到达key的最小距离
     * 如果在表中,没有T的记录,则代表从from点出发到达T这个点的距离为正无穷
     */
    HashMap<Node, Integer> distanceMap = new HashMap<>();
    distanceMap.put(from, 0);
    // 已经求过距离的结点,存在selectedNodes中,以后再也不碰
    HashSet<Node> selectedNodes = new HashSet<>();
    Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
    while (minNode != null) {
      int distance = distanceMap.get(minNode);
      for (Edge edge : minNode.edges) {
        Node toNode = edge.to;
        if (!distanceMap.containsKey(toNode)) {
          distanceMap.put(toNode, distance + edge.weight);
        } else {
          distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));
        }
      }
      selectedNodes.add(minNode);
      minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
    }
    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;
  }
  public static class NodeRecord {
    public Node node;
    public int distance;
    public NodeRecord(Node node, int distance) {
      this.node = node;
      this.distance = distance;
    }
  }
  public static class NodeHeap {
    private Node[] nodes; // 实际的堆结构
    // key 某一个node, value 上面堆中的位置
    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++);
      }
    }
    public NodeRecord pop() {
      NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));
      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;
      }
    }
    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算法
  // 从head出发,所有head能到达的节点,生成到达每个节点的最小路径记录并返回
  public static HashMap<Node, Integer> dijkstra2(Node head, int size) {
    NodeHeap nodeHeap = new NodeHeap(size);
    nodeHeap.addOrUpdateOrIgnore(head, 0);
    HashMap<Node, Integer> result = new HashMap<>();
    while (!nodeHeap.isEmpty()) {
      NodeRecord record = nodeHeap.pop();
      Node cur = record.node;
      int distance = record.distance;
      for (Edge edge : cur.edges) {
        nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);
      }
      result.put(cur, distance);
    }
    return result;
  }
}


相关文章
|
7月前
|
算法 搜索推荐 图计算
图计算中的社区发现算法是什么?请解释其作用和常用算法。
图计算中的社区发现算法是什么?请解释其作用和常用算法。
149 0
|
7月前
|
存储 算法 测试技术
☆打卡算法☆LeetCode 133. 克隆图 算法解析
☆打卡算法☆LeetCode 133. 克隆图 算法解析
|
7月前
|
存储 人工智能 算法
图与树的遍历:探索广度优先、深度优先及其他遍历算法的原理与实现
图与树的遍历:探索广度优先、深度优先及其他遍历算法的原理与实现
457 0
|
6月前
|
存储 算法 Java
Java中,树与图的算法涉及二叉树的前序、中序、后序遍历以及DFS和BFS搜索。
【6月更文挑战第21天】Java中,树与图的算法涉及二叉树的前序、中序、后序遍历以及DFS和BFS搜索。二叉树遍历通过访问根、左、右子节点实现。DFS采用递归遍历图的节点,而BFS利用队列按层次访问。以下是简化的代码片段:[Java代码略]
51 4
|
5月前
|
存储 算法 Python
“解锁Python高级数据结构新姿势:图的表示与遍历,让你的算法思维跃升新高度
【7月更文挑战第13天】Python中的图数据结构用于表示复杂关系,通过节点和边连接。常见的表示方法是邻接矩阵(适合稠密图)和邻接表(适合稀疏图)。图遍历包括DFS(深度优先搜索)和BFS(广度优先搜索):DFS深入探索分支,BFS逐层访问邻居。掌握这些技巧对优化算法和解决实际问题至关重要。**
52 1
|
5月前
|
数据采集 存储 算法
「AIGC算法」图搜索算法详解
本文探讨了图搜索算法,包括遍历和最短路径搜索。DFS和BFS是遍历算法,前者使用栈深入搜索,后者用队列逐层遍历。Dijkstra、Bellman-Ford、A*、Floyd-Warshall和Johnson算法则解决最短路径问题。文中还给出了DFS的Python实现示例。这些算法在路径规划、网络分析等领域有重要应用。
157 0
|
7月前
|
存储 算法 C++
c++算法学习笔记 (8) 树与图部分
c++算法学习笔记 (8) 树与图部分
|
7月前
|
算法 数据可视化 大数据
圆堆图circle packing算法可视化分析电商平台网红零食销量采集数据
圆堆图circle packing算法可视化分析电商平台网红零食销量采集数据
|
6月前
|
算法 计算机视觉
图像处理之基于图的广度优先搜索组件标记算法
图像处理之基于图的广度优先搜索组件标记算法
39 0
|
6月前
|
存储 算法
数据结构学习记录——图应用实例-六度空间(题目描述、算法思路、伪代码及解读、图解)
数据结构学习记录——图应用实例-六度空间(题目描述、算法思路、伪代码及解读、图解)
72 0