(day 6)持续学习:数据结构-图

172 阅读7分钟

邻接矩阵

实现图的最简单的方法之一是使用二维矩阵。在该矩阵实现中,每个行和列表示图中的顶点。存储在行 i 和列 j 的交叉点处的单元中的值表示是否存在从顶点 i 到顶点 j 的边。当两个顶点通过边连接时,我们说它们是相邻的。单元格中的值表示从顶点 i 到顶点 j 的边的权重。

V0V1V2V3V4V5
V052
V14
V29
V373
V41
V58

邻接表

实现稀疏连接图的更空间高效的方法是使用邻接表。在邻接表实现中,我们保存Graph 对象中的所有顶点的主列表,然后图中的每个顶点对象维护连接到的其他顶点的列表。 在我们的顶点类的实现中,我们使用hashmap将节点相邻的节点相连,节点的值是边的权重。

class Node{
  int value;
}

//保存图
HashMap<Integer, Node> graph = new HashMap();
邻接表
V0 --->[(V1,5), (V5, 2)]
V1 --->[(V2,4)]
V2 --->[(V3,9)]
V3 --->[(V4,7), (V5, 3)]
V4 --->[(V0,1)]
V5 --->[(V4,8)]

统一的图结构

/**
 * 通用化图结构 对象
 *
 * 将图转换成 统一的图 对象结构,方便算法的计算
 *
 * N node : 节点的泛型
 *
 * E edge : 边的泛型
 */
@Data
public class Graph<N, E> {


	HashMap<N, Node<N, E>> nodes;

	HashSet<Edge<E>> edges;

	public Graph(){
		this.nodes = new HashMap<>();
		this.edges = new HashSet<>();
	}

	/**
	 *
	 * @param from 入
	 * @param to 出
	 * @param edgeW 边的权重
	 */
	public void addNode(N from , N to, E edgeW){
		Node<N, E> fromNode, toNode;

		fromNode = getNode(from);
		toNode = getNode(to);

		Edge edge = new Edge<E>();
		edge.setWeight(edgeW);
		edge.setFrom(fromNode);
		edge.setTo(toNode);
		edges.add(edge);

		fromNode.getEdges().add(edge);
		fromNode.getNexts().add(toNode);
		fromNode.setOut(fromNode.getOut()+1);

		toNode.setIn(toNode.getIn()+1);

	}

	/**
	 * 根据节点 值,查询节点
	 *
	 * @param nodeValue
	 * @return  如果找不到,返回新建的节点对象
	 */
	public Node getNode(N nodeValue){
		Node<N, E> node;
		if(nodes.containsKey(nodeValue)){
			node = nodes.get(nodeValue);
		}else{
			node = new Node();
			node.setValue(nodeValue);
			nodes.put(nodeValue, node);
		}
		return node;
	}


}

/**
 * 图的边
 */
@Data
public class Edge<E> {

	/**
	 * 边的 来源
	 */
	private Node from;

	/**
	 * 边的 去向
	 */
	private Node to;

	/**
	 * 边的权重
	 */
	private E weight;

	@Override
	public int hashCode(){
		return  super.hashCode();
	}
}

/**
 * 图里面的节点
 */
@Data
public class Node<N, E> {

	/**
	 * 节点入的度数
	 */
	private int in;

	/**
	 * 节点 出的度数
	 */
	private int out;

	/**
	 * 节点信息
	 */
	private N value;

	/**
	 * 当前节点  邻居节点
	 * 仅仅指 图节点 的出的邻居
	 */
	private List<Node<N, E>> nexts;

	/**
	 * 当前节点 邻居边
	 */
	private List<Edge<E>> edges;

	public Node(){
		this.nexts = new ArrayList<>();
		this.edges = new ArrayList<>();
	}

	public Node(N value){
		this();
		this.value = value;
	}


	@Override
	public String toString(){
		return this.value.toString();
	}

}

在图的问题中,难的是图的数据结构的设计,而不在于算法的处理过程。 在处理图的问题中,我们可以将图用该数据结构进行保存,然后用同一的结构进行处理。用自己熟悉的机构来code,会提高很大的效率。

拓扑排序

	/**
	 * 将图 进行拓扑 排序
	 */
	public static <N, E> List<Node<N, E>> topologySort(Graph<N, E> graph){


		Queue<Node<N, E>> zeroInQueue = new LinkedList<>();

		for (Node<N, E> node : graph.getNodes().values()) {
			//入度为0的边
			if(node.getIn() == 0){
				zeroInQueue.add(node);
			}
		}

		List<Node<N, E>> sortedNode = new ArrayList<>();

		/**
		 * 1、将入度为0的 node剔除
		 * 2、将node 对应的邻居节点的入度 -1
		 */
		while (!zeroInQueue.isEmpty()){

			//将入度为0 的node移除
			Node<N, E> node = zeroInQueue.poll();
			sortedNode.add(node);

			System.out.println("拓扑排序,移除:" + node.getValue());

			for (Node<N, E> next : node.getNexts()) {

				int in = next.getIn();
				//将邻居节点的入度数 -1
				next.setIn(--in);
				if(in == 0){
					//将邻居节点 的入度-1后 是0的节点 加入0入度节点队列
					zeroInQueue.add(next);
				}
			}
		}
		return sortedNode;

	}

最小生成子树

给定一个图,选择n条边,使其是一个联通的图,且各边权重相加的值最小。

K算法(Kruskal)

思路:

1、按照边的权重按照从小到大进行排序。

2、从小到大取边,依据并查集的思路,判断边的两个邻居节点是否在一个并查集里面。如果不在同一个并查集里面,当前边选中。

如果,两个节点已经在同一个并查集里面,放弃当前边的选择。

3、最后将选中的边进行返回即可。

	/**
	 * 按照 边的权重,从小到大排序;小根堆
	 * 并查集 查看两个 边的两个点是否在同一个集合内
	 */

	public static HashSet<Edge> kSMT(Graph<Integer, Integer> graph){

		PriorityQueue<Edge<Integer>> smallQueue = new PriorityQueue<>(new Comparator<Edge<Integer>>(){
			@Override
			public int compare(Edge<Integer> o1, Edge<Integer> o2) {
				return o1.getWeight() - o2.getWeight();
			}
		});

		graph.getEdges().forEach(edge -> {
			smallQueue.add(edge);
		});

		FindSet findSet = new FindSet();
		HashSet<Edge> edges = new HashSet<>();
		while (!smallQueue.isEmpty()){
			Edge<Integer> edge = smallQueue.poll();

			boolean isSameSet = findSet.isSameSet(edge.getFrom(), edge.getTo());

			if(!isSameSet){
				edges.add(edge);
				findSet.union(edge.getFrom(), edge.getTo());
			}
		}

		return edges;

	}


	/**
	 * 并查集 集合类
	 */
	@Data
	public static class FindSet{

		//所有节点集
		private HashSet<Node> nodes = new HashSet<>();
		//当前节点的 代表象征节点
		private HashMap<Node, Node> symbolMap = new HashMap<>();

		/**
		 * 将Node1 和 node2两个节点的聚合,进行合并
		 *
		 */
		public void union(Node node1, Node node2){

			if(isSameSet(node1, node2)){
				//在同一个 集合内,不需要合并
				return;
			}

			Node symbol1 = symbolMap.get(node1);
			Node symbol2 = symbolMap.get(node2);

			//将node1  合并到  node2
			symbolMap.entrySet().forEach(entity -> {

				if(Objects.equals(entity.getValue(), symbol2)){
					entity.setValue(symbol1);
				}

			});

		}

		/**
		 * 比较是否为 同一个集合
		 * 如果是,构建到 symbolmap中
		 */
		public boolean isSameSet(Node node1, Node node2){

			checkAndAdd(node1);
			checkAndAdd(node2);

			Node symbol1 = symbolMap.get(node1);
			Node symbol2 = symbolMap.get(node2);

			return Objects.equals(symbol1, symbol2);

		}

		public void checkAndAdd(Node node){

			if(!nodes.contains(node)){
				nodes.add(node);
				symbolMap.put(node, node);//自己的 象征节点,初始为自己
			}
		}

	}

P算法(primMST)

思路:

1、随机挑选一个点

2、将点解锁放到解锁点集内,将点的邻边放到待解锁小根堆里面。

3、然后,从小到大挑选边,如果边两侧的点都在解锁的点集,抛弃该边。否之,保留边到结果集合,回到2继续执行

4、所有节边都解锁


	/**
	 1、随机挑选一个点
	 2、将点解锁放到解锁点集内,将点的邻边放到待解锁小根堆里面。
	 3、然后,从小到大挑选边,如果边两侧的点都在解锁的点集,抛弃该边。否之,保留边到结果集合,回到2继续执行
	 4、所有节边都解锁
	 */
	public static HashSet<Edge<Integer>> primMST(Graph<Integer, Integer> graph){

		Iterator<Node<Integer, Integer>> iterator = graph.getNodes().values().iterator();

		PriorityQueue<Edge<Integer>> smallEdgeQueue = new PriorityQueue<>(new Comparator<Edge<Integer>>(){
			@Override
			public int compare(Edge<Integer> o1, Edge<Integer> o2) {
				return o1.getWeight() - o2.getWeight();
			}
		});

		//已经解锁的点集合
		HashSet<Edge<Integer>> unlockedEdgeSet = new HashSet<>();
		//已经解锁的边集合
		HashSet<Node<Integer, Integer>> unlockedNodeSet = new HashSet<>();

		/**
		 * 用于检测 给的图,是否存在森林的情况
		 * 若存在森林,给出所有森林的 最小生成树
		 */
		while (iterator.hasNext()){

			Node<Integer, Integer> node = iterator.next();

			unlockedNodeSet.add(node);

			smallEdgeQueue.addAll(node.getEdges());

			while (!smallEdgeQueue.isEmpty()){
				Edge<Integer> edge = smallEdgeQueue.poll();
				Node<Integer, Integer> toNode = edge.getTo();

				if(!unlockedNodeSet.contains(toNode)){
					//下个邻居节点 没有解锁,将其解锁
					unlockedNodeSet.add(toNode);
					unlockedEdgeSet.add(edge);

					//邻居节点解锁后,将邻居节点的 邻边 加入到 samllEdgeQueue

					smallEdgeQueue.addAll(toNode.getEdges());
				}
			}
		}

		return unlockedEdgeSet;
	}

Dijkstra最短路径算法

用于寻找途中A点到其他点的最短路径。

思路:

1、设置一个distanceMap,distanceMap中保存着从点A到其他点已经确的距离。放入第一个值,从A点到A点的距离为0。即:distanceMap={(A, 0)}

2、设置一个selectNodeSet,标记已经执行过的点的集合。

3、从distanceMap中获取2中不存在的距离最小的点X。

4、遍历从3取出点X的所有邻居节点,将X作为跳板节点Xn、Xn+1……,计算由X节点到他邻居节点Xn、Xn的距离。并和distanceMap已经存在点A到Xn、Xn+1……的距离取小值,重新返回distanceMap。

5、重复3、4两步,知道步骤3没有值位置。

6、返回distanceMap,即为点A到Xn、Xn+1……的最小距离。如果需要计算从A到X点的最小距离和路线,在步骤4中进行保留。


	/**
	 * 1、设置一个distanceMap,distanceMap中保存着从点A到其他点已经确的距离。放入第一个值,从A点到A点的距离为0。即:distanceMap={(A, 0)}
	 * 2、设置一个selectNodeSet,标记已经执行过的点的集合。
	 * 3、从distanceMap中获取,2中不存在的点X。
	 * 4、遍历从3取出点X的所有邻居节点,将X作为跳板节点Xn、Xn+1……,计算由X节点到他邻居节点Xn、Xn的距离。并和distanceMap已经存在点A到Xn、Xn+1……的距离取小值,重新返回distanceMap。
	 * 5、重复3、4两步,知道步骤3没有值位置。
	 * 6、返回distanceMap,即为点A到Xn、Xn+1……的最小距离。如果需要计算从A到X点的最小距离和路线,在步骤4中进行保留。
	 */
	public static Map<Node, Integer> dijkstra(Node<Integer, Integer> startNode){

		HashSet<Node> selectedNodes = new HashSet<>();
		Map<Node, Integer> distanceMap = new HashMap<>();

		distanceMap.put(startNode, 0);

		Node<Integer, Integer> jumpNode = getUnselectedNodeInDistanceMap(distanceMap,selectedNodes);
		while (jumpNode != null){

			//以当前节点作为跳板节点  计算时候需要的基数
			Integer jumpBaseDistance = distanceMap.get(jumpNode);

			for (Edge<Integer> edge : jumpNode.getEdges()) {

				Node<Integer, Integer> toNode = edge.getTo();
				if(!distanceMap.containsKey(toNode)){
					distanceMap.put(toNode, jumpBaseDistance + edge.getWeight());
				}else{

					Integer minDistance = Math.min(jumpBaseDistance + edge.getWeight(), distanceMap.get(toNode));
					distanceMap.put(toNode, minDistance);
					//如果需要记录,根据判断结果 记录。 <node, jumpBasePath + edge>
				}
			}

			selectedNodes.add(jumpNode);
			jumpNode = getUnselectedNodeInDistanceMap(distanceMap,selectedNodes);
		}

		return distanceMap;
	}

	public static Node<Integer, Integer> getUnselectedNodeInDistanceMap(Map<Node<Integer, Integer>, Integer> distanceMap, HashSet<Node> selectedNodes){

		Integer min = Integer.MAX_VALUE;
		Node minNode = null;
		for (Node<Integer, Integer> node : distanceMap.keySet()) {
			if(!selectedNodes.contains(node) && node.getValue() < min){
				min = node.getValue();
				minNode = node;
			}
		}
		return minNode;
	}

关于Dijkstra的优化 由于getUnselectedNodeInDistanceMap方法在取最小节点值的时候,采用的是遍历判断的方法,时间复杂度为O(n^2),可以修改成小根堆的形式。

1、根据起始点到节点node的距离构建小根堆。

2、当起始点到node的距离被修改成更小的值时,小根堆内对应的node的根据就需要heapify。

3、小根堆内,已经pop出的节点,从实际小根堆内移除。


	public static Map<Node<Integer, Integer>, Integer> dijkstra2(Graph<Integer, Integer> graph, Node<Integer, Integer> startNode) {

		NodeHeap nodeHeap = new NodeHeap(graph.getNodes().size());

		NodeRecord originPoint = new NodeRecord(startNode, 0);
		nodeHeap.addOrUpdateOrIgnore(originPoint);

		while (nodeHeap.size != 0){

			NodeRecord<Integer, Integer> topRecord = nodeHeap.pop();

			int jumpNodeDistance = topRecord.distance;

			for(Edge<Integer> edge : topRecord.node.getEdges()){

				NodeRecord<Integer, Integer> neighborNode = new NodeRecord(edge.getTo(), jumpNodeDistance + edge.getWeight());

				nodeHeap.addOrUpdateOrIgnore(neighborNode);
			}
		}

		return  nodeHeap.distanceMap;

	}

	static class NodeRecord<N, E>{

		Node<N, E> node;

		int distance;

		public NodeRecord(Node node, int distance){
			this.node = node;

			this.distance = distance;
		}

	}
	/**
	 * 小根堆
	 */
	static class NodeHeap{

		//实际节点的堆
		NodeRecord<Integer, Integer>[] nodes;

		//堆中  node对应的下标
		Map<Node<Integer, Integer>, Integer> nodeIndexMap;

		Map<Node<Integer, Integer>, Integer> distanceMap;
		//堆中生效的节点数量
		int size;


		public NodeHeap(int size){
			this.nodes = new NodeRecord[size];
			this.nodeIndexMap = new HashMap<>();
			this.distanceMap = new HashMap<>();
			this.size = 0;
		}

		public NodeRecord pop(){
			if(size == 0){
				return null;
			}

			NodeRecord topNode = this.nodes[0];
			this.nodes[0] = this.nodes[size-1];
			size--;
			int index = 0;
			while (index*2+1 < size){

				if(nodes[index].distance > nodes[index*2 +1].distance){
					index = index*2+1;
					continue;
				}else if((index*2 + 2) < size && nodes[index].distance > nodes[index*2 +2].distance){
					index = index*2+2;
					continue;
				}else {
					break;
				}
			}

			swap(0, index);

			nodeIndexMap.put(topNode.node, -1);

			return topNode;
		}

		public void addOrUpdateOrIgnore(NodeRecord nodeRecord){
			if(!nodeIndexMap.containsKey(nodeRecord.node)){
				//节点原本不存在,新增节点到小根堆内
				addNode(nodeRecord);
			}else if(nodeIndexMap.get(nodeRecord.node) == -1){
				return;
			}else{
				int distance = distanceMap.get(nodeRecord.node);
				if(distance > nodeRecord.distance){
					distanceMap.put(nodeRecord.node, nodeRecord.distance);
				}
			}
		}

		private void addNode(NodeRecord nodeRecord){
			int index = size;
			nodes[index] = nodeRecord;
			size++;

			while (nodes[index].distance < nodes[(index-1)/2].distance){

				NodeRecord parent = nodes[(index-1)/2];

				if(nodeRecord.distance < parent.distance){

					NodeRecord tem = parent;
					nodes[(index-1)/2] = nodes[index];
					nodes[index] = tem;
					index = (index -1)/2;
				}else{
					break;
				}

			}

			distanceMap.put(nodeRecord.node, nodeRecord.distance);
			nodeIndexMap.put(nodeRecord.node, index);

		}

		private void swap(int i, int j){

			nodeIndexMap.put( this.nodes[i].node, j);
			nodeIndexMap.put(this.nodes[j].node, i);

			NodeRecord tmp = this.nodes[i];
			this.nodes[i] = this.nodes[j];
			this.nodes[j] = tmp;
		}
	}