一、如何表达图,生成图?
package FaceQuestion.图; import java.util.ArrayList; /** * 图的节点类 */ public class Node { public int value;//图节点的值 public int in;//入度 public int out;//出度 public ArrayList<Node> nexts;//邻接节点的集合 public ArrayList<Edge> edges;//当前节点连接边的集合 public Node(int value){ this.value=value; in=0; out=0; nexts=new ArrayList<>(); edges=new ArrayList<>(); } }
package FaceQuestion.图; /** * 图的边类 */ 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 FaceQuestion.图; import java.util.HashMap; import java.util.HashSet; /** * 图的实体类 */ public class Graph { public HashMap<Integer,Node> nodes;//图的所有节点集合 public HashSet<Edge> edges;//图的所有边的集合 public Graph(){ nodes=new HashMap<>(); edges=new HashSet<>(); } }
package FaceQuestion.图; /** * 图的生成器 */ public class GraphGenerator { /** * 图的生成方法 * @param matrix 表示一条边的二维数组 * @return 一张图 */ public static Graph createGraph(Integer[][] matrix){ Graph graph=new Graph(); for (int i=0;i<matrix.length;i++){ Integer from=matrix[i][0];//边的入节点的key值 Integer to=matrix[i][1];//边的出节点的key值 Integer weight=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 FaceQuestion.图; import java.util.HashSet; import java.util.LinkedList; import java.util.Queue; /** * 图的宽度优先搜索算法 * 其邻居节点尚未被检验过的节点会被放置在一个被称为 open 的容器中(例如队列或是链表), * 而被检验过的节点则被放置在被称为 closed 的容器中。(open-closed表) * 1,利用队列实现 * 2,从源节点开始依次按照宽度进队列,然后弹出 * 3,每弹出一个点,把该节点所有没有进过队列的邻接点放入队列 * 4,直到队列变空 */ public class BFS { /** * 宽度优先搜索 * @param node 当前节点 */ public void bfs(Node node){ if (node==null){ return; } Queue<Node> queue=new LinkedList<>();//未被检验过的节点集合 HashSet<Node> map=new HashSet<>();//已经被检验过的节点集合 queue.add(node); map.add(node); while (!queue.isEmpty()){ Node cur=queue.poll(); System.out.println(cur.value); for (Node next:cur.nexts){ //循环遍历当前节点的邻居节点 if (!map.contains(next)){ //如果没有被遍历过 map.add(next); queue.add(next); } } } } }
三、深度优先搜索算法
1,利用栈实现
2,从源节点开始把节点按照深度放入栈,然后弹出
3,每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
4,直到栈变空
package FaceQuestion.图; import java.util.HashSet; import java.util.Stack; /** * 图的深度优先搜索算法 * 1,利用栈实现 * 2,从源节点开始把节点按照深度放入栈,然后弹出 * 3,每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈 * 4,直到栈变空 */ public class 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.push(cur); stack.push(next); set.add(next); System.out.println(next.value); break; } } } } }
四、拓扑排序算法
适用范围:要求有向图,且有入度为0的节点,且没有环
例子:比如有ABCDE五件事情需要去完成,A事件的完成必须依赖B和C事件的完成,而B事件要完成的话D事件必须先完成,C事件要完成那么E事件必须先完成,所以完成此事件链的排序有EDCBA或者DECBA等方法。这就是拓扑排序算法;
package FaceQuestion.图; import java.util.*; /** * 图的拓扑排序算法 * 1.遍历所有节点,找到所有入度为0 的节点 * 2.将所有入度为0的节点的邻居节点的入度减1,那么就会产生新的入度为0的节点 */ public class TopologySort { /** * 拓扑排序算法 * @param graph * @return */ public List<Node> sortedTopology(Graph graph){ HashMap<Node,Integer> inMap=new HashMap<>();//所有节点的入度哈希表 Queue<Node> zeroInQueue=new LinkedList<>();//所有入度为0 节点的哈希表 for (Node node:graph.nodes.values()){ inMap.put(node,node.in);//将所有节点的入度存储 if (node.in==0){ zeroInQueue.add(node);//将所有入度为0的节点存储 } } List<Node> result=new ArrayList<>();//结果集 while (!zeroInQueue.isEmpty()){ //遍历所有入度为0 的节点的邻居节点,并将其入度减1 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; } }
五、最小生成树算法
适用范围:无向图
在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
在说这个之前,需要先谈一下并查集这个概念:什么是并查集?
在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中。这一类问题近几年来反复出现在信息学的国际国内赛题中,其特点是看似并不复杂,但数据量极大,若用正常的数据结构来描述的话,往往在空间上过大,计算机无法承受;即使在空间上勉强通过,运行的时间复杂度也极高,根本就不可能在比赛规定的运行时间(1~3秒)内计算出试题需要的结果,只能用并查集来描述。
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。
按照我的理解,并查集一开始将所有数据单独作为一个集合,它提供两种操作,第一个是判断两个数是否是同一个集合,第二个是合并两个集合;
并查集算法:
package FaceQuestion.二叉树; import java.util.HashMap; import java.util.List; /** * 并查集 */ public class UnionFind { public static class Node { // whatever you like } public static class DisjointSets { //存储的是一个节点和它的父节点的对应关系 public HashMap<Node, Node> fatherMap; //存储的是一个节点如过是一个集合的特征节点,那么代表的就是特征节点和集合大小的对应关系 public HashMap<Node, Integer> rankMap; public DisjointSets() { fatherMap = new HashMap<Node, Node>(); rankMap = new HashMap<Node, Integer>(); } /** * 初始化所有元素,使得每一个元素各成一个集合 * @param nodes */ public void makeSets(List<Node> nodes) { fatherMap.clear(); rankMap.clear(); for (Node node : nodes) { fatherMap.put(node, node); rankMap.put(node, 1); } } /** * 找到一个集合的特征节点 * @param n * @return */ public Node findFather(Node n) { Node father = fatherMap.get(n); if (father != n) { father = findFather(father); } fatherMap.put(n, father); return father; } /** * 合并集合的操作 * @param a * @param b */ public void union(Node a, Node b) { if (a == null || b == null) { return; } Node aFather = findFather(a); Node bFather = findFather(b); if (aFather != bFather) { int aFrank = rankMap.get(aFather); int bFrank = rankMap.get(bFather); if (aFrank <= bFrank) { fatherMap.put(aFather, bFather); rankMap.put(bFather, aFrank + bFrank); } else { fatherMap.put(bFather, aFather); rankMap.put(aFather, aFrank + bFrank); } } } } public static void main(String[] args) { } }
有了并查集,我们就可以很容易的实现Kruskal算法了
package FaceQuestion.图.最小生成树算法; import FaceQuestion.二叉树.UnionFind; import FaceQuestion.图.Edge; import FaceQuestion.图.Graph; import FaceQuestion.图.Node; import java.util.*; /** * 最小生成树Kruskal算法 * 遍历所有边,依次选择最小权重的边,判断是否存在回路,如果不存在,那么就选择 * 使用并查集判断是否存在回路问题,边的入节点和出节点是否在一个集合中 */ public class Kruskal { // Union-Find Set public static class UnionFind { private HashMap<Node, Node> fatherMap; private HashMap<Node, Integer> rankMap; public UnionFind() { fatherMap = new HashMap<Node, Node>(); rankMap = new HashMap<Node, Integer>(); } private Node findFather(Node n) { Node father = fatherMap.get(n); if (father != n) { father = findFather(father); } fatherMap.put(n, father); return father; } public void makeSets(Collection<Node> nodes) { fatherMap.clear(); rankMap.clear(); for (Node node : nodes) { fatherMap.put(node, node); rankMap.put(node, 1); } } public boolean isSameSet(Node a, Node b) { return findFather(a) == findFather(b); } public void union(Node a, Node b) { if (a == null || b == null) { return; } Node aFather = findFather(a); Node bFather = findFather(b); if (aFather != bFather) { int aFrank = rankMap.get(aFather); int bFrank = rankMap.get(bFather); if (aFrank <= bFrank) { fatherMap.put(aFather, bFather); rankMap.put(bFather, aFrank + bFrank); } else { fatherMap.put(bFather, aFather); rankMap.put(aFather, aFrank + bFrank); } } } } public static class EdgeComparator implements Comparator<Edge> { @Override public int compare(Edge o1, Edge o2) { return o1.weight - o2.weight; } } public 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){ priorityQueue.add(edge); } Set<Edge> result=new HashSet<>(); while (!priorityQueue.isEmpty()){ Edge edge=priorityQueue.poll(); if (!unionFind.isSameSet(edge.from,edge.to)){ //如果边的入节点和出节点不是同一个集合的,那么就意味着不是一个环 //那么就可一选择这条边作为最小生成树的一个边 result.add(edge); unionFind.union(edge.from,edge.to); } } return result; } }
最小生成树还有一种算法,Prim算法,他是以节点作为观察的,从一个节点开始,选择与他连接边中的权重最小的边作为连接边;
package FaceQuestion.图.最小生成树算法; import FaceQuestion.图.Edge; import FaceQuestion.图.Graph; import FaceQuestion.图.Node; import java.util.Comparator; import java.util.HashSet; import java.util.PriorityQueue; import java.util.Set; /** * prim算法 */ public class Prim { 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> set=new HashSet<>(); Set<Edge> result=new HashSet<>(); for (Node node:graph.nodes.values()){ if (!set.contains(node)){ set.add(node); for (Edge edge:node.edges){ //将node所有边加入小根堆 priorityQueue.add(edge); } while (!priorityQueue.isEmpty()){ //从当前节点的所有边中选择权重最小的一条边 Edge edge=priorityQueue.poll(); Node toNode=edge.to; //考察最小权重边的出节点是否已经存在与集合中 //如果不存在,那么就加入到集合中 //如果已经存在,那么忽略 if (!set.contains(toNode)){ set.add(toNode); result.add(edge); for (Edge nextEdge:node.edges){ priorityQueue.add(nextEdge); } } } } } return result; } }六、Dijkstra算法
适用范围:没有权值为负数的边
package FaceQuestion.图; import java.util.HashMap; import java.util.HashSet; import java.util.Map; /** * 图的最短路径算法 * Dijkstra算法 */ public class Dijkstra { public static HashMap<Node,Integer> dijkstra1(Node head){ //所有节点对应的路径长度和 HashMap<Node,Integer> distanceMap=new HashMap<>(); //头结点路径为0 distanceMap.put(head,0); //已经被选择过的节点集合 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); } distanceMap.put(edge.to,Math.min(distanceMap.get(toNode),distance+edge.weight)); } selectedNodes.add(minNode); minNode=getMinDistanceAndUnselectedNode(distanceMap,selectedNodes); } return distanceMap; } /** * 在未被选择的节点中选择拥有最短路径的节点 * @param distanceMap 所有节点的集合 * @param selectedNodes 已经选择过节点集合 * @return */ private static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> distanceMap, HashSet<Node> selectedNodes) { Node minNode=null; int minDistance=Integer.MAX_VALUE; for (Map.Entry<Node,Integer> entry:distanceMap.entrySet()){ Node node=entry.getKey(); int distance=entry.getValue(); if (!selectedNodes.contains(node)&&distance<minDistance){ minNode=node; minDistance=distance; } } return minNode; } }