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;
	}
}

猜你喜欢

转载自blog.csdn.net/weixin_44337241/article/details/121243297