图
邻接矩阵
实现图的最简单的方法之一是使用二维矩阵。在该矩阵实现中,每个行和列表示图中的顶点。存储在行 i 和列 j 的交叉点处的单元中的值表示是否存在从顶点 i 到顶点 j 的边。当两个顶点通过边连接时,我们说它们是相邻的。单元格中的值表示从顶点 i 到顶点 j 的边的权重。
| V0 | V1 | V2 | V3 | V4 | V5 | |
|---|---|---|---|---|---|---|
| V0 | 5 | 2 | ||||
| V1 | 4 | |||||
| V2 | 9 | |||||
| V3 | 7 | 3 | ||||
| V4 | 1 | |||||
| V5 | 8 |
邻接表
实现稀疏连接图的更空间高效的方法是使用邻接表。在邻接表实现中,我们保存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;
}
}