目录
图的存储方式
图的存储方式有很多,在很多题目中会给出不同种类的图的表示方式,建议大家自己使用一种自己熟练的图的存储表达方式实现图的各种算法,然后遇到新的题目的图的表达方式时,只需要写一个接口程序,将新的表达方式转换为自己常用的表达方式,其它的各种算法的操作就可以直接实现出来。
//先定义一个图
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;//从node节点出发,直接发散出去的点
public ArrayList<Edge> edges;//从node出去的边,属于node的边
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 static Graph createGraph(Integer[][] matrix) {//将一个数组的表达方式转换成上述熟悉的表达方式
Graph graph = new Graph();
for (int i = 0; i < matrix.length; i++) {
Integer weight = matrix[i][0];//将每一行的点权重提取出来
Integer from = matrix[i][1];//将每一行的点从那个点出来的路径提取出来
Integer to = matrix[i][2];//将每一行的点进去那个点的路径提取出来
if (!graph.nodes.containsKey(from)) {//通过哈希表查询,如果上面的from城市没有出现过
graph.nodes.put(from, new Node(from));//在图的点集中将from这个点新建出来,放到点集里面
}
if (!graph.nodes.containsKey(to)) {//同样将没有的to新建出来
graph.nodes.put(to, new Node(to));
}
Node fromNode = graph.nodes.get(from);//将图中的from点拿出来
Node toNode = graph.nodes.get(to);//将图中的to点拿出来
Edge newEdge = new Edge(weight, fromNode, toNode);//把新的边建立好
fromNode.nexts.add(toNode);//将新建立好的边的from节点的nexts中添加上toNode
fromNode.out++;//从from出来的,from的出度加1
toNode.in++;//从from出来指向to,to的入度加1
fromNode.edges.add(newEdge);//将新建立的边添加到from已经有的边集中
graph.edges.add(newEdge);//将新建立的边放到边集里面
}
return graph;
}
图的遍历
图的宽度优先遍历
图的宽度优先遍历利用队列实现,从源节点开始依次按照宽度进队列,同时将进入队列的节点使用哈希表记录下来,然后弹出,每弹出一个点,把该节点所有没有进过队列的邻接点放入队列中,直到队列变空。
public static void bfs(Node node) {
if (node == null) {
return;
}
Queue<Node> queue = new LinkedList<>();
HashSet<Node> map = new HashSet<>();//哈希表用来保存进过队列的节点,避免重复
queue.add(node);
map.add(node);
while (!queue.isEmpty()) {
Node cur = queue.poll();
System.out.println(cur.value);
for (Node next : cur.nexts) {
if (!map.contains(next)) {
map.add(next);
queue.add(next);
}
}
}
}
图的深度优先遍历
图的深度优先遍历利用栈实现,从源节点开始把节点按照深度放入栈,同时设置一个哈希表进行记录,然后弹出,每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈,直到栈变空。
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.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的节点,且没有环。
public static List<Node> sortedTopology(Graph graph) {
HashMap<Node, Integer> inMap = new HashMap<>();//记录某个点的入度
Queue<Node> zeroInQueue = new LinkedList<>();//入度为0的点进入这个队列
for (Node node : graph.nodes.values()) {//统计所有点的入度
inMap.put(node, node.in);
if (node.in == 0) {
zeroInQueue.add(node);//将第一个入度为0的点,放入零入度队列
}
}
List<Node> result = new ArrayList<>();
while (!zeroInQueue.isEmpty()) {
Node cur = zeroInQueue.poll();//将零入度队列的点弹出
result.add(cur);//将弹出的点添加到结果数组中
for (Node next : cur.nexts) {//对弹出节点的邻接点进行操作
inMap.put(next, inMap.get(next) - 1);//将弹出节点的邻接点的入度都减去1
if (inMap.get(next) == 0) {//找到新的入度为0的点进入零入度队列
zeroInQueue.add(next);
}
}
}
return result;
}
生成最小生成树的算法
最小生成树指的是让图上所有点连通且总的权值最小的连通图。实现的算法有以下两个,均要求图是无向图。
kruskal算法
从图中最小的边开始找,然后依次添加最小的边,同时判断是否形成环,如果形成环则不添加,直到所有的边全部尝试。而对于这个算法的实现,可能需要用到两块集合查询或者连通在一起,因此采用并查集的结构。
public class Code04_Kruskal {
// Union-Find Set
public static 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);
}
}
}
}
public static class EdgeComparator implements Comparator<Edge> {//比较器
@Override
public int compare(Edge o1, Edge o2) {
return o1.weight - o2.weight;
}
}
public static 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> result = new HashSet<>();
while (!priorityQueue.isEmpty()) {
Edge edge = priorityQueue.poll();
if (!unionFind.isSameSet(edge.from, edge.to)) {
result.add(edge);
unionFind.union(edge.from, edge.to);
}
}
return result;
}
}
prim算法
prim算法指的是从图中任意找一个点作为起始点,加入集合,然后从和所找的点连通的边中选择一条最小的边连通,此时这条边的另一个点加入集合,然后依次类推这个过程,直到所有的点都进入集合,此时所走的路径就是最小生成树。因为这个集合是一个一个节点的不断加入,因此可以采用哈希表的结构实现。
public static class EdgeComparator implements Comparator<Edge> {//比较器
@Override
public int compare(Edge o1, Edge o2) {
return o1.weight - o2.weight;
}
}
public static Set<Edge> primMST(Graph graph) {
PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(
new EdgeComparator());//定义小根堆
HashSet<Node> set = new HashSet<>();
Set<Edge> result = new HashSet<>();//最小生成树的结果
for (Node node : graph.nodes.values()) {//处理森林问题,如果遇到森林,那么每一棵树上求一个最小生成树
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;//将这条边的to节点放进toNode
if (!set.contains(toNode)) {
set.add(toNode);//将toNode节点添加到集合中
result.add(edge);//将得到的边添加进去
for (Edge nextEdge : toNode.edges) {
priorityQueue.add(nextEdge);
}
}
}
}
}
return result;
}
Dijkstra算法
适用范围:没有累加和权值为负数的环。Dijkstra算法适用于求解某一个点到其它各个点的最短距离。算法流程是从要求解的节点出发,然后将它到其它各个节点的距离为正无穷,到自己的距离为0。然后从要求解的点出发,开始依次找它到其它各个节点的最小值,然后再从剩余节点中找出到待求解节点的距离最小的节点按照同样的方式求解,处理过的节点不再处理,直到处理完所有的点。
public static HashMap<Node, Integer> dijkstra1(Node head) {
//从head出发到所有点的最小距离
//如果在表中没有记录,那么距离就是正无穷
HashMap<Node, Integer> distanceMap = new HashMap<>();
distanceMap.put(head, 0);//先将自身放进去
HashSet<Node> selectedNodes = new HashSet<>();//已经求过距离的节点,存进去再也不动
Node minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
while (minNode != null) {
int distance = distanceMap.get(minNode);//原节点到此时处理节点的距离
for (Edge edge : minNode.edges) {
Node toNode = edge.to;//和此时处理节点相连的节点
if (!distanceMap.containsKey(toNode)) {//如果不是不能动的节点
distanceMap.put(toNode, distance + edge.weight);//更新最短距离
}
distanceMap.put(edge.to, Math.min(distanceMap.get(toNode),distance+edge.weight));//从之前找的距离,和此时新出现的路进行比较,找出最小值更新
}
selectedNodes.add(minNode);//将此节点添加到不能动的哈希表中
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
}
return distanceMap;
}
public static 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 static class NodeRecord {
public Node node;//节点
public int distance;//head到该节点的最短距离
public NodeRecord(Node node, int distance) {
this.node = node;
this.distance = distance;
}
}
public static class NodeHeap {
private Node[] nodes;
private HashMap<Node, Integer> heapIndexMap;//任何一个节点在堆上的位置表,对于弹出堆的位置记录为-1
private HashMap<Node, Integer> distanceMap;//head到该节点的最短距离表
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;
}
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]));//将堆顶元素的位置和head到它的最短距离的信息返回
swap(0, size - 1);//将堆的最后一个元素和堆顶元素交换位置
heapIndexMap.put(nodes[size - 1], -1);//把原本的头节点放在-1位置
distanceMap.remove(nodes[size - 1]);//在这个表中删除它的记录
nodes[size - 1] = null;在nodes数组中释放,指向空
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) {//node有没有进来过堆
return heapIndexMap.containsKey(node);
}
private boolean inHeap(Node node) {//查某一个节点在不在堆上
return isEntered(node) && heapIndexMap.get(node) != -1;//进来过并且在堆上的位置不等于-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;
}
}
//改进后的Dijikstra算法
//从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;//记录head到堆顶元素的最短距离
for (Edge edge : cur.edges) {//遍历所有弹出节点的相连的边
nodeHeap.addOrUpdateOrIgnore(edge.to, edge.weight + distance);//根据弹出节点的head到它的最短距离,对其它节点的最短距离进行比较更新
}
result.put(cur, distance);
}
return result;
}