一、图
1)由点的集合和边的集合构成
2)虽然存在有向图和无向图的概念,但实际上都可以用有向图来表达
3)边上可能带有权值
二、图结构的表达
1)邻接表法 类似哈希表, key就是当前节点。value就是对应有指向的邻接节点
2)邻接矩阵法 类似二维数据 横纵分别都是节点,每个[i][j]表示i j 两点的连接
3)除此之外还有其他众多的方式
三、图的题目如何搞定
图的算法都不算难,只不过coding的代价比较高
1)先用自己最熟练的方式,实现图结构的表达
2)在自己熟悉的结构上,实现所有常用的图算法作为模板
3)把题提供的图结构转化为自己熟悉的图结构,再调用模板或改写即可
四、点、边、图定义类 、二维数组信息转换图结构
package class16;
import java.util.ArrayList;
import java.util.List;
//节点类结构
public class NodeTrain {
public int value; //节点值
public int in; //入度 即指向该节点的前节点个数
public int out; //出度 即从该节点指向的后节点个数
public List<NodeTrain> nexts; //该节点指向的后节点集合
public List<EdgeTrain> edges; //从该节点指向后节点的边
public NodeTrain(int v){
value = v;
in = 0;
out = 0;
nexts = new ArrayList<>();
edges = new ArrayList<>();
}
}
package class16;
//两节点相连的边结构类
public class EdgeTrain {
public int weight; //边权重
public NodeTrain from; //该边的起始节点 from -> to
public NodeTrain to; //该边的终止节点
public EdgeTrain(int w,NodeTrain f, NodeTrain t){
weight = w;
from = f;
to = t;
}
}
package class16;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
//图结构类,包含了图中的节点,节点之间的边
public class GraphTrain {
public HashMap<Integer,NodeTrain> nodes; //图中的节点 key表示节点值 value表示 对应值封装的节点类结构
public HashSet<EdgeTrain> edges; //图中的边
public GraphTrain(){
nodes = new HashMap<>();
edges = new HashSet<>();
}
}
package class16;
public class GraphGeneratorTrain {
// matrix 所有的边
// N*3 的矩阵
// [weight, from节点上面的值,to节点上面的值]
//
// [ 5 , 0 , 7]
// [ 3 , 0, 1]
//
public static GraphTrain createGraph(int[][] matrix) {
GraphTrain graphTrain = new GraphTrain();
//将二维数组信息 分别取出 边权重 起始节点 终止节点
for (int i = 0; i < matrix.length; i++) {
//每一行就是一组节点数组 取出对应值
int weight = matrix[i][0];
int fromV = matrix[i][1];
int toV = matrix[i][2];
//判断该两节点是否不在图类中的nodes集合 不在那么就添加进去
if(!graphTrain.nodes.containsKey(fromV)){
graphTrain.nodes.put(fromV,new NodeTrain(fromV));
}
if(!graphTrain.nodes.containsKey(toV)){
graphTrain.nodes.put(toV,new NodeTrain(toV));
}
//接着就是添加两节点的边 取出两个节点
NodeTrain fromNode = graphTrain.nodes.get(fromV);
NodeTrain toNode = graphTrain.nodes.get(toV);
EdgeTrain edgeTrain = new EdgeTrain(weight, fromNode, toNode);
//图添加边 起始节点的指向节点集合添加终止节点 出度+1 终止节点入度+1 起始节点的边集合添加该边
graphTrain.edges.add(edgeTrain);
fromNode.nexts.add(toNode);
fromNode.out++;
toNode.in++;
fromNode.edges.add(edgeTrain);
}
return graphTrain;
}
}
五、图的宽度优先&深度优先遍历
宽度优先遍历
1,利用队列实现
2,从源节点开始依次按照宽度进队列,然后弹出
3,每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
4,直到队列变空
深度优先遍历
1,利用栈实现
2,从源节点开始把节点按照深度放入栈,然后弹出
3,每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
4,直到栈变空
package class16;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Queue;
/**
* 图宽度优先遍历
* 1,利用队列实现
* 2,从源节点开始依次按照宽度进队列,然后弹出
* 3,每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
* 4,直到队列变空
*/
public class BFS {
//从start节点开始 进行宽度优先遍历
public void bfs(NodeTrain start){
if(start == null){ //空节点就直接返回
return;
}
//定义队列,将开始节点入队列,后续用来遍历
Queue<NodeTrain> queue = new LinkedList<>();
queue.add(start);
//定义有序集合,将开始节点入队列,目的是为了避免图中多个指向一个节点的情况,就需要判断存在就不需遍历,否则会存在重复打印或者死循环
HashSet<NodeTrain> set = new HashSet<>();
set.add(start);
while(!queue.isEmpty()){
//先弹出队列节点 并打印
NodeTrain poll = queue.poll();
System.out.println(poll.value);
//判断当前节点的下层指向节点
for(NodeTrain next:poll.nexts){
//如果当前的有序集合中 不存在该下层节点时,那么就可以入队列,入有序集合 用于后续的弹出以及避免再打印
if(!set.contains(next)){
queue.add(next);
set.add(next);
}
}
}
}
}
package class16;
import java.util.HashSet;
import java.util.Stack;
/**
* 图深度优先遍历
* 1,利用栈实现
* 2,从源节点开始把节点按照深度放入栈,然后弹出
* 3,每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈,一旦有入栈,注意就要把该节点和邻接节点一起入栈。然后直接退出当前循环
* 4,直到栈变空
*/
public class DFS {
public void dfs(NodeTrain start){
if(start == null){
return ;
}
//定义一个栈用来存在节点 ,开始节点入栈 用于后续的遍历
Stack<NodeTrain> stack = new Stack<>();
stack.add(start);
//定义一个有序集合,开始节点入集合 用于后续遍历不重复打印有多个指向的邻接节点
HashSet<NodeTrain> set = new HashSet<>();
set.add(start);
//先进行打印
System.out.println(start.value);
while(!stack.isEmpty()){
//弹出栈节点
NodeTrain pop = stack.pop();
for(NodeTrain next : pop.nexts){
//判断该节点的下层节点是否已经在有序集合中 不在那么就需要 将当前节点与下层节点一起入栈,入集合 当前节点也需要重新入是因为要保存节点,假设还有其他的 邻接节点 后续往上往返时就可以取到节点
if(!set.contains(next)){
stack.push(pop);
stack.push(next);
set.add(next);
//入栈后就直接打印,这样就是直接打印下层邻接节点
//最后打印到最后一个节点时,因为我们每次都是将上层的节点也入栈,最终整个栈保存的就是完整的一次深度优先遍历的
//整个链条节点,依次往上弹出,因为都入集合了,索引就不会再执行打印,直接有其他下层邻接点没有入集合的,就会进入
//这个节点 入栈,入集合,打印。 循环往返直至整个图的节点都打印完,栈也就为空了
System.out.println(next.value);
//因为是DFS深度优先,有一个下层节点,入栈后就退出循环,不需要再入栈同层下层节点
break;
}
}
}
}
}
六、图的拓扑排序算法
1)在图中找到所有入度为0的点输出
2)把所有入度为0的点在图中删掉,继续找入度为0的点输出,周而复始
3)图的所有点都被删除后,依次输出的顺序就是拓扑排序
要求:有向图且其中没有环
应用:事件安排、编译顺序
package class16;
import java.util.*;
/**
* 自定义图的拓扑排序
* 1)在图中找到所有入度为0的点输出
* 2)把所有入度为0的点在图中删掉,继续找入度为0的点输出,周而复始
* 3)图的所有点都被删除后,依次输出的顺序就是拓扑排序
*
* 要求:有向图且其中没有环
* 应用:事件安排、编译顺序
*/
public class TopologySort {
// directed graph and no loop
public static List<NodeTrain> sortedTopology(GraphTrain graph) {
//定义一个哈希表 存在每个节点对应的入度数
HashMap<NodeTrain,Integer> inMap = new HashMap<>();
//定义一个队列存在入度为0的节点,用于后续的遍历输出 图拓扑排序 就是入度0的节点排在前面
Queue<NodeTrain> zeroQueue = new LinkedList<>();
for(NodeTrain node: graph.nodes.values()){
inMap.put(node,node.in);
if(node.in == 0){
zeroQueue.add(node);
}
}
//定义一个结果集合,用来接收zeroQueue弹出的节点
List<NodeTrain> ans = new ArrayList<>();
//开始遍历zeroQueue队列
while(!zeroQueue.isEmpty()){
NodeTrain poll = zeroQueue.poll();
//弹出的元素,就是入度0的 直接入结果集合 这个就是拓扑排序顺序
ans.add(poll);
//弹出后,需要刷新其邻接节点的入度数-1
for(NodeTrain next:poll.nexts){
inMap.put(next,inMap.get(next)-1);
//-1后 会有些邻接节点入度变成0 这个时候需要再入队列中 用于后续弹出 放到结果集
if(next.in == 0){
zeroQueue.add(next);
}
}
}
return ans;
}
}
七、lintCode 127拓扑排序
![](https://i-blog.csdnimg.cn/blog_migrate/55d2144172de9cbd7f7a61cce89ad40b.png)
![](https://i-blog.csdnimg.cn/blog_migrate/c98bd9dd042da057aee70174a465db13.png)
* 思路1:BFS 宽度优先遍历
* 1)在图中找到所有入度为0的点输出
* 2)把所有入度为0的点在图中删掉,继续找入度为0的点输出,周而复始
* 3)图的所有点都被删除后,依次输出的顺序就是拓扑排序
package class16;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Queue;
/**
* 图的拓扑排序算法
* OJ链接:https://www.lintcode.com/problem/topological-sorting
* 描述
*
* 给定一个有向图,图节点的拓扑排序定义如下:
*
* 对于图中的每一条有向边 A -> B , 在拓扑排序中A一定在B之前.
* 拓扑排序中的第一个节点可以是图中的任何一个没有其他节点指向它的节点.
*
* 针对给定的有向图找到任意一种拓扑排序的顺序.
*
*
* 思路:BFS 宽度优先遍历
* 1)在图中找到所有入度为0的点输出
* 2)把所有入度为0的点在图中删掉,继续找入度为0的点输出,周而复始
* 3)图的所有点都被删除后,依次输出的顺序就是拓扑排序
*
* 要求:有向图且其中没有环
* 应用:事件安排、编译顺序
*/
public class TopologicalOrderBFS {
// 不要提交这个类
public static class DirectedGraphNode {
public int label;
public ArrayList<DirectedGraphNode> neighbors;
public DirectedGraphNode(int x) {
label = x;
neighbors = new ArrayList<DirectedGraphNode>();
}
}
//方法:通过BFS宽度优先遍历来对有向图进行拓扑排序
public ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph) {
//找出入度为0 的节点 这里我们用哈希表存放每个节点的入度情况
//定义节点入度哈希表 遍历节点集合 初始值value为0
HashMap<DirectedGraphNode,Integer> inMap = new HashMap<>();
for(DirectedGraphNode node: graph){
inMap.put(node,0);
}
//再遍历集合节点的每个邻接点,将存在有邻接点,将该邻接点的入度+1
for(DirectedGraphNode node : graph){
for(DirectedGraphNode next : node.neighbors){
inMap.put(next,inMap.get(next)+1);
}
}
//当前我们就把inMap每个节点的入度数都确认好了。 依次取数入度为0的节点存放到一个集合中,后续用于遍历 依次取出就是图的拓扑排序
Queue<DirectedGraphNode> zeroQueue = new LinkedList<>();
for(DirectedGraphNode node : inMap.keySet()){
if(inMap.get(node) == 0){
zeroQueue.add(node);
}
}
//定义结果集 依次弹出zeroQueue 入度为0 的节点 入集合,就是拓扑排序 顺序就是入度0的开始指向
ArrayList<DirectedGraphNode> ans = new ArrayList<>();
//开始遍历入度为0的队列
while(!zeroQueue.isEmpty()){
//弹出入度0的节点,并且入集合
DirectedGraphNode poll = zeroQueue.poll();
ans.add(poll);
//接着需要剔除这个节点,也就是将该节点的邻接点的入度-1
for(DirectedGraphNode next:poll.neighbors){
inMap.put(next,inMap.get(next)-1);
//邻接点入度-1之后 判断入度0的邻接节点 再将节点入队列
if(inMap.get(next) == 0){
zeroQueue.add(next);
}
}
}
return ans;
}
}
**思路2:DFS 深度优先遍历 定义节点类包含节点对象和节点深度
* 1.深度遍历 每个节点的深度 比如最后的节点:没有邻接点的节点 其深度为1 而指向该节点的上层节点,深度为2...
* 得到每个节点对应的深度
* 2.按节点的深度降序排序,然后依次输出节点 就是图拓扑排序
* 要求:有向图且其中没有环
* 应用:事件安排、编译顺序
package class16;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
/**
* 图的拓扑排序算法
* OJ链接:https://www.lintcode.com/problem/topological-sorting
* 描述
* <p>
* 给定一个有向图,图节点的拓扑排序定义如下:
* <p>
* 对于图中的每一条有向边 A -> B , 在拓扑排序中A一定在B之前.
* 拓扑排序中的第一个节点可以是图中的任何一个没有其他节点指向它的节点.
* <p>
* 针对给定的有向图找到任意一种拓扑排序的顺序.
* <p>
* <p>
* 思路:DFS 深度优先遍历 定义节点类包含节点对象和节点深度
* <p>
* 1.深度遍历 每个节点的深度 比如最后的节点:没有邻接点的节点 其深度为1 而指向该节点的上层节点,深度为2...
* 得到每个节点对应的深度
* 2.按节点的深度降序排序,然后依次输出节点 就是图拓扑排序
* <p>
* 要求:有向图且其中没有环
* 应用:事件安排、编译顺序
*/
public class TopologicalOrderDFS1 {
// 不要提交这个类
public static class DirectedGraphNode {
public int label;
public ArrayList<DirectedGraphNode> neighbors;
public DirectedGraphNode(int x) {
label = x;
neighbors = new ArrayList<DirectedGraphNode>();
}
}
//定义一个节点深度类,包含节点类信息,该节点的深度 用来最后按深度降序排序输出 得到图拓扑顺序
public static class NodeDeep{
public DirectedGraphNode node; //题目定义的节点类
public int deep; //该节点所在的深度
public NodeDeep(DirectedGraphNode n,int d){
node = n;
deep = d;
}
}
public static ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph) {
//定义哈希表 DirectedGraphNode:NodeDeep 节点做键,节点深度类做值
HashMap<DirectedGraphNode,NodeDeep> map = new HashMap<>();
for(DirectedGraphNode cur: graph){
//进行递归操作 将每个节点的深度类信息添加好哈希表中
dfs(cur,map);
}
//将哈希表的value遍历保存到集合中
ArrayList<NodeDeep> list = new ArrayList<>();
for(NodeDeep node:map.values()){
list.add(node);
}
//将集合中的深度类信息按深度进行降序,降序完成,按序输出就是图的拓扑顺序
list.sort(new Comparator<NodeDeep>(){
public int compare(NodeDeep o1,NodeDeep o2){
return o2.deep - o1.deep;
}
});
ArrayList<DirectedGraphNode> ans = new ArrayList<>();
for(NodeDeep node:list){
ans.add(node.node);
}
return ans;
}
//深度优先遍历:传入一个节点,以及上面程序定义的一个哈希表记录 这个节点对应的深度
public static NodeDeep dfs(DirectedGraphNode cur,HashMap<DirectedGraphNode,NodeDeep> map){
if(map.containsKey(cur)){
//如果当前哈希表没有记录该节点信息 那么就直接从表中取出返回上层
return map.get(cur);
}
//定义当前类的 初始高度0 后续用来跟其邻接节点深度做比较大小 最终返回
int deep = 0;
//递归遍历邻接节点 取其最大的深度赋值
for(DirectedGraphNode next:cur.neighbors){
deep = Math.max(deep,dfs(next,map).deep);
}
//定义好了这两个信息后就可以创建类返回,注意deep是邻接点深度 需要+1 才是当前节点深度
NodeDeep ans = new NodeDeep(cur,deep+1);
//注意:还需要将这个没有在哈希表记录的节点,添加进去,后续如果又递归到该值就会直接返回 提高效率
map.put(cur,ans);
return new NodeDeep(cur,deep+1);
}
}
* 思路3:DFS 深度优先遍历 定义节点类包含节点对象和节点次数
* 1.深度遍历 每个节点的点次数 比如最后一个节点a时,点次数就是自己一个1,往上一个节点o,它有a,b,c 三个邻接节点 那么它节点次数就是4
* 有指向多个节点的可以重复累计点次
* 2.按节点的点次降序排序,然后依次输出节点 就是图拓扑排序
* 要求:有向图且其中没有环
* 应用:事件安排、编译顺序
package class16;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
/**
* 图的拓扑排序算法
* OJ链接:https://www.lintcode.com/problem/topological-sorting
* 描述
* <p>
* 给定一个有向图,图节点的拓扑排序定义如下:
* <p>
* 对于图中的每一条有向边 A -> B , 在拓扑排序中A一定在B之前.
* 拓扑排序中的第一个节点可以是图中的任何一个没有其他节点指向它的节点.
* <p>
* 针对给定的有向图找到任意一种拓扑排序的顺序.
* <p>
* <p>
* 思路:DFS 深度优先遍历 定义节点类包含节点对象和节点次数
* <p>
* 1.深度遍历 每个节点的点次数 比如最后一个节点a时,点次数就是自己一个1,往上一个节点o,它有a,b,c 三个邻接节点 那么它节点次数就是4
* 有指向多个节点的可以重复累计点次
*
* 2.按节点的点次降序排序,然后依次输出节点 就是图拓扑排序
* <p>
* 要求:有向图且其中没有环
* 应用:事件安排、编译顺序
*/
public class TopologicalOrderDFS2 {
// 不要提交这个类
public static class DirectedGraphNode {
public int label;
public ArrayList<DirectedGraphNode> neighbors;
public DirectedGraphNode(int x) {
label = x;
neighbors = new ArrayList<DirectedGraphNode>();
}
}
//节点次数类
public static class NodeDeep{
public DirectedGraphNode node;
public long size;
public NodeDeep(DirectedGraphNode n,long s){
node = n;
size = s;
}
}
public static ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph) {
//定义一个哈希表,存储节点类对应的节点次数类
HashMap<DirectedGraphNode,NodeDeep> map = new HashMap<>();
//遍历节点集合,刷新节点的节点次数类
for(DirectedGraphNode cur:graph){
dfs(cur,map);
}
//定义一个集合存放哈希表的 节点次数类
ArrayList<NodeDeep> list = new ArrayList<>();
for(NodeDeep node:map.values()){
list.add(node);
}
//进行按次数降序排序, 就是图的拓扑排序
list.sort(new Comparator<NodeDeep>() {
@Override
public int compare(NodeDeep o1, NodeDeep o2) {
//比较器注意 因为是Long类型 所以我们要控制返回0 -1 1
return o1.node == o2.node ? 0 : o1.size - o2.size > 0? -1:1;
}
});
//定义结果集合 将集合中的节点次数类的节点类添加到集合
ArrayList<DirectedGraphNode> ans = new ArrayList<>();
for(NodeDeep node:list){
ans.add(node.node);
}
return ans;
}
//递归将该节点的节点次数往下找返回其次数
public static NodeDeep dfs(DirectedGraphNode cur,HashMap<DirectedGraphNode,NodeDeep>map){
if(map.containsKey(cur)){
//如果哈希表中存在该节点的记录 就直接返回
return map.get(cur);
}
long size = 0; //定义当前节点的节点次数
for(DirectedGraphNode next:cur.neighbors){
//遍历下层邻接点,返回其节点次数累加给当前节点
size += dfs(next,map).size;
}
//定义当前节点的信息,次数要+1当前节点 并返回
NodeDeep ans = new NodeDeep(cur,size+1);
//注意并且要将节点信息添加到哈希表中记录 下次遍历相同节点可以直接返回
map.put(cur,ans);
return ans;
}
}
八、最小生成树算法之Kruskal 克鲁斯卡尔算法
1)总是从权值最小的边开始考虑,依次考察权值依次变大的边
2)当前的边要么进入最小生成树的集合,要么丢弃
3)如果当前的边进入最小生成树的集合中不会形成环,就要当前边
4)如果当前的边进入最小生成树的集合中会形成环,就不要当前边
5)考察完所有边之后,最小生成树的集合也得到了
思路:并查集+小根堆
package class16;
//undirected graph only 无向图
import java.util.*;
/**
* 最小生成树算法之Kruskal
*
* 1)总是从权值最小的边开始考虑,依次考察权值依次变大的边
* 2)当前的边要么进入最小生成树的集合,要么丢弃
* 3)如果当前的边进入最小生成树的集合中不会形成环,就要当前边
* 4)如果当前的边进入最小生成树的集合中会形成环,就不要当前边
* 5)考察完所有边之后,最小生成树的集合也得到了
*
* 思路:
* 1.克鲁斯卡尔是 对路径权重排序,依次取出路径,如果取出路径会使得当前全路径成环,则跳过不选该路径,直到最后点都已经连接通
* 2.采用并查集来解决这个问题 依次从小到大遍历路径 路径两端点不在一个集合的那么就把路径入集合,直到遍历完整个路径
*/
public class Kruskal {
//定义一个并查集,给图使用
public static class UnionFind{
HashMap<NodeTrain,NodeTrain> parent; //哈希表记录 图节点key所在区域的头节点是图节点value
HashMap<NodeTrain,Integer> sizeMap; //哈希表记录 图节点key是所在区域头节点,该区域的大小个数value
public UnionFind(){
//初始化不带参 通过makeSet方法传图节点参数 来填充两个属性的内容
parent = new HashMap<>();
sizeMap = new HashMap<>();
}
//创建图节点初始化的 parent sizeMap 集合数据 参数是图的多个节点 集合类型
public void makeSet(Collection<NodeTrain> nodes){
//一开始清空两表的记录
parent.clear();
sizeMap.clear();
for(NodeTrain node: nodes){
//遍历每个图节点 初始化 父节点为自身 自身集合大小1
parent.put(node,node);
sizeMap.put(node,1);
}
}
//查找图节点的父节点
public NodeTrain findParent(NodeTrain cur){
//定义辅助栈进行存放该图节点所在集合的全部节点 后续用来做节点指向的路径压缩优化,直接指向父节点
Stack<NodeTrain> stack = new Stack<>();
while(cur != parent.get(cur)){
//当前节点不是作为该集合的头节点就一直遍历往上,节点入栈
stack.push(cur);
cur = parent.get(cur);
}
//遍历完成后 cur 节点就是头节点 最后返回,返回前做一个路径压缩优化 将栈中该区域的节点的头节点指向当前这个头节点。后续遍历就能直接去到头节点 节省遍历路径
while(!stack.isEmpty()){
parent.put(stack.pop(),cur);
}
return cur;
}
//判断是否两图节点是一个集合的
public boolean isSameSet(NodeTrain node1,NodeTrain node2){
//两图节点所在区域的头节点 相等则在一个集合
return findParent(node1) == findParent(node2);
}
//进行两图节点的合并
public void union(NodeTrain node1, NodeTrain node2){
if(node1 == null || node2 == null) return;
//取两节点所在区域的头节点 如果不相等 就进行合并
NodeTrain head1 = findParent(node1);
NodeTrain head2 = findParent(node2);
if(head1 != head2){
if(sizeMap.get(head1) >= sizeMap.get(head2)){
//如果1 大于等于2 那么就将小的2节点 挂到1 将父节点指向1 1的集合大小要刷新加上2的大小 再删除2在的集合
parent.put(head2,head1);
sizeMap.put(head1,sizeMap.get(head1)+sizeMap.get(head2));
sizeMap.remove(head2);
}else{
//否则 相反的处理
parent.put(head1,head2);
sizeMap.put(head2,sizeMap.get(head1)+sizeMap.get(head2));
sizeMap.remove(head1);
}
}
}
}
//克鲁斯卡尔获取图的最小生成树路径集合
public static Set<EdgeTrain> kruskalMST(GraphTrain graph) {
//首先 先定义并查集 并将图的全部节点建立好初始状态值
UnionFind unionFind = new UnionFind();
//调用并查集的创建集合的方法,将图的每个节点传入,依次初始各个节点为自己的集合 头节点也为自己
unionFind.makeSet(graph.nodes.values());
//定义一个小根堆,按图的边权重从小到大排序,依次将图的边入小根堆
PriorityQueue<EdgeTrain> heap = new PriorityQueue<>((o1,o2)->(o1.weight-o2.weight));
for(EdgeTrain edgeTrain:graph.edges){
heap.add(edgeTrain);
}
//定义一个边结果集
Set<EdgeTrain> ans = new HashSet<>();
while(!heap.isEmpty()){
//遍历小根堆弹出边,直到没有边 结束循环
EdgeTrain poll = heap.poll();
if(!unionFind.isSameSet(poll.from,poll.to)){
//如果边两节点不是一个集合,那么就将这个边添加到结果边集合中,说明这个路径是我们需要的
ans.add(poll);
//并将两图节点合并成一个集合
unionFind.union(poll.from,poll.to);
}
}
return ans;
}
}
九、最小生成树算法之Prim
1)可以从任意节点出发来寻找最小生成树
2)某个点加入到被选取的点中后,解锁这个点出发的所有新的边
3)在所有解锁的边中选最小的边,然后看看这个边会不会形成环
4)如果会,不要当前边,继续考察剩下解锁的边中最小的边,重复3)
5)如果不会,要当前边,将该边的指向点加入到被选取的点中,重复2)
6)当所有点都被选取,最小生成树就得到了
package class16;
import java.util.HashSet;
import java.util.PriorityQueue;
import java.util.Set;
/**
* 最小生成树算法之Prim
* 1)可以从任意节点出发来寻找最小生成树
* 2)某个点加入到被选取的点中后,解锁这个点出发的所有新的边
* 3)在所有解锁的边中选最小的边,然后看看这个边会不会形成环
* 4)如果会,不要当前边,继续考察剩下解锁的边中最小的边,重复3)
* 5)如果不会,要当前边,将该边的指向点加入到被选取的点中,重复2)
* 6)当所有点都被选取,最小生成树就得到了
*
* 思路:
* 普里姆算法是任意一个点出发,取该点当前权重最小的路径连接,然后再将该路径对端点 的直接路径与前面点的其他未选路径比较选取最小权重,以此类推 直到连接完全部点
* 1.用小根堆将解锁的边保存,用一个set集合存放被解锁的节点
* 2.遍历图的节点 如果不在解锁点集合set 就将其加入,并将该点的边都加入到小根堆中
* 3.接着遍历当前小根堆中的边,依次取出就是最小权重的边,弹出一个边,同时判断如果对端节点不在当前解锁set集合点中,也就是没有被选中的,那就将其点加入set
* 4.此时说明这一条边被我们选择了,因为两端点都入集合,那么就将该边加入到ans结果集合中 ,接着对这个选中的节点的边进行入队列,进行循环3
*
*/
public class Prim {
public static Set<EdgeTrain> primMST(GraphTrain graph){
//定义一个小根堆保存解锁过的边 边按照权重排序 升序
PriorityQueue<EdgeTrain> heap = new PriorityQueue<>((o1,o2)->(o1.weight-o2.weight));
//定义一个有序表保存解锁过的节点
HashSet<NodeTrain> nodeSet = new HashSet<>();
//定义一个边集合用来返回结果
Set<EdgeTrain> ans = new HashSet<>();
//先取任一一个节点,我们就直接遍历取图节点
for(NodeTrain node: graph.nodes.values()){
//如果当前节点没有被解锁,那么就加入点集合
if(!nodeSet.contains(node)){
nodeSet.add(node);
//解锁点的同时,该点的边也需要解锁
for(EdgeTrain edge:node.edges){
heap.add(edge);
}
}
//点解锁,边解锁,接着判断权重小的边,是否可以选择做路径
while(!heap.isEmpty()){
//弹出小根堆的边,依次弹出权重小的
EdgeTrain poll = heap.poll();
//取该边的对端节点,看是否已经被解锁 也就是被遍历过
NodeTrain toNode = poll.to;
if(!nodeSet.contains(toNode)){
//如果没有被解锁,说明可以选择,将点加入点解锁集合中,结果集加入当前弹出的边
nodeSet.add(toNode);
ans.add(poll);
//选择了该对端点,在接着解锁该对端点的边,加入解锁的小根堆边 接着循环弹出小根堆当前最小权重的边,如果对端点是被解锁过的 那么就是跳过逻辑 接着弹出其他的边
for(EdgeTrain edge:toNode.edges){
heap.add(edge);
}
}
}
}
return ans;
}
// 请保证graph是连通图
// graph[i][j]表示点i到点j的距离,如果是系统最大值代表无路
// 返回值是最小连通图的路径之和
//图以二维数组作为参数,返回路径和 不同的数据结构类型
public static int prim(int[][] graph) {
//定义一个点个数,矩阵的长度就是代表个数
int size = graph.length;
//定义一个数组 随机定义graph[0][0]到其他每个点之间的距离
int[] distances = new int[size];
//定义一个布尔数组,判断节点是否被访问
boolean[] visit = new boolean[size];
//visit[0] 假设从0节点出发开始遍历 所以初始值为true
visit[0] = true;
//定义一个结果路径和
int sum = 0;
//遍历二维数组将距离值赋值给distances
for(int i = 0; i<size;i++){
distances[i] = graph[0][i]; // 0-0 0->1 0->2
}
//开始遍历0节点开始的有相连的节点
for(int i = 1;i<size;i++){
//定义当前 路径最小值以及对应的节点索引位置 初始化值
int minIndex = -1;
int minDis = Integer.MAX_VALUE;
for(int j = 0;j<size;j++){
//还是遍历全部节点,依次比较是否是被访问过的,以及0到该节点的距离取最小
//刷新与0节点相连的最小距离以及最小索引节点
if(!visit[j] && distances[j] < minDis){
minIndex = j;
minDis = distances[j];
}
}
//注意 如果假如遍历后 已经没有节点是没有访问过的了 那么就说明已经遍历完成。最后就退出
if(minIndex == -1) return sum;
//如果是还有没有访问的节点 那么就取到了与0 相连的最小路径的节点,接着就是刷新节点的访问状态
visit[minIndex] = true;
//刷新最小路径和
sum += minDis;
//解锁了新节点,接着就是刷新,新节点到的其他路径
for(int j = 0;j<size;j++){
//刷新新节点到其他每个节点的最小路径graph[minIndex][j] min-》0, ->1 ->2.. 这里如果重复遍历到前面的节点也不会出错 因为有visit[]的判断,会跳过那些被访问解锁过的点
if(!visit[j] && distances[j] > graph[minIndex][j]){
distances[j] = graph[minIndex][j];
}
}
}
return sum;
}
}
十、最小生成树题目
Kruskal算法 解
package class16;
// 课上没讲这个实现
// 因为是一样的,都是用Kruskal算法实现最小生成树,只不过是牛客网的测试数据
// 测试链接 : https://www.nowcoder.com/questionTerminal/c23eab7bb39748b6b224a8a3afbe396b
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下所有代码,把主类名改成Main,可以直接通过
/**
* 一个有n户人家的村庄,有m条路连接着。村里现在要修路,每条路都有一个代价,现在请你帮忙计算下,最少需要花费多少的代价,就能让这n户人家连接起来。
* 输入描述:
*
* 输入第一行,两个整数n,m;
* 接下来m行,每行三个整数a,b,c,表示有路连接编号为a和b的人家,修路要花费的代价为c。
* 数据保证能将每户人家都连接起来。
* 注意重边的情况。n≤10000, m≤100,000n≤10000, m≤100,000,边权0<c≤100000<c≤10000。
*
*
*
* 输出描述:输出最小的花费代价使得这n户人家连接起来。
*
*/
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.Arrays;
public class KruskalNowCoder {
public static int MAXM = 100001;
public static int[][] edges = new int[MAXM][3];
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
int n = (int) in.nval;
in.nextToken();
int m = (int) in.nval;
for (int i = 0; i < m; i++) {
in.nextToken();
edges[i][0] = (int) in.nval;
in.nextToken();
edges[i][1] = (int) in.nval;
in.nextToken();
edges[i][2] = (int) in.nval;
}
Arrays.sort(edges, 0, m, (a, b) -> a[2] - b[2]);
build(n);
int ans = 0;
for (int[] edge : edges) {
if (union(edge[0], edge[1])) {
ans += edge[2];
}
}
out.println(ans);
out.flush();
}
}
// 下面是并查集结构
public static int MAXN = 10001;
public static int[] father = new int[MAXN];
public static int[] size = new int[MAXN];
public static int[] help = new int[MAXN];
public static void build(int n) {
for (int i = 1; i <= n; i++) {
father[i] = i;
size[i] = 1;
}
}
private static int find(int i) {
int size = 0;
while (i != father[i]) {
help[size++] = i;
i = father[i];
}
while (size > 0) {
father[help[--size]] = i;
}
return i;
}
// 如果i和j,原本是一个集合,返回false
// 如果i和j,不是一个集合,合并,然后返回true
public static boolean union(int i, int j) {
int fi = find(i);
int fj = find(j);
if (fi != fj) {
if (size[fi] >= size[fj]) {
father[fj] = fi;
size[fi] += size[fj];
} else {
father[fi] = fj;
size[fj] += size[fi];
}
return true;
} else {
return false;
}
}
}
Prim普利姆算法 解
package class16;
// 课上没讲这个实现
// 因为是一样的,都是用Prim算法实现最小生成树,只不过是牛客网的测试数据
// 测试链接 : https://www.nowcoder.com/questionTerminal/c23eab7bb39748b6b224a8a3afbe396b
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下所有代码,把主类名改成Main,可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.ArrayList;
import java.util.PriorityQueue;
public class PrimNowCoder {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
ArrayList<ArrayList<int[]>> graph = new ArrayList<>();
int n = (int) in.nval;
for (int i = 0; i <= n; i++) {
graph.add(new ArrayList<>());
}
in.nextToken();
int m = (int) in.nval;
for (int i = 0; i < m; i++) {
in.nextToken();
int A = (int) in.nval;
in.nextToken();
int B = (int) in.nval;
in.nextToken();
int cost = (int) in.nval;
graph.get(A).add(new int[] { B, cost });
graph.get(B).add(new int[] { A, cost });
}
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[1] - b[1]);
boolean[] visited = new boolean[n + 1];
for (int[] edge : graph.get(1)) {
heap.add(edge);
}
visited[1] = true;
int ans = 0;
while (!heap.isEmpty()) {
int[] edge = heap.poll();
int next = edge[0];
int cost = edge[1];
if (!visited[next]) {
visited[next] = true;
ans += cost;
for (int[] e : graph.get(next)) {
heap.add(e);
}
}
}
out.println(ans);
out.flush();
}
}
}
十一、Dijkstra算法
1)Dijkstra算法必须指定一个源点
2)生成一个源点到各个点的最小距离表,一开始只有一条记录,即原点到自己的最小距离为0,源点到其他所有点的最小距离都为正无穷大
3)从距离表中拿出没拿过记录里的最小记录,通过这个点发出的边,更新源点到各个点的最小距离表,不断重复这一步
4)源点到所有的点记录如果都被拿过一遍,过程停止,最小距离表得到了
package class16;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**Dijkstra算法
* 1)Dijkstra算法必须指定一个源点
* 2)生成一个源点到各个点的最小距离表,一开始只有一条记录,即原点到自己的最小距离为0,源点到其他所有点的最小距离都为正无穷大
* 3)从距离表中拿出没拿过记录里的最小记录,通过这个点发出的边,更新源点到各个点的最小距离表,不断重复这一步
* 4)源点到所有的点记录如果都被拿过一遍,过程停止,最小距离表得到了
*求从一个出发点,到所有点的最小距离,所有的信息都要
* 求两点的最短路径,不用包含图全部节点,从开始节点取最小权重的直接路径,然后再接着看对端节点的直接路径与前面节点未选的路径比较取最小权重路径,以此类推 直到最后点都全部遍历的 已选的路径即为最短路径
*
*
*/
public class Dijkstra {
//给定图中的一个节点,返回以该节点到其他节点的最小路径 key 节点: value from节点到key节点的最小路径
public static HashMap<NodeTrain,Integer> dijkstra1(NodeTrain from){
//定义一个结果集,存放from节点 到其他节点的最小路径
HashMap<NodeTrain,Integer> disMap = new HashMap<>();
//首先将自身到自身的距离键值对加到哈希表中
disMap.put(from,0);
//定义一个集合,表示已经访问过的节点集合
HashSet<NodeTrain> nodeSet = new HashSet<>();
//取当前哈希表中路径最小的节点
NodeTrain minNode = getminNode(disMap, nodeSet);
while(minNode != null){
//先获取下该最小节点的路径
Integer dis = disMap.get(minNode);
//然后就遍历该节点的全部边,取出对端点
for(EdgeTrain edge : minNode.edges){
NodeTrain toNode = edge.to;
if(!disMap.containsKey(toNode)){
//如果距离结果集合没有这个对端点 那么就需要添加进去,长度就是前一个节点距离dis+该边的权重
disMap.put(toNode,dis + edge.weight);
}else {
//如果有这个对端点 就需要刷新 是否会有更短距离值
disMap.put(toNode, Math.min(disMap.get(toNode),dis+edge.weight));
}
}
//最后再将当前取出来的最小节点加入 访问节点集合 然后最小节点接着从getmin方法中取
nodeSet.add(minNode);
minNode = getminNode(disMap,nodeSet);
}
return disMap;
}
//从disMap中找最小路径的节点,且这些节点是没有访问过的 没有存在nodeSet
//同时刷新路径,如果是哈希表中存在的,那么就判断表中该节点路径 是否大于当前的新路径 取较小值刷新路径
public static NodeTrain getminNode(HashMap<NodeTrain,Integer> disMap,HashSet<NodeTrain> nodeSet){
//定义一个最小节点用于返回结果
NodeTrain ans = null;
//定义最小值
int minDis = Integer.MAX_VALUE;
for(Map.Entry<NodeTrain,Integer>map:disMap.entrySet()){
NodeTrain key = map.getKey();
Integer value = map.getValue();
if(!nodeSet.contains(key) && value < minDis){
//如果节点没有被访问过并且节点路径小于当前的最小路径值 那么就加入 set节点集合
ans = key;
minDis = value;
}
}
return ans;
}
/**
* 方法二优化:
* 前面的方法getminNode 每次都是需要进行遍历比较取该集合当前路径最小的节点,复杂度高,
* 可以通过 加强堆来实现,为什么用加强堆来做小根堆排序,系统自带的优先队列,对于已经入集合的元素,
* 如果后续再改动值属性 而且属性是作为排序的判断。这个时候改完可能会影响排序,但系统的是不会对
* 已经进入堆的元素再次排序的,所以题中 我们每个节点到头节点的最小距离都是会动态刷新的,那么就要用
* 加强堆,来令他多次修改可重排序后再次排序
*/
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);
}
//节点是否入过堆并且是否已经使用过了 弹出堆了 值为-1 表示我们弹出 但不会remove 避免会重复避免到 所以要保留 同时给值-1特殊处理
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;
}
}
十二、LeetCode 网络延迟时间
![](https://i-blog.csdnimg.cn/blog_migrate/3f4759b5fbd9b38b4837b59cbbe56cbb.png)
![](https://i-blog.csdnimg.cn/blog_migrate/73a3410bbf6156fd70036d64e5ad6d1d.png)
package class16;
// leetcode 743题,可以用这道题来练习Dijkstra算法
// 测试链接 : https://leetcode.com/problems/network-delay-time
import java.lang.reflect.Array;
import java.util.*;
/**
* 有 n 个网络节点,标记为 1 到 n。
* <p>
* 给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。
* <p>
* 现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。
* <p>
* 思路:
* 1.数据转换:定义一个集合list<list<int[]>> 外层list 索引表示ui 源节点 里面元素int[0]表示vi目标节点 int[1]表示wi时间
* 即list.get(1) 就表示源节点1 值对应就是 源节点1 指向的多个目标节点 时间集合 [[2,2],[4,2]] 1->2 1->4
* 2.定义小根堆,按时间排序 先添加 指定的k节点 距离0 、布尔数组判断是否遍历过节点、num判断遍历个数是否到n个、max最大值取从k节点到每个节点的最长距离
* 3.遍历小根堆 如果堆非空,并且num < n 就执行遍历节点 弹出堆首节点 取出目标节点信息和时间 如果遍历过就跳过 没有则加入判断数组,num++ 刷新最大值
* 遍历该节点的指向下个节点集合信息。 依次入堆 再循环反复 最后如果跳出循环 num<n说明是没有联通全部节点 返回-1 返回就是返回max
*/
public class NetworkDelayTime {
public static int networkDelayTime(int[][] times, int n, int k) {
//定义一个集合 索引表示目标节点 而元素也是一个集合 表示目标节点指向有多个节点的情况
List<List<int[]>> nexts = new ArrayList<>();
//注意需要将内层的list依次初始化。 共有n个点 而且点是从1 开始的 所以就是0-n n+1个 第一个0空着
for (int i = 0; i <= n; i++) {
nexts.add(new ArrayList<>());
}
//将times数组转换到nexts中 times中的元素假如time第一个元素是[1,2,3] 我们就将源节点1,time[0] 作为nexts索引坐标。 nexts.get(1) 值就是[2,3]
for (int[] time : times) {
//将目标节点和时间作为数组元素存放到以源节点做索引的集合中
nexts.get(time[0]).add(new int[]{time[1], time[2]});
}
//定义一个小根堆,存放数组 [x节点,从k节点到x节点的距离] 根据距离做排序
PriorityQueue<int[]> heap = new PriorityQueue<>(new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[1] - o2[1];
}
});
//将初始节点k值赋值,k节点到k节点 距离0
heap.add(new int[]{k, 0});
//定义一个数组 判断每个节点是否选取过了
boolean[] used = new boolean[n + 1];
//定义num 遍历的节点个数 max遍历整体图的最长时间
int num = 0;
int max = 0;
//开始遍历小根堆 直到堆空且遍历的个数num==n就跳出
while (!heap.isEmpty() && num < n) {
//弹出堆中当前最小时间的节点
int[] poll = heap.poll();
int curNode = poll[0]; //k指向的某个节点
int time = poll[1]; //k指向的某个节点之间的时间
if (used[curNode]) {
//如果该节点前面已经访问过 那就直接弹出跳过
continue;
}
//没有访问过 那就访问值刷新 num个数刷新 时间刷新
used[curNode] = true;
num++;
max = Math.max(max, time); //max刷新 与当前节点time到k节点的时间比较 取较大
//接着判断该节点的指向节点 目标节点
for (int[] next : nexts.get(curNode)) {
//将它的目标节点和对应的距离依次入堆,判断下次堆中取时间最小的再循环弹出,注意距离要把当前节点到k的时间ime 加上
heap.add(new int[]{next[0], time + next[1]});
}
}
//返回值注意,假网络节点不连通 那么跳出循环后num个数是没有遍历完n个的。就表示不能包含全部节点 按要求返回-1 否则就返回max值 max值就是每次遍历比较取k节点到某个节点时间最大
return num < n ? -1 : max;
}
// 方法二 : 加强堆的解法
public static int networkDelayTime2(int[][] times, int n, int k) {
//定义list集合 存储list<int[]> 多个目标节点时间信息
List<List<int[]>> nexts = new ArrayList<>();
for (int i = 0; i <= n; i++) {
//初始化 依次要创建集合先 避免出现空指针异常
nexts.add(new ArrayList<>());
}
for (int[] time : times) {
//将tiems数据转换到集合中 list.get(i) i索引表示起始节点 对应的元素是list<int[]> 多组指向 目标节点时间
nexts.get(time[0]).add(new int[]{time[1], time[2]});
}
//创建一个加强堆类 初始添加题目给的k节点开始 到k节点距离0
Heap heap = new Heap(n);
heap.add(k, 0);
int max = 0;
int num = 0;
while (!heap.isEmpty()) {
//弹出堆节点 取其连接节点和时间
int[] poll = heap.poll();
int cur = poll[0];
int time = poll[1];
//刷新节点个数以及最大值
num++;
max = Math.max(max, time);
//遍历其cur节点连接的其他目标节点集合 依次入堆, 时间需要注意加上当前节点到k节点的时间time
for (int[] ints : nexts.get(cur)) {
heap.add(ints[0], ints[1] + time);
}
}
return num < n ? -1 : max;
}
//定义一个加强堆
public static class Heap {
public boolean[] used; //判断节点是否被访问过 长度n+1 节点是从1开始的 没有0
public int[][] heap; //二维数组 外层表示索引位置,size, 内层表示节点信息和时间信息
public int[] hIndex; //int[cur]=size 表示节点信息索引对应的heap堆的位置
public int size; //堆大小
public Heap(int n) { //构造函数传递n 个节点
used = new boolean[n + 1];
heap = new int[n + 1][2]; //外层n+1个节点 对应的指向目标节点和时间信息的一维数组
hIndex = new int[n + 1]; //节点的索引位置 初始化为-1
Arrays.fill(hIndex, -1);
size = 0;
}
//添加目标节点 时间入堆
public void add(int cur, int time) {
if (used[cur]) return; //如果节点已经使用,那么就不添加 跳过
if (hIndex[cur] == -1) {
//节点索引值-1 说明还没遍历过 进行添加数据信息
heap[size][0] = cur; //堆当前位置heap[size] 添加一组目标节点cur time时间
heap[size][1] = time;
hIndex[cur] = size; //堆反向索引数组 节点cur 对应的位置size
heapInsert(size++); //入堆操作,注意heap外层数组就是size堆索引顺序
} else {
//如果索引值不为-1 说明之前已经访问过,hIndex[cur]取出已在的位置,将其对应的当前时间heap[index][1] 与目前新添加的cur新时间time比较
//如果time 小于 当前的值,那么就刷新time赋值 然后hIndex[cur]该位置index就需要进行向上调整 是堆依旧有序
int index = hIndex[cur];
if (time < heap[index][1]) {
heap[index][1] = time;
heapInsert(index);
}
}
}
//开始弹出目标节点
public int[] poll() {
//取出堆顶元素 以及对应的目标节点信息
int[] ints = heap[0];
//先将堆顶与尾交换同时堆元素-1 并进行下沉操作
swap(0, --size);
heapify(0);
//然后调整好顺序后 再刷新弹出节点的used访问值和索引 该节点选取使用 true, 索引重新回到-1
int cur = ints[0];
used[cur] = true;
hIndex[cur] = -1;
return ints;
}
public boolean isEmpty() {
return size == 0;
}
//堆上沉操作
public void heapInsert(int cur) {
int parent = (cur - 1) / 2;
//父节点小于当前节点 就交换 往上走
while (heap[cur][1] < heap[parent][1]) {
swap(cur, parent);
cur = parent;
parent = (cur - 1) / 2;
}
}
//堆下沉操作
public void heapify(int cur) {
int left = (cur *2) +1;
//左子节点 不超过堆大小 循环下沉操作
while (left < size) {
//比较左右子节点 取最小子节点
int min = (left + 1) < size && heap[left + 1][1] < heap[left][1] ? left + 1 : left;
//较小子节点与cur节点比较 刷新最小节点
min = heap[min][1] < heap[cur][1] ? min : cur;
//最小节点如果就是cur 就说明不用再下沉 直接退出
if (min == cur) break;
//子节点小于当前节点 就交换 cur下沉到min left节点下沉到cur的左节点
swap(min, cur);
cur = min;
left = (cur *2) +1;
}
}
public void swap(int i, int j) {
//交换两节点排序 取heap[]进行交换
int[] inti = heap[i];
int[] intj = heap[j];
heap[i] = intj;
heap[j] = inti;
//索引数组 对inti[0]节点也要同步交换
int hIndexi = hIndex[inti[0]];
int hIndexj = hIndex[intj[0]];
hIndex[inti[0]] = hIndexj;
hIndex[intj[0]] = hIndexi;
}
}
}