现在我们分别创建图的结点、边和图本身的类:

Node.java:

 1 package com.hw.Graph;
 2 
 3 import java.util.ArrayList;
 4 
 5 public class Node {
 6     public int value;
 7     public int in; //入度
 8     public int out; //出度
 9     public ArrayList<Node> nexts; //从该结点可以通向的结点
10     public ArrayList<Edge> edges; //与该结点相连的所有边
11 
12     public Node(int value) {
13         this.value = value;
14         in = 0;
15         out = 0;
16         nexts = new ArrayList<>();
17         edges = new ArrayList<>();
18     }
19     
20     public String toString() {
21         return value + "";
22     }
23 }

Edge.java:

 

 1 package com.hw.Graph;
 2 
 3 public class Edge {
 4     public int weight; //权重
 5     public Node from; //始结点
 6     public Node to; //末结点
 7     
 8     public Edge(Node from, Node to, int weight) {
 9         this.from = from;
10         this.to = to;
11         this.weight = weight;
12     }
13     
14     public String toString() {
15         return "边:<" + from + "," + to + ">,权值:" + weight;
16     }
17     
18 }

 

Graph.java:

 1 package com.hw.Graph;
 2 
 3 import java.util.HashMap;
 4 import java.util.HashSet;
 5 
 6 public class Graph {
 7     public HashMap<Integer, Node> nodes; //通过结点上的数值访问该结点
 8     public HashSet<Edge> edges; //存储图的所有边
 9     
10     public Graph() {
11         nodes = new HashMap<>();
12         edges = new HashSet<>();
13     }
14 }

好了,拥有了这些,我们就可以编写一些图的基本算法了。但是,就这样,我们怎样才能创建一个图呢?而且图的创建方式有许多种,有没有什么方式,在面对不管是以何种方式创建的图时,我们总能快速地把它转化为我们熟悉的结构,并使用我们自己编写的算法对于进行测试?肯定是有的。

我们不妨统一一下,以后在创建图时,我们用一个二维数组来存储,二维数组实际是一个一维数组,只是这个一维数组存储的每一个元素都是另一个一维数组。所以,在这个二维数组的每一个一维数组中,我们存储三个数据,分别是:开始结点的数值,结束结点的数值,这条边的权重。例如:

 

 

 这就表示这个图一共有8条边,第一条边的起始结点是1和2,这条边的权值是3...以此类推。而这个图创建出来,以可视化的形式表示则是下面这样的:

 

 接下来,我们来看看如何在拥有了以上三个基础类之后创建一个图:

 GraphGenerator.java:

 1 package com.hw.Graph;
 2 
 3 public class GraphGenerator {
 4     public static Graph createGraph(Integer[][] matrix) {
 5         Graph graph = new Graph();
 6         for (int i = 0; i < matrix.length; ++i) {
 7             int from = matrix[i][0];
 8             int to = matrix[i][1];
 9             int weight = matrix[i][2];
10             
11             //首先,我们往这个图中添加结点
12             //如果不存在结点,就添加进去
13             if (!graph.nodes.containsKey(from)) {
14                 graph.nodes.put(from, new Node(from));
15             }
16             if (!graph.nodes.containsKey(to)) {
17                 graph.nodes.put(to, new Node(to));
18             }
19             
20             //现在创建一条边
21             //当前前提是我们要先拿到这条边的两个结点
22             Node fromNode = graph.nodes.get(from);
23             Node toNode = graph.nodes.get(to);
24             Edge newEdge = new Edge(fromNode, toNode, weight);
25             
26             //我们假设这个图是个有向图,当然,就算不是也没关系,按照无向图的处理方式处理就好了
27             //如何处理?例如下面这行代码反着再写一遍
28             //现在,因为从起始节点可以通过边找到末尾节点,因此:
29             fromNode.nexts.add(toNode);
30             
31             //起始结点的出度+1:
32             ++fromNode.out;
33             //末尾结点的入度+1:
34             ++toNode.in;
35             
36             //别忘了,结点还有一个边的属性:
37             //由于从起始结点可以找到末尾结点,因此这条边应该属于起始结点:
38             fromNode.edges.add(newEdge);
39             
40             //现在离成功只差最后一步,我们把这条边添加到这个图里:
41             graph.edges.add(newEdge);
42         }
43         //最后,把这个图返回,就大功告成了
44         return graph;
45     }
46 }

上面的代码中,from,to和weight是我们事先说好的。

这样,一个图就创建好了。而且以后不管拿到一个以什么样的图,我们都可以像这样把它转化为我们熟悉的方式。

好了,接下来,我们来尝试编写图的广度优先算法

首先我们需要一个队列,因为是广度优先,我们创建的这个方法需要先传一个结点过来,把这个结点放进队列,然后获取它的所有邻居结点,然后也全部放进队列,如此循环往复。由于队列是先进先出的结构,因此这样做可以做到遍历图时以“广度”优先。然后我们还需要一个访问数组,由于在取得结点邻居的过程中,我们不可避免地会重复访问结点,所以我们需要设置一个访问数组,对于已访问的结点,我们不做任何操作。

 1 package com.hw.Graph;
 2 
 3 import java.util.HashSet;
 4 import java.util.LinkedList;
 5 import java.util.Queue;
 6 
 7 public class BFS {
 8     public static void bfs(Node node) {
 9         if (node == null) {
10             return;
11         }
12         HashSet<Node> set = new HashSet<>();
13         Queue<Node> queue = new LinkedList<>();
14         //首先把传过来的结点添加进队列
15         queue.offer(node);
16         //这个set相当于一个访问数组,我们同样也把结点添加进数组中
17         set.add(node);
18         while(!queue.isEmpty())
19         {
20             //把这个结点弹出来
21             Node cur = queue.poll();
22             //这里对结点进行操作
23             System.out.print(cur.value + " ");
24             //遍历这个结点的所有可以通向的结点,广度优先的思想在这里就体现出来了
25             for (Node nextNode : cur.nexts) {
26                 //我们肯定只能访问没有访问过的
27                 if (!set.contains(nextNode)) {
28                     queue.offer(nextNode);
29                     set.add(nextNode);
30                 }
31              }
32         }
33         //循环结束后,所有结点就都被访问过了
34         //我们在这里换个行,优化输出格式
35         System.out.println();
36     }
37 }

现在,我们可以试着测试一下这个图。

为了测试,我创建了一个新图:

 1 package com.hw.Graph;
 2 
 3 public class TestGraph {
 4     public static void main(String[] args) {
 5         Integer[][] matrix = {
 6                 {1, 2, 8},
 7                 {1, 3, 10},
 8                 {2, 3, 5},
 9                 {2, 5, 21},
10                 {3, 5, 4},
11                 {3, 4, 2},
12                 {5, 6, 1},
13                 {4, 6, 7},
14         };
15         Graph graph = GraphGenerator.createGraph(matrix);
16         Node root = graph.nodes.get(1);
17         BFS.bfs(root);
18     }
19 }

这个图长这样:

 

结果如下:

 

 

 Perfect!

 现在我们来编写深度优先遍历算法

在这之前呢,为了比较方便地表示图,我把用来表示图的每个结点的数字换成了相应的大写字符,例如1:A,2:B这样。改动很简单,相信我们都可以做到。

深度优先与广度优先类似,只是需要把广度优先使用的队列换成栈。我们先来看看代码:

 1 package com.hw.Graph;
 2 
 3 import java.util.HashSet;
 4 import java.util.Stack;
 5 
 6 public class DFS {
 7     public static void dfs(Node node) {
 8         if (node == null) {
 9             return;
10         }
11         Stack<Node> stack = new Stack<>();
12         HashSet<Node> set = new HashSet<>();
13         stack.push(node);
14         set.add(node);
15         System.out.print(node.value + " ");
16         while(!stack.isEmpty())
17         {
18             Node cur = stack.pop();
19             for (Node temp : cur.nexts) {
20                 if (!set.contains(temp)) {
21                     stack.push(cur);
22                     stack.push(temp);
23                     set.add(temp);
24                     System.out.print(temp.value + " ");
25                     break;
26                 }
27             }
28         }
29         System.out.println();
30     }
31 }

重点解释一下while循环里的代码:

我在循环外面弄进去了传过来的数据,然后在循环里面把它拿出来,访问他所有的邻居,这个时候,如果没有被访问过,我把这个数据和访问到的它的第一个邻居,又放回栈里,然后break掉。这是因为我现在是深度优先,所以我需要拿到一结点,我就不停地向它的深度挖去,如果不break掉,就相当于这个结点的深度还没挖到底,我就换另外一条路了挖了。而从栈中拿出来又放回去,其实这样子等图中所有结点都遍历完后,栈中的顺序,就是深度优先的顺序。到最后,栈中的元素会一个一个弹出去,倘若其中有一条路还没走完,那么就会从返回的深度继续深度优先,而这时候,要做到从返回的深度开始,栈就是必须的。因此才需要拿出来又放回去的骚操作。

我们来看看效果:

 

 

 Perfect!

接下来,我们不妨来看看图的普里姆算法

普里姆算法的作用就是求解图的最小生成树。

主要算法思路就是,我从某一个结点开始,在与其所有相连的边中寻找一条权值最短的边,然后把这条边加进来,这条边另一端的结点也加进来。然后再次寻找权值最小的边添加进来,同时还要注意不能生成回路。其中,我选择的权值最小的边,是从与所有我已经添加进来的结点相连的边中,选择一条权值最小的边,并没有局限与哪个结点。

我们来看看代码:

 1 package com.hw.Graph;
 2 
 3 import java.util.Comparator;
 4 import java.util.HashSet;
 5 import java.util.PriorityQueue;
 6 
 7 public class Prim {
 8     private static class EdgeComparator implements Comparator<Edge> {
 9         @Override
10         public int compare(Edge o1, Edge o2) {
11             // TODO Auto-generated method stub
12             return o1.weight - o2.weight;
13         }
14     }
15     
16     public static void prim(Graph graph) {
17         //我们创建这个小顶堆,作用是对所有边进行排序
18         PriorityQueue<Edge> queue = new PriorityQueue<>(new EdgeComparator());
19         HashSet<Node> set = new HashSet<>();
20         //首先我们要拿到所有的点,这样才能拿到所有的边
21         for (Node node : graph.nodes.values()) {
22             //如果不在set中,就添加进去
23             if (!set.contains(node)) {
24                 set.add(node);
25                 //现在,我们可以访问边了
26                 for (Edge edge : node.edges) {
27                     //拿到边怎么办呢?当然是全部扔进小顶堆里排序
28                     queue.offer(edge);
29                 }
30                 //好,现在排完序了,我们拿出一条边,这条边就是我们要找的
31                 //因为已经排过序了
32                 while(!queue.isEmpty())
33                 {
34                     Edge edge = queue.poll();
35                     //现在我们要得到这条边另一端的结点
36                     //因为我们需要把那个结点加到集合里,防止形成环
37                     Node toNode = edge.to;
38                     if (!set.contains(toNode)) {
39                         set.add(toNode);
40                         System.out.println(edge);
41                         //现在,我以toNode为基准,把它的所有边再加进堆中,全部重新排序
42                         //如果不这么干,那么队列中的边将永远只有那么几条
43                         for (Edge nextEdges : toNode.edges) {
44                             queue.offer(nextEdges);
45                         }
46                     }
47                 }
48             }
49         }
50     }
51 }

效果:

 

 Perfect!

 

posted @ 2021-08-26 14:27  EvanTheBoy  阅读(37)  评论(0编辑  收藏  举报