图
图的存储方式
-
图:点集、边集、有向、无向
-
邻接表
-
邻接矩阵
表达图的方式(数据结构)有很多种,不同的题目可能有不同的数据结构和处理方式,但算法就只有几种且基本一致,所以应使用邻接表法或者其它数据结构模板把所有算法都实现一遍,后续遇到类似的题目即使是其他的数据结构,就把特殊的数据结构转化成熟悉的模板再进行实现
一般结构
public class Graph{
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<>();
}
}
//无向边是特殊的有向边:双向
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 Graph createGraph(Integer[][] matrix){
Graph graph=new Graph();
for(int i=0;i<matrix.length;i++){
Integer from=matrix[i][0];
Integer to=matrix[i][1];
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;
}
如果某些题目不需要使用某些数据项,可以不要,这个是通用模板
图的宽度优先遍历(出队列的时候处理)
- 利用队列实现
- 从源节点开始依次按照宽度进队列,然后弹出
- 每弹出一个点,把该节点所有没进过队列的邻接点放入队列
- 直到队列变空
注:图中是有环的,不像二叉树,因此需要一个检查机制HashSet
public void bfs(Node node){
if(node==null){
return;
}
Queue<Node> queue=new LinkedList<>();
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);
}
}
}
}
图的广度(深度)优先遍历(进栈之后处理)
- 利用栈实现
- 从源节点开始把节点按照深度放入栈,然后弹出
- 每弹出一个点,把该节点的下一个没进过栈的邻接点放入栈
- 直到栈变空
一旦发现某条路没走过,就逮着这条路往死里走
public void dfs(Node node){
if(node==null){
return;
}
Stack<Node> stack=new Stack<>();
HashSet<Node> set=new HashSet<>();
stack.push(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的节点,且没有环
常用于:编译顺序
工程依赖:没有循环依赖,有些过程需要依赖前面的过程完成才能进行
怎么决定编译顺序?
先找到入度为0的节点A,把A以及其影响去掉,就能找到下一个入度为0的节点B,以此类推,得到拓扑排序ABCD
public List<Node> sortedTopology(Graph graph){
//key:某一个node value:剩余入度
HashMap<Node,Integer> inMap=new HashMap<>();
//入度为0的点才能进此队列
Queue<Node> zeroInQueue=new LinkedList<>();
for(Node node:graph.nodes.values()){//values()
inMap.put(node,node.in);
if(node.in==0){
zeroInQueue.add(node);
}
}
List<Node> res=new ArrayList<>();
while(!zeroInQueue.isEmpty()){
Node cur=zeroInQueue.poll();
res.add(cur);
for(Node next:cur.nexts){
inMap.put(next,inMap.get(next)-1);//update
if(inMap.get(next)==0){
zeroInQueue.add(next);
}
}
}
return res;
}
生成最小生成树(kruskal算法、prim算法)
适用范围:要求无向图
最小生成树:保证所有点连通且边之和最小
kruskal算法
从边的角度出发,从最小的边开始,判断如果加上的话有无形成环,没有环则加上,有则不要
keypoint:怎么判断有无形成环:并查集
假设所有的点一开始自己都是一个集合
边加上之前判断边的两个点是否在一个集合中,是则不要,不是则将两个点所在的集合合并
并查集简单版本: 没有并查集快,并查集是常数级别的
public class Mysets{
public HashMap<Node,List<Node>> setMap;
public MySets(List<Node> nodes){
for(Node cur:nodes){
List<Node> set=new ArrayList<>();
set.add(cur);
setMap.put(cur,set);
}
}
//判断两个点是否在同一个集合
public boolean isSameSet(Node from,Node to){
List<Node> fromSet=setMap.get(from);
List<Node> toSet=setMap.get(to);
return fromSet==toSet;
}
//合并两个集合
public void union(Node from,Node to){
List<Node> fromSet=setMap.get(from);
List<Node> toSet=setMap.get(to);
for(Node toNode:toSet){
fromSet.add(toNode);
setMap.put(toNode,fromSet);
}
}
}
并查集
public 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);
}
}
}
}
kruskal算法
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> res=new HashSet<>();
while(!priorityQueue.isEmpty()){
Edge edge=priorityQueue.poll();
if(!unionFind.isSameSet(edge.from,edge.to)){
res.add(edge);
unionFind.union(edge.from,edge.to);
}
}
return res;
}
public class EdgeComparator implements Comparator<Edge>{
@Override
public int compare(Edge e1,Edge e2){
return e1.weight-e2.weight;
}
}
prim算法
从点出发(任意一个点),然后解锁该点相关的所有边,在所有边中选取最小的,然后把该边连接的另外一个点加上,再解锁所有边,依次循环,直到所有点都加进来了,在这个过程中,如果一条边的两个点都已经加入过,则该边不在选择范围内
k算法可以看作是两团点连载一起,即连通顺序不是一个一个点来连通的,所以需要使用到并查集这种结构,而p算法只会一次连上一个点,用一个哈希表即可实现
public Set<Edge> primMST(Graph graph){
//小根堆
PriorityQueue<Edge> priorityQueue=new PriorityQueue<>(new EdgeComparator());
//放点
HashSet<Node> set=new HashSet<>();
//放边(结果)
Set<Edge> res=new HashSet<>();
for(Node node:graph.nodes.values()){//这层for循环是为了解决森林问题 如果没有该问题可以省去(即整个图没有互相连在一起)
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);
res.add(edge);
for(Edge nextEdge:toNode.edges){
priorityQueue.add(nextEdge);//这里边可能会重复加进集合 但不影响结果 增加的是常数时间 如果想优化该部分 可以再加一层判断
}
}
}
}
}
return res;
}
Dijkstra算法
适用范围:没有权值为负数的边,准确说是没有出现累加和为负数的环
规定一个出发点,这个点到其他所有点的最短距离是多少
从该点出发,到其他点的距离,有更小的就更新,全部边都遍历结束后锁死两点之间的距离,然后在剩下的距离中选择最小的继续,所有距离都锁死之后就结束
public HashMap<Node,Integer> dijkstra(Node head){
//从head出发到所有点的最小距离,key是指从head出发到达的点,value是指head出发到key的最小距离,如果在表中没有记录则含义是head出发到这个点的距离为正无穷
HashMap<Node,Integer> distanceMap=new HashMap<>();
distanceMap.put(head,0);
//已经求过距离的节点,存在selectedNodes中以后再也不碰
HashSet<Node> selectedNodes=new HashSet<>();
//在所有没有被求过距离的节点中选择最小的节点,如果全部节点的距离都被计算过了则返回null
Node minNode=getMinDistanceAndUnselectedNode(distanceMap,selectedNodes);
while(minNode!=null){
int distance=distanceMap.get(minNode);
for(Edge edge:minNode.edges){
if(!distanceMap.containsKey(edge.to)){
distanceMap.put(edge.to,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 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;
}
该算法有一个堆的优化
尝试使用堆的结构进行时间复杂度上的优化
有些题目弹出一个点不会影响其他点的距离 则可以使用系统自带的堆结构 否则需要自行实现堆结构
在选择剩下的距离中最小的时候使用堆来优化时间复杂度 目前的实现方式是遍历
需要自行实现堆结构(堆中的值会发生改变,并且需要重新调整结构)值发生改变然后再调整
改进后的dijkstra算法
public HashMap<Node,Integer> dijkstra(Node head,int size){
NodeHeap nodeHeap=new NodeHeap(size);
nodeHeap.addOrUpdateOrIgnore(head,0);//黑盒
HashMap<Node,Integer> res=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);
}
res.put(cur,distance);
}
return res;
}
//自行实现堆结构
public class NodeHeap{
private Node[] nodes;//底层数组
private HashMap<Node,Integer> heapIndexMap;//value是索引值,作用:查node是否在堆中
private HashMap<Node,Integer> distanceMap;//value是距离
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进来过且被弹出来 heapIndexMap中node节点对应的value值是-1
public boolean isEntered(Node node){
return heapIndexMap.containsKey(node);
}
//判断节点是否在堆中 即进来过且没弹出
public boolean inHeap(Node node){
return inEntered(node)&&heapIndexMap.get(node)!=-1;
}
//这里的交换操作需要在两个地方进行交换:数组以及索引值
public 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;
}
//小根堆 向上
public 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;
}
}
//小根堆 向下
public void heapify(int index,int size){
int left=2*index+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;//不要忘记了
}
}
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++);//向上 这里的索引值可能会在swap中改变
}
//剩下一种情况进来过但弹出去了 就Ignore
}
//弹出
public NodeRecord pop(){
NodeRecord nodeRecord=new NodeRecord(nodes[0],distanceMap.get(nodes[0]));
swap(0,size-1);
heapIndexMap.put(node[size-1],-1);//两个map处理方式不同
distanceMap.remove(nodes[size-1]);
nodes[size-1]=null;//gc
heapify(0,--size);//x
return nodeRecord;
}
}
public class NodeRecord{
public Node node;
public int distance;
public NodeRecord(Node node,int distance){
this.node=node;
this.distance=distance;
}
}