目录
图
基础第六课题目一:图的存储方式
邻接表法表示无向图:
矩阵表法:
有向无环图:
图的难点在于:图的算法不难,但是图的结构有多种表达,在这类题的技巧在于,先用一种图结构a实现所有算法,把其他遇到的图结构转换成图结构a,在代入算法。
左神常用图结构:
public class Graph { //integer 是索引 public HashMap<Integer,Node> nodes; public HashSet<Edge> edges; public Graph() { nodes = new HashMap<>(); edges = new HashSet<>(); } }
节点结构:
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<>(); } }
边的结构:无向边只要2个反向边一拼就可以了
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; } }
图接口的实现:
public class GraphGenerator { //matrix的每一行结构为:源城市编号,目标城市编号,距离 //将它转换为我的图结构表示 public static Graph createGraph(Integer[][] matrix) { Graph graph = new Graph(); for (int i = 0; i < matrix.length; i++) { Integer weight = matrix[i][0];//距离 Integer from = matrix[i][1];//源城市编号 Integer 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++; fromNode.edges.add(newEdge); //完善目标城市节点信息 toNode.in++; //完善图结构信息 graph.edges.add(newEdge); } return graph; } }
基础第六课题目二:优先遍历
图的宽度优先遍历
0:46:00附近
public static 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) { //不重复,则添加到队列和set中 if (!map.contains(next)) { map.add(next); queue.add(next); } } } }
广度优先遍历
基础第六课题目三:拓扑排序
解决依赖之间,有互相依赖的情况,哪一个优先编译
依次找到入度为0的点,把它和它的指向都擦掉,如此循环:
// directed graph and no loop public static List<Node> sortedTopology(Graph graph) { //记录节点,及其入度 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 ArrayList<>(); while (!zeroInQueue.isEmpty()) { //弹出一个当前入度为0的节点 Node cur = zeroInQueue.poll(); //加入到输出中 result.add(cur); //将此节点的指向全部擦去,指向的节点入度全部减一 for (Node next : cur.nexts) { inMap.put(next, inMap.get(next) - 1); //若入度减一后变为入度为0的节点,加入到收集队列中 if (inMap.get(next) == 0) { zeroInQueue.add(next); } } } return result; }
基础第六课题目四:kruskal算法
和prim功能一样,但出发点不同,kruskal从边的角度出发
消除多余的边,同时保证连通性
从最小的边开始考虑,加上这条边,看有没有形成环,有环就不加,没环就保留。
查看有没有环:集合查询和集合合并的机制,使用并查集
1:37:00附近
// 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 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) { 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算法
和kruskal功能一样,但出发点不同,prim从node的角度出发
1:49:00附近
假设从A开始,解锁了3条边,然后选最小的,就到了C,再解锁4条边,选最小的,到F,解锁2条边,选最小,到D,没有解锁新边,从剩余已解锁的边选最小的,到B,解锁一条新的边,到E。
public static Set<Edge> primMST(Graph graph) { // 把解锁的边放入小根堆,小根堆会从小到大将元素弹出, // 定义个比较器,用于比较边的大小 PriorityQueue<Edge> priorityQueue = new PriorityQueue<>( new EdgeComparator()); // 考察过的点都放入这个集合里面 HashSet<Node> set = new HashSet<>(); Set<Edge> result = new HashSet<>(); // values(),返回一个collection迭代器 // 这个for循环是为了解决多个连通区域不连通的问题 for (Node node : graph.nodes.values()) {//随便挑一个点开始 // 如果这个点没考察过,记录到集合中 if (!set.contains(node)) { set.add(node); // 遍历这个点的所有边,放入小根堆 for (Edge edge : node.edges) { 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 : toNode.edges) { priorityQueue.add(nextEdge); } } } } } return result; }
基础第六课题目六:Dijkstra算法
用于记录一个点出发,到其他点的最小距离,可以使用小根堆的优化。
// 从head出发到所有点的最小距离
public static HashMap<Node, Integer> dijkstra1(Node head) {
// key : 从head出发到达key
// value : 从head出发到达key的最小距离
HashMap<Node, Integer> distanceMap = new HashMap<>();
// 头结点到自己的距离为0
distanceMap.put(head, 0);
// 求过距离的节点,存在selectedNodes中,以后再也不碰,锁住了
// head到selectedNode的距离不会再更新
HashSet<Node> selectedNodes = new HashSet<>();
// 一开始,所有节点都没锁,所以返回的节点必然是head
Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
while (minNode != null) {
int distance = distanceMap.get(minNode);
for (Edge edge : minNode.edges) {
Node toNode = edge.to;
// 如果head到tonode的距离从来没有计算过,那么距离就是head到
// 当前节点距离,加上当前节点到tonode距离
if (!distanceMap.containsKey(toNode)) {
distanceMap.put(toNode, distance + edge.weight);
}
// 如果head到tonode的距离计算过,那么距离就是(head到当前节点距离加上
// 当前节点到tonode距离),或者(head到tonode距离距离)之间较小的一个
distanceMap.put(edge.to, Math.min(distanceMap.get(toNode), distance + edge.weight));
}
// 并将当前节点锁住
selectedNodes.add(minNode);
// 在没有被锁死的点中,再次找到离head距离最小的点
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
}
return distanceMap;
}
// 在没有被锁死的点中,找到离head距离最小的点
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();
// 如果head节点到当前终点节点距离的计算没有被锁死,并且距离是最小
// 的,把这个点选出来,作为下一轮距离计算的起点
if (!touchedNodes.contains(node) && distance < minDistance) {
minNode = node;
minDistance = distance;
}
}
return minNode;
}