1. 图
- 由点的集合和边的集合构成
- 虽然存在有向图和无向图的概念,但实际上都可以用有向图来表达
- 边上可能带有权值
2. BFS
宽度优先遍历,使用队列来进行层的遍历。
- 利用队列实现
- 从源节点开始依次按宽度进队列,然后弹出
- 每弹出一个点,把该节点所有没有进过队列的邻接电放入队列
- 直到队列变空
public static void bfs(Node node) {if (node == null) {return;}LinkedList<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);for (Node next : cur.nexts) {// 如果当前元素没有进入过队列if (!set.contains(node)) {set.add(next);queue.add(next);}}}}
3. 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.vaule);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.vaule); // 输出当前子元素值break; // 结束当前根节点的子元素遍历}}}}
4. 图的拓扑排序
拓扑排序是对一个有向图构造拓扑序列,解决工程是否能顺利进行的问题。构造时有 2 种结果:
- 此图全部顶点被输出:说明说明图中无「环」存在, 是 AOV 网
- 没有输出全部顶点:说明图中有「环」存在,不是 AOV 网
AOV(Activity On Vertex Network) :一种 有向 无回路 的图

如下面的DAG图
- 从 DAG 图中选择一个 没有前驱(即入度为0)的顶点并输出。
- 从图中删除该顶点和所有以它为起点的有向边。
- 重复 1 和 2 直到当前的 DAG 图为空或当前图中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
于是,得到拓扑排序后的结果是 { 1, 2, 4, 3, 5 }。
通常,一个有向无环图可以有一个或多个拓扑排序序列。
拓扑排序常用来确定一个依赖关系集中,事物发生的顺序。例如,在日常工作中,可能会将项目拆分成A、B、C、D四个子部分来完成,但A依赖于B和D,C依赖于D。为了计算这个项目进行的顺序,可对这个关系集进行拓扑排序,得出一个线性的序列,则排在前面的任务就是需要先完成的任务。
注意:这里得到的排序并不是唯一的!就好像你早上穿衣服可以先穿上衣也可以先穿裤子,只要里面的衣服在外面的衣服之前穿就行。

如果当前节点入度为则说明此节点是首次读取,入栈并存储到结果集合中
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 class Graph {public HashMap<Integer, Node> nodes;// key存储为value value存储为Node节点public HashSet<Edge> edges;public Graph() {nodes = new HashMap<>();edges = new HashSet<>();}}public static List<Node> sortedTopology(Graph graph) {// key为节点 value为剩下的入度HashMap<Node, Integer> inMap = new HashMap<>();// 只有剩下入度为0的点,才进入队列中LinkedList<Node> queuq = new LinkedList<>();for (Node node : graph.nodes.values()) {inMap.put(node, node.in); // 存储每个节点的入度if (node.in == 0) {// 入队列queuq.add(node);}}List<Node> result = new ArrayList<>();while (!queuq.isEmpty()) {Node cur = queuq.poll();result.add(cur);for (Node node : cur.nexts) {// 更新他的邻居的入度inMap.put(node, inMap.get(node.value) - 1);if (inMap.get(node) == 0) {// 更新后为0 入队列queuq.add(node);}}}return result;}
4.1. 127 · 拓扑排序
4.1.1. DFS解法1
统计每个节点后面共有多少连通的节点,包括邻接节点的数量,如果两节点作比较数量较大的应该拓扑序在前,数量较小的拓扑序在后。
public static class DirectedGraphNode {public int label;public ArrayList<DirectedGraphNode> neighbors;public DirectedGraphNode(int x) {label = x;neighbors = new ArrayList<DirectedGraphNode>();}}public static class info {DirectedGraphNode node; //当前节点long nodes; //当前节点连同节点的数量public info(DirectedGraphNode node, long nodes) {super();this.node = node;this.nodes = nodes;}}public ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph) {// key为节点 value为节点和节点数包装类HashMap<DirectedGraphNode, info> map = new HashMap<>();//遍历拓扑结构 得到info信息存储到map中for (DirectedGraphNode directedGraphNode : graph) {f(directedGraphNode, map);}//存储所有info信息放入集合ArrayList<info> recordArr = new ArrayList<>();for (info info : map.values()) {recordArr.add(info);}//进行排序 节点数量多放前 少放后Collections.sort(recordArr, new Comparator<info>() {@Overridepublic int compare(info o1, info o2) {// 无法强制成int 强转后出错return o1.nodes == o2.nodes ? 0 : (o1.nodes > o2.nodes ? -1 : 1);}});//结果集合ArrayList<DirectedGraphNode> res = new ArrayList<>();for (info info2 : recordArr) {res.add(info2.node);}return res;}// 返回当前节点 所有连同节点的个数的mapprivate info f(DirectedGraphNode directedGraphNode, HashMap<DirectedGraphNode, info> map) {if (map.containsKey(directedGraphNode)) {// 如果之前有数据 则直接取出return map.get(directedGraphNode);}// 之前没算过long nodes = 0;for (DirectedGraphNode next : directedGraphNode.neighbors) {nodes += f(next, map).nodes; // 递归}info ans = new info(directedGraphNode, nodes + 1);// 更新存储数据map.put(directedGraphNode, ans);return ans;}
4.1.2. DFS解法2
我们求出每个节点的最大深度,根据每个节点的深度进行排序,深度大的放前,深度小的放后。
public static class DirectedGraphNode {public int label;public ArrayList<DirectedGraphNode> neighbors;public DirectedGraphNode(int x) {label = x;neighbors = new ArrayList<DirectedGraphNode>();}}public static class info {DirectedGraphNode node; // 当前节点int deep; // 当前节点的最大深度public info(DirectedGraphNode node, int deep) {super();this.node = node;this.deep = deep;}}public ArrayList<DirectedGraphNode> topSort(ArrayList<DirectedGraphNode> graph) {HashMap<DirectedGraphNode, info> map = new HashMap<>();for (DirectedGraphNode node : graph) {f(node, map); // 求每个节点的深度}ArrayList<info> recordArr = new ArrayList<>();for (info info : map.values()) {recordArr.add(info);}Collections.sort(recordArr, (o1, o2) -> (o2.deep - o1.deep));ArrayList<DirectedGraphNode> ans = new ArrayList<>();for (info info : recordArr) {ans.add(info.node);}return ans;}private info f(DirectedGraphNode node, HashMap<DirectedGraphNode, info> map) {if (map.containsKey(node)) {// 有记录直接返回return map.get(node);}int follow = 0;for (DirectedGraphNode next : node.neighbors) {follow = Math.max(follow, f(next, map).deep);}info ans = new info(node, follow + 1); // 深度加上自身map.put(node, ans);return ans;}
5. 最小生成树
5.1. Kruskal
克鲁斯卡尔算法查找最小生成树的方法是:将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。
举个例子,图 1 是一个连通网,克鲁斯卡尔算法查找图 1 对应的最小生成树,需要经历以下几个步骤:

将连通网中的所有边按照权值大小做升序排序:

从 B-D 边开始挑选,由于尚未选择任何边组成最小生成树,且 B-D 自身不会构成环路,所以 B-D 边可以组成最小生成树。

D-T 边不会和已选 B-D 边构成环路,可以组成最小生成树:

C-B 边会和已选 C-D、B-D 边构成环路,因此不能组成最小生成树:

- 直到生成所有点 或者 权值大小集合遍历完则生成完成最小生成树

- 总是从取权值最小的边开始考虑
- 当前边要么进入最小生成树的集合,要么丢弃
- 如果当前的边进入最小生成树的集合中不会形成环,就要当前边
- 如果当前的边进入最小生成树的集合中会形成环,就不要当前边
- 考察完所有边之后,最小生成树的集合也得到了
模板写法:
- 并查集
- 权值集合最小堆/进行排序
- 查询当前权值的两节点是否连同如无连同则连同,并合并并查集
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 class Graph {public HashMap<Integer, Node> nodes;// key存储为value value存储为Node节点public HashSet<Edge> edges;public Graph() {nodes = new HashMap<>();edges = new HashSet<>();}}public static class UnionFind {// 每个节点当前所在集合的父节点HashMap<Node, Node> fatherMap;// 每个节点当前所在集合的sizeHashMap<Node, Integer> sizeMap;public UnionFind() {fatherMap = new HashMap<>();sizeMap = new HashMap<>();}// 初始化public void makeSet(Collection<Node> nodes) {fatherMap.clear();sizeMap.clear();for (Node node : nodes) {fatherMap.put(node, node);sizeMap.put(node, 1);}}// 查找父节点+路径压缩public Node find(Node node) {Stack<Node> path = new Stack<>();while (node != fatherMap.get(node)) {path.add(node);node = fatherMap.get(node);}while (!path.isEmpty()) {fatherMap.put(path.pop(), node);}return node;}public boolean isSameSet(Node a, Node b) {return find(a) == find(b);}public void union(Node a, Node b) {if (a == null || b == null) {return;}Node f1 = find(a);Node f2 = find(b);if (f1 != f2) {Integer f1Size = sizeMap.get(f1);Integer f2Size = sizeMap.get(f2);if (f1Size >= f2Size) {fatherMap.put(b, a);//更新父节点sizeMap.put(a, f1Size+f2Size); //合并大小fatherMap.remove(b); //从并查集集合中移除合并后的节点} else {fatherMap.put(b, a);sizeMap.put(b, f2Size+f1Size);fatherMap.remove(a);}}}}public static Set<Edge> kruskalMST(Graph graph){UnionFind unionFind = new UnionFind();unionFind.makeSet(graph.nodes.values()); //初始化并查集//最小堆 以权值排序PriorityQueue<Edge> priorityQueue = new PriorityQueue<>((o1,o2) -> (o1.weight - o2.weight));for (Edge edge : graph.edges) {priorityQueue.add(edge); //加入堆中}Set<Edge> result = new HashSet<>();while(!priorityQueue.isEmpty()) {Edge edge = priorityQueue.poll();//查看 from 和 to 两节点是否在一个集合中 如之前合并过则不进行操作 从堆中弹出if(!unionFind.isSameSet(edge.from, edge.to)) {result.add(edge); //添加到最小生成树结果集合中unionFind.union(edge.from, edge.to); //合并两节点}}return result;}
5.2. Prim
- 寻找图中任意点,以它为起点,它的所有边V加入集合(优先队列)
q1,设置一个boolean数组bool[]标记该位置已经确定。 - 从集合q1找到距离最小的那个边
v1并判断边另一点p是否被标记(访问),如果p被标记说明已经确定那么跳过,如果未被标(访问)记那么标记该点p,并且与p相连的未知点(未被标记)构成的边加入集合q1,边v1(可以进行计算距离之类,该边构成最小生成树) . - 重复1,2直到q1为空,构成最小生成树 !


因为prim从开始到结束一直是一个整体在扩散,所以不需要考虑两棵树合并的问题,在这一点实现上稍微方便了一点。
当然,要注意的是最小生成树并不唯一,甚至同一种算法生成的最小生成树都可能有所不同,但是相同的是无论生成怎样的最小生成树:
- 能够保证所有节点连通(能够满足要求和条件)
- 能够保证所有路径之和最小(结果和目的相同)
- 最小生成树不唯一,可能多样的如下

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 class Graph {public HashMap<Integer, Node> nodes;// key存储为value value存储为Node节点public HashSet<Edge> edges;public Graph() {nodes = new HashMap<>();edges = new HashSet<>();}}public static Set<Edge> primMST(Graph graph) {// 解锁的边 放人小根堆中PriorityQueue<Edge> queue = new PriorityQueue<>((o1, o2) -> (o1.weight - o2.weight));// 有哪些点被解锁过 已经被连同过的HashSet<Node> nodeSet = new HashSet<>();// 最小生成树节点顺序集合HashSet<Edge> reslut = new HashSet<>();// 从任意点开始for (Node node : graph.nodes.values()) {// 如果当前点未被使用过if (!nodeSet.contains(node)) {nodeSet.add(node); // 添加到使用过的集合// 遍历它的邻接点 加入到小根堆中for (Edge edge : node.edges) {queue.add(edge);}while (!queue.isEmpty()) {Edge edge = queue.poll(); // 弹出解锁边中 最小Node toNode = edge.to; // 最小权值的 to节点if (!nodeSet.contains(toNode)) { // 该节点没有被解锁过 即使用当前最小边的节点 不会形成环nodeSet.add(toNode); // 加到解锁节点的集合reslut.add(edge); // 结果集合for (Edge nextEdge : toNode.edges) {queue.add(nextEdge); // 将当前的新解锁节点的邻接节点放入小根堆中}}}}break;}return reslut;}
6. 单元最短路径算法Dijkstra
狄克斯特拉算法是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。
狄克斯特拉算法默认为无负值权值边,如果多个节点成环,并且多个权值的和为负值的会出现无法得出最短路径。
假设v1为源点,找从v1到其它节点的最短路径

- 集合S用来存储已经找到的最短路径
- v1到自己显然最短,故为初始最短路径

第一轮:从v1出发,计算v1到其它节点的距离(无法连接则用“无穷”符号)
- 全表找最小值,发现v1到v6最短为3
- S中添加一条最短路径:v1——v6
- v6列标红不再考虑

第二轮:从v1——v6出发,计算v1通过v6到其它节点的距离
已知v1到v6为3;v6可以到v2,v4,v5;因此,v1通过v6到其它节点的距离为3+n,n为6到各节点的距离

- 在全表中找最小值(v6列已经删除),此时4为最小值,对应路径v1——v6——v5
- 添加最短路径v1——v6——v5
- v5列不再考虑
第三轮:从v1——v6——v5出发,计算v1通过v6及v5到其它节点的距离

第四轮:从v1——v6——v2出发,计算v1通过v6及v2到其它节点的距离

第五轮:从v1——v6——v4出发,计算v1通过v6及v4到其它节点的距离

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 class Graph {public HashMap<Integer, Node> nodes;// key存储为value value存储为Node节点public HashSet<Edge> edges;public Graph() {nodes = new HashMap<>();edges = new HashSet<>();}}public static HashMap<Node, Integer> dijkstra1(Node node) {// key为节点 value为从头节点出单源最短路径值HashMap<Node, Integer> map = new HashMap<>();map.put(node, 0); // 自身到自身为0// 存储已经是最短路径的节点HashSet<Node> selectNode = new HashSet<>();Node minNode = getMinDistanceAndUnselectedNode(map, selectNode); // 获取没有被标记过 并且是当前单源最短路径的最小值 目前为node自身 为权值0while (minNode != null) {// 获取原始点 到 minNode 的最小权值距离Integer distance = map.get(minNode);// 遍历当前minNode所有边for (Edge edge : minNode.edges) {Node toNode = edge.to; // 边指向的节点if (!map.containsKey(toNode)) {// 说明当前指向的节点没有来过 更新为当前minNode的权值 + 当前边权值map.put(toNode, distance + edge.weight);} else {// 之前有记录 查看是否能更新为最小值map.put(toNode, Math.min(map.get(toNode), distance + edge.weight));}}selectNode.add(minNode); // 查找到去往下一个节点的最短路径 将当前的出发节点 打上标记 存储到已经是最短路径的节点集合minNode = getMinDistanceAndUnselectedNode(map, selectNode); // 获取没有被标记过 并且是当前单源最短路径的最小值的节点}return map;}public static Node getMinDistanceAndUnselectedNode(HashMap<Node, Integer> map, HashSet<Node> touchedNodes) {Node minNode = null;int minDistance = Integer.MAX_VALUE;// 遍历单源最短路径节点和值 散列集合for (Entry<Node, Integer> entry : map.entrySet()) {Node node = entry.getKey();Integer distance = entry.getValue();// 如果当前节点没有被标记为过已经是最短路径 并且当前权值是最小if (!touchedNodes.contains(node) && distance < minDistance) {minNode = node;minDistance = distance;}}return minNode;}
6.1. 加强堆优化
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 class Graph {public HashMap<Integer, Node> nodes;// key存储为value value存储为Node节点public HashSet<Edge> edges;public Graph() {nodes = new HashMap<>();edges = new HashSet<>();}}public static class NodeRecord {Node node; // 节点int distance; // 开始源节点到当前节点最短路径的取值和public NodeRecord(Node node, int distance) {this.node = node;this.distance = distance;}}public static class NodeHeap {// 堆Node[] nodes;HashMap<Node, Integer> heapIndexMap; // 当前节点在小根堆数组的位置HashMap<Node, Integer> distanceMap; // 开始源节点到当前节点最短路径的取值和int size;public NodeHeap(int size) {this.size = size;nodes = new Node[size]; // 堆数组heapIndexMap = new HashMap<>();distanceMap = new HashMap<>();}public boolean isEmpty() {return size == 0;}// 判断当前节点是否在堆中 从索引表中查找public boolean isEnterd(Node node) {return heapIndexMap.containsKey(node);}// 判断当前节点是否被标记过为最短路径了 即之前成为过堆顶并被弹出过 如被弹过索引表中记录值应为-1public boolean inHeap(Node node) {// 在堆中 并且没有被标记为最短路径return isEnterd(node) && heapIndexMap.get(node) != -1;}// 加入堆中public void addOrUpdateOrigonre(Node node, int dijkstra) {// 更新节点 即存在与堆中 并且没有为标记为最短路径if (inHeap(node)) {// 更新为更小的权值点distanceMap.put(node, Math.min(distanceMap.get(node), dijkstra));}}// 往上移动public void insertHeapify(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 = index * 2 + 1;while (left < size) {// 查找当前节点左右孩子的最小节点 判断是否能下沉int small = left + 1 < size && distanceMap.get(nodes[left + 1]) < distanceMap.get(nodes[left])? left + 1: left;// 左右孩子的最小节点 小于 当前下标small = distanceMap.get(nodes[small]) < distanceMap.get(index) ? small : index;if (small == index) {break; // 当前自身已经是最小的 无法下沉}swap(index, small);index = small;left = index * 2 + 1;}}// 弹出堆顶 最小值public NodeRecord pop() {// 封装节点 堆顶节点 最小路径和NodeRecord nodeRecord = new NodeRecord(nodes[0], distanceMap.get(nodes[0]));swap(0, size - 1); // 将堆顶交换到堆数组尾部heapIndexMap.put(nodes[size - 1], -1); // 将节点的索引表更新为-1distanceMap.remove(nodes[size - 1]); // 不再保存 当前节点的最短路径和nodes[size - 1] = null; // 释放节点heapify(0, --size); // 下沉交换的元素 即当前堆顶return nodeRecord;}// 交换元素private void swap(int index1, int index2) {heapIndexMap.put(nodes[index1], index2); // 更新索引表heapIndexMap.put(nodes[index2], index1);Node temp = nodes[index1];nodes[index1] = nodes[index2];nodes[index2] = temp;}}/*** 加强堆实现的迪杰斯克拉算法** @param head 开始源节点* @param size 一共有多少节点* @return 返回为散列集合 key为节点 value为从开始源节点到当前节点最短路径的取值和*/public static HashMap<Node, Integer> dijkstra(Node head, int size) {NodeHeap nodeHeap = new NodeHeap(size);nodeHeap.addOrUpdateOrigonre(head, 0); // 源节点入加强堆 并且最短路径权值为0 自身到自身为0HashMap<Node, Integer> result = new HashMap<>(); // key为节点 value为从源节点到自身节点的最小路径权值和while (!nodeHeap.isEmpty()) {NodeRecord record = nodeHeap.pop(); // 弹出堆顶Node cur = record.node; // 当前节点int distance = record.distance; // 当前节点的最小路径权值和// 遍历当前节点所有邻接节点for (Edge edge : cur.edges) {// edge.to去往的节点 edge.weight为去往路径边的权值 要加上当前节点的最小路径和nodeHeap.addOrUpdateOrigonre(edge.to, edge.weight + distance);}result.put(cur, distance);}return result;}
