图的结构

对于图来说,主要有邻接矩阵邻接表两种表示方法,在这里定义邻接表结构

  1. //图
  2. public class Graph{
  3. //图中所有节点的集合
  4. public HashMap<Integer,Node> nodes;
  5. //图中所有边的集合
  6. public HashSet<Edge> edges;
  7. public Graph() {
  8. this.nodes = new HashMap<>();
  9. this.edges = new HashSet<>();
  10. }
  11. }
  12. //节点
  13. public Node {
  14. //节点的值
  15. public int value ;
  16. //节点的入度
  17. public int in;
  18. //节点的出度
  19. public int out;
  20. //节点的所有指向节点(出度对应节点)
  21. public ArrayList<Node> nexts;
  22. //节点的所有出边
  23. public ArrayList<Edge> edges;
  24. public Node(int value) {
  25. this.value = value;
  26. this.in = 0 ;
  27. this.out = 0 ;
  28. this.nexts = new ArrayList<>();
  29. this.edges = new ArrayList<>();
  30. }
  31. }
  32. //边
  33. public Edge {
  34. //边的权值
  35. public int weight;
  36. //边的from节点
  37. public Node from;
  38. //边的to节点
  39. public Node to;
  40. public Edge(int weight,Node from,Node to) {
  41. this.weight = weight;
  42. this.from = from;
  43. this.to = to;
  44. }
  45. }

图的宽度优先遍历BFS

图的宽度优先遍历就是从一个节点开始,逐步遍历这个节点的所有邻接节点,然后再去遍历这些邻接节点的邻接节点
image.png

  1. //从node节点开始进行宽度优先遍历
  2. public void bfs(Node node) {
  3. if (node == null) {
  4. return ;
  5. }
  6. Queue<Node> queue = new LinkedList<>();
  7. //确保不会重复遍历
  8. Set<Node> set = new HashSet<>();
  9. queue.offer(node);
  10. set.add(node);
  11. while (!queue.isEmpty()) {
  12. Node current = queue.poll();
  13. System.out.println(current.val);
  14. //加入这个节点的邻接节点
  15. for (Node node1 : current.nexts) {
  16. if (!set.contains(node1)) {
  17. queue.offer(node1);
  18. set.add(node1);
  19. }
  20. }
  21. }
  22. }

图的深度优先遍历
image.png

  1. public void dfs(Node node) {
  2. //用来进行dfs的栈
  3. Stack<Node> stack = new Stack<>();
  4. Set<Node> set = new HashSet<>();
  5. stack.push(node);
  6. set.add(node);
  7. System.out.println(node.value);
  8. while (!stack.isEmpty()) {
  9. //得到当前stack的栈顶元素
  10. Node current = stack.pop();
  11. for (Node next : current.nexts) {
  12. if (!set.contains(next)) {
  13. //有了邻接节点之后就要从这个邻接节点开始遍历了
  14. //但是还需要将当前的current节点入栈,防止next无法继续遍历了
  15. //就回来继续从current遍历
  16. stack.push(current);
  17. stack.push(next);
  18. set.add(next);
  19. System.out.println(next.value);
  20. //有邻接节点了,就不用继续遍历current了,直接break
  21. break;
  22. }
  23. }
  24. }
  25. }

拓扑排序

与拓扑排序相关的问题是:假设有许多任务,一些任务有着前置任务,只有完成了前置任务才能完成当前任务,如何得到最终处理任务的顺序
拓扑排序的思路是:先从入度为0的节点开始执行, 执行了之后更新其他节点的入度,然后重复执行这个过程。
image.png

  1. public void topological(Graph graph) {
  2. //存储所有节点的入度
  3. Map<Node,Integer> map = new HashMap<Node,Integer>();
  4. //存储0入度节点
  5. Queue<Node> zeroInQueue = new LinkedList<>();
  6. for (Integer integer : graph.nodes.keySet()) {
  7. map.put(graph.nodes.get(integer),graph.nodes.get(integer).in);
  8. if (graph.nodes.get(integer).in == 0) {
  9. zeroInQueue.offer(graph.nodes.get(integer));
  10. }
  11. }
  12. //不断访问入度为0的节点并更新其他节点入度
  13. while (!zeroInQueue.isEmpty()) {
  14. Node node = zeroInQueue.poll();
  15. System.out.println(node.value);
  16. for (Node next : node.nexts) {
  17. map.put(next,map.get(next)-1);
  18. if (map.get(next) == 0) {
  19. zeroInQueue.offer(next);
  20. }
  21. }
  22. }
  23. }

最小生成树算法

最小生成树就是在整个图联通的基础上,能到达最小边权重和的图结构,最小生成树的算法使用到了贪心思想
image.png

克鲁斯卡尔算法

作为贪心算法核心,每次观察当前最小边两侧的节点集合是否是同一个集合,如果是一个节点集合说明产生了环路,这条边不能用,就去找下一条最小边
克鲁斯卡尔算法的重点在于定义集合结构用来表示节点集
image.png

  1. class MeSets {
  2. //一开始应该给每个点都分配一个集合,这个集合中只有一个点
  3. public HashMap<Node,Set<Node>> setMap;
  4. public MySets(List<Node> nodes) {
  5. //初始化节点集合
  6. for (Node node : nodes) {
  7. setMap.put(node,new HashSet<>());
  8. //把自己加入到自己的集合中
  9. setMap.get(node).add(node);
  10. }
  11. }
  12. //判断两个节点所属的集合是否是一个集合
  13. public boolean isSameSet(Node from,Node to) {
  14. Set<Node> fromSet = setMap.get(from);
  15. Set<Node> toSet = setMap.get(to);
  16. return fromSet == toSet;
  17. }
  18. //合并两个节点集合
  19. public void union(Node from,Node to) {
  20. Set<Node> fromSet = setMap.get(from);
  21. Set<Node> toSet = setMap.get(to);
  22. //把toSet中的所有节点加入到fromSet,并更新setMap
  23. for (Node node : toSet) {
  24. fromSet.add(node);
  25. setMap.put(node,fromSet);
  26. }
  27. }
  28. }
  1. public static Set<Edge> kruskal(Graph graph) {
  2. //结果集合
  3. Set<Edge> result = new HashSet<>();
  4. //初始化集合结构
  5. MySets mySets = new MySets(new ArrayList<>(graph.nodes.values()));
  6. //建立边的小顶堆来存储最小的边
  7. PriorityQueue<Edge> queue = new PriorityQueue<>(new Comparator<Edge>(){
  8. @Override
  9. public int compare(Edge o1,Edge o2) {
  10. return o1.weight - o2.weight;
  11. }
  12. });
  13. //把图中所有的节点加入到queue
  14. for (Edge edge : graph.edges) {
  15. queue.offer(edge);
  16. }
  17. while (!queue.isEmpty()) {
  18. //得到当前最小的边
  19. Edge edge = queue.poll();
  20. //如果这条边的to和from不是一个集合就合并
  21. if (!mySets.isSameSet(edge.from,edge.to)) {
  22. result.add(edge);
  23. mySets.union(edge.from,edge.to);
  24. }
  25. }
  26. return result;
  27. }

普里姆算法Prim

普里姆算法是以节点为贪心核心的最小生成树算法:定义一个节点集合,从某个节点开始,找到距离这个节点最近的非集合内节点,将节点加入集合,重复过程。
image.png

//返回List<Set<Edge>> 是为了应对可能出现的不同森林状况,即图内可能有多个连通图
public List<Set<Edge>> prim(Graph graph) {
    //Set结合,来避免节点重复计算
    Set<Node> set = new HashSet<>();
    //结果集
    List<Set<Edge>> result = new ArrayList<>(); 
    //这一层循环是森林层面的
    for (Node node : graph.nodes.values()) {
        Set<Edge> edgeSet = new HashSet<>();
        //如果这个节点还没有被遍历过,就进入(一个新的森林)
        if (!set.contains(node)) {
            set.add(node);
            //小顶堆,存储当前最小的边
            PriorityQueue<Edge> queue = new PriorityQueue<>(new Comparator<Edge>(){
                @Override
                public int compare(Edge o1,Edge o2) {
                    return o1.weight - o2.weight; 
                }
            }); 
            //把当前节点所有的邻接边加入队列
            for (Edge edge : node.edges) {
                queue.offer(edge); 
            }
            while (!queue.isEmpty()) {
                //当前最小的边
                Edge edge = queue.poll();
                Node to = edge.to;
                //如果通向新的节点
                if (!set.contains(to)) {
                    edgeSet.add(edge);
                    set.add(to); 
                    //加入新节点的邻接边
                    for (Edge edge : to.edges) {
                        queue.offer(edge); 
                    } 
                }
            }
        }
        list.add(edgeSet);
    }
    return list; 
}

单源最短路径问题

单源最短路径问题 :求某个节点到达图中其他所有节点的最短距离

迪杰斯特拉算法

迪杰斯特拉算法用到了动态规划算法,每次找到一个新的点的时候,就更新整个**distanceMap**的距离 ,看看利用这个点作为中转,到达其他点的距离能否进一步缩小,当图中所有的节点都被遍历过之后,整个图的单源最短路径就求出来了。
image.png

//计算node到其他节点的最短距离
public Map<Node,Integer> dijkstra(Node node) {
    //记录节点node到其他节点距离
    Map<Node,Integer> distanceMap = new HashMap<>();
    //自己到自己的距离是0 
    distanceMap.put(node,0); 
    //已经被遍历过的节点
    HashSet<Node> selectedNodes = new HashSet<>(); 
    //找到不在selectedNodes中并且距离当前节点最近的节点
    Node minNode = getMinDistanceNodeAndNotSelected(distanceMap,selectedNodes);
    while (minNode != null) {
        //当前的距离
        int currentDistance = distanceMap.get(minNode); 
        //通过距离来更新整个图的distance
        Set<Edge> edges = minNode.edges;
        for (Edge edge : edges) {
            Node to = edge.to;
            //找到了一个新的节点(之前不连通,距离为+oo)
            if (!distanceMap.containsKey(to)) {
                distanceMap.put(to,currentDistance+edge.weight);
            }
            //如果通过这个点计算,有的点有了更短的距离,就更新
            distanceMap.put(to,Math.min(currentDistance+edge.weight,distanceMap.get(to)));
        }
        selectedNodes.add(minNode);
        //继续寻找下一个节点
        minNode = getMinDistanceNodeAndNotSelected(distanceMap,selectedNodes);
    }
    return distanceMap; 
}
//找到不在selectedSet中并且距离当前节点最近的节点
public Node getMinDistanceNodeAndNotSelected(Map<Node,Integer> distanceMap,Set selectedSet) {
    Node ans = null;
    int min = Integer.MAX_VALUE;
    Set<Node> keySet = distanceMap.keySet();
    for (Node node : keySet) {
        if (!selectedSet.contains(node) && distanceMap.get(node) < min) {
            min = distanceMap.get(node);
            ans = node; 
        }
    }
    return node; 
}