java怎么表示正无穷大_Java中的图形:用代码表示图形

介绍

图形是存储某些类型的数据的便捷方法。该概念是从数学移植而来的,适合于计算机科学的需求。

由于许多事物可以用图形表示,因此图形遍历已成为一项常见的任务,尤其是在数据科学和机器学习中。图遍历是指通过连接边访问图中的节点(又称顶点)的过程。这通常用于在图形中查找特定节点,或用于绘制图形。

在本系列中,我们将研究计算机科学中图形的使用和表示方式,以及一些流行的遍历算法:

  • Java中的图
    • 用代码表示图
    • 深度优先搜索(DFS)
    • 广度优先搜索(BFS)
    • Dijkstra的算法

用代码表示图

既然我们已经了解了什么是图形以及它们何时有用,我们应该知道如何在代码中实现它们。

解决此问题的主要两种方法是邻接矩阵邻接列表

邻接矩阵

让我们开始假设我们有n节点并且它们被方便地命名0,1,...n-1,并且它们包含与它们具有相同名称的值。当然,这种情况很少发生,但是它使解释邻接矩阵更加容易。

我们的节点/顶点是对象(最有可能是对象)的情况非常复杂,并且需要很多维护方法,这些维护方法会使邻接矩阵比大多数情况下麻烦,因此,我们仅提供执行“简单”案例。

假设我们有以下图形:

76a9bf378ea9e3cd5cc9a8f9fb70a939.png

在此图中,有5个节点-(0,1,2,3,4),其边缘为{1,2},{1,3},{2,4},{3,0}。根据定义,当我们来看一个未加权无向图-的位置(i,j)在我们的邻接矩阵是1,如果节点之间存在的边缘ij,否则其为0。在无向图的邻接矩阵是对称的情况下。

上一个示例的邻接矩阵如下所示:

373fa02bfc192a69aa9c6f76ba977b7e.png

我们也可以颠倒该过程,从给定的邻接矩阵绘制图形。

我们将给出一个反向过程的示例,但带有加权图的邻接矩阵。在这种情况下的位置(i,j)在我们的矩阵等于节点之间的边的权重ij如果存在的话,否则它等于无穷大。

注意:使用无穷大作为权重被视为表明边缘不存在的“安全”方法。但是,例如,如果我们知道我们只有正权重,则可以改用-1或任何合适的值来决定。

让我们从以下邻接矩阵构造一个加权图:

49809eedc3c6eaf4e5a59f7b7b460cac.png

552a16c01fac7c414ddc194ca3fd8a51.png

在最后一个示例中,我们将显示如何使用邻接矩阵表示有向加权图:

f47e2dcecbe4e0b92cd7bd7759bdcbd7.png

9bac33ced5229fabeebd325ece7a5de1.png

请注意,有向图的邻接矩阵如何不对称的,例如,我们的值是(0,3)而不是(3,0)。同样也没有理由为什么一个节点不能成为边缘的起点和终点,而我们可以拥有完全未连接的节点。

实施邻接矩阵

既然我们已经了解了邻接矩阵在纸上的工作方式,那么我们需要考虑它们的实现。如果我们的“节点”确实只是整数值0,1,...n-1,则实现将非常简单。

但是,由于通常不是这种情况,因此我们需要弄清楚当节点是对象时如何利用矩阵索引作为节点的便利。

在我们的实现中,我们将使我们的课程尽可能多才多艺。这反映在更多的方法和一些边缘情况中。

我们还将提供有向图和无向图以及加权/未加权图之间的选择。

public class Graph {

    private int numOfNodes;
    private boolean directed;
    private boolean weighted;
    private float[][] matrix;

    /*
     This will allow us to safely add weighted graphs in our class since
     we will be able to check whether an edge exists without relying
     on specific special values (like 0)
    */
    private boolean[][] isSetMatrix;

    // ...
}

然后,我们将有一个简单的构造函数:

public Graph(int numOfNodes, boolean directed, boolean weighted) {

    this.directed = directed;
    this.weighted = weighted;
    this.numOfNodes = numOfNodes;

    // Simply initializes our adjacency matrix to the appropriate size
    matrix = new float[numOfNodes][numOfNodes];
    isSetMatrix = new boolean[numOfNodes][numOfNodes];
}

现在,让我们编写一个允许添加边的方法。我们要确保在图形被加权且未提供权重的情况下,将边值设置为0,如果未加权则将其简单地添加1:

/*
 Since matrices for directed graphs are symmetrical, we have to add
 [destination][source] at the same time as [source][destination]
*/
public void addEdge(int source, int destination) {

    int valueToAdd = 1;

    if (weighted) {
        valueToAdd = 0;
    }

    matrix[source][destination] = valueToAdd;
    isSetMatrix[source][destination] = true;

    if (!directed) {
        matrix[destination][source] = valueToAdd;
        isSetMatrix[destination][source] = true;
    }
}

如果未对图形进行加权并提供了权重,我们只需忽略该[source,destination]值,并将其值设置为1,就表示确实存在边:

public void addEdge(int source, int destination, float weight) {

    float valueToAdd = weight;

    if (!weighted) {
        valueToAdd = 1;
    }

    matrix[source][destination] = valueToAdd;
    isSetMatrix[source][destination] = true;

    if (!directed) {
        matrix[destination][source] = valueToAdd;
        isSetMatrix[destination][source] = true;
    }
}

在这一点上,让我们添加一个方法,使我们可以轻松打印出邻接矩阵:

public void printMatrix() {
    for (int i = 0; i < numOfNodes; i++) {
        for (int j = 0; j < numOfNodes; j++) {
            // We only want to print the values of those positions that have been marked as set
            if (isSetMatrix[i][j])
                System.out.format("%8s", String.valueOf(matrix[i][j]));
            else System.out.format("%8s", "/  ");
        }
        System.out.println();
    }
}

在那之后,一种方便的方法以一种更易于理解的方式打印出边缘:

/*
 We look at each row, one by one.
 When we're at row i, every column j that has a set value represents that an edge exists from
 i to j, so we print it
*/
public void printEdges() {
    for (int i = 0; i < numOfNodes; i++) {
        System.out.print("Node " + i + " is connected to: ");
        for (int j = 0; j < numOfNodes; j++) {
            if (isSetMatrix[i][j]) {
                System.out.print(j + " ");
            }
        }
        System.out.println();
    }
}

最后,让我们编写两个辅助方法,这些方法将在以后使用:

public boolean hasEdge(int source, int destination) {
    return isSetMatrix[source][destination];
}

public Float getEdgeValue(int source, int destination) {
    if (!weighted || !isSetMatrix[source][destination])
        return null;
    return matrix[source][destination];
}

为了展示邻接矩阵是如何工作的,让我们使用我们的类来制作一个图,用关系填充它,并打印它们:

public class GraphShow {
    public static void main(String[] args) {

        // Graph(numOfNodes, directed, weighted)
        Graph graph = new Graph(5, false, true);

        graph.addEdge(0, 2, 19);
        graph.addEdge(0, 3, -2);
        graph.addEdge(1, 2, 3);
        graph.addEdge(1, 3); // The default weight is 0 if weighted == true
        graph.addEdge(1, 4);
        graph.addEdge(2, 3);
        graph.addEdge(3, 4);

        graph.printMatrix();

        System.out.println();
        System.out.println();

        graph.printEdges();

        System.out.println();
        System.out.println("Does an edge from 1 to 0 exist?");
        if (graph.hasEdge(0,1)) {
            System.out.println("Yes");
        }
        else System.out.println("No");
    }
}

这给了我们输出:

/       /      19.0    -2.0     /
     /       /       3.0     0.0     0.0
    19.0     3.0     /       0.0     /
    -2.0     0.0     0.0     /       0.0
     /       0.0     /       0.0     /


Node 0 is connected to: 2 3
Node 1 is connected to: 2 3 4
Node 2 is connected to: 0 1 3
Node 3 is connected to: 0 1 2 4
Node 4 is connected to: 1 3

Does an edge from 1 to 0 exist?
No
null

如果我们基于此矩阵构造图,则其外观如下所示:

4aea5e4b993d0400292a00af1d3d7e83.png

邻接表

邻接表的实现更加直观,比邻接表的使用频率更高。

订阅我们的新闻
在收件箱中获取临时教程,指南和作业。从来没有垃圾邮件。随时退订。
订阅电子报
订阅

顾名思义,我们使用列表表示节点具有边的所有节点。通常,这是通过HashMaps和LinkedLists实现的。

76a9bf378ea9e3cd5cc9a8f9fb70a939.png

邻接表倾向于有向图,因为这是最直接的地方,无向图只需要多一点维护。

在此示例中,我们可以看到:

Node 0 is connected with node 3
Node 1 is connected with nodes 3, 2
Node 2 is connected with nodes 1, 4
Node 3 is connected with nodes 1, 0
Node 4 is connected with node 2

显然,对于节点0,我们将创建一个LinkedList包含节点3的节点。对于节点1,我们将创建一个LinkedList包含节点3和2的节点,依此类推。

对于加权图,如下图所示,我们需要数组列表而不是节点列表。数组将包含位于边缘另一端的节点作为第一个参数,并将关联的权重作为第二个参数。

552a16c01fac7c414ddc194ca3fd8a51.png
0: [1,-50] -> [3,3]
1: [0,-50]
2: [3, 10]
3: [0,3] -> [2,10] -> 4,7
4: [3,7]

f47e2dcecbe4e0b92cd7bd7759bdcbd7.png
0: [2,10]
1: null
2: [2,5] -> [3,5] -> [4,3]
3: [0,-2]
4: [3,5]

关于邻接表的一件很棒的事情是,与邻接矩阵相比,使用对象要容易得多。

我们将以对象作为节点而不是索引来实现邻接表。在解释邻接表时,这既受青睐,对了解更为有用,因为您可能会在项目中使用对象。

实施邻接表

乍一看,该代码可能看起来很复杂,但是如果仔细观察,它会很简单。首先,让我们从一个简单的Node类开始:

public class Node {
    int n;
    String name;

    Node(int n, String name){
        this.n = n;
        this.name = name;
    }
}

现在,让我们定义一个Graph

public class Graph {

// Each node maps to a list of all his neighbors
private HashMap<Node, LinkedList<Node>> adjacencyMap;
private boolean directed;

public Graph(boolean directed) {
    this.directed = directed;
    adjacencyMap = new HashMap<>();
}

现在,让我们添加方法addEdge()。尽管这一次我们将使用两种方法,辅助方法和实际方法。

在辅助方法中,我们还将检查可能的重复边缘。在A和之间添加边之前B,我们先将其删除,然后再添加。如果存在(我们要添加重复的边),则将其删除,然后再次添加,只有一个。

但是,如果它不存在,那么删除不存在的边将导致,NullPointerException因此我们引入了列表的临时副本:

public void addEdgeHelper(Node a, Node b) {
    LinkedList<Node> tmp = adjacencyMap.get(a);

    if (tmp != null) {
        tmp.remove(b);
    }
    else tmp = new LinkedList<>();
    tmp.add(b);
    adjacencyMap.put(a,tmp);
}

public void addEdge(Node source, Node destination) {

    // We make sure that every used node shows up in our .keySet()
    if (!adjacencyMap.keySet().contains(source))
        adjacencyMap.put(source, null);

    if (!adjacencyMap.keySet().contains(destination))
        adjacencyMap.put(destination, null);

    addEdgeHelper(source, destination);

    // If a graph is undirected, we want to add an edge from destination to source as well
    if (!directed) {
        addEdgeHelper(destination, source);
    }
}

最后,我们将使用printEdges()hasEdge()帮助方法,它们非常简单:

public void printEdges() {
        for (Node node : adjacencyMap.keySet()) {
            System.out.print("The " + node.name + " has an edge towards: ");
            if (adjacencyMap.get(node) != null) {
                for (Node neighbor : adjacencyMap.get(node)) {
                    System.out.print(neighbor.name + " ");
                }
                System.out.println();
            }
            else {
                System.out.println("none");
            }
        }
    }

    public boolean hasEdge(Node source, Node destination) {
        return adjacencyMap.containsKey(source) && adjacencyMap.get(source) != null && adjacencyMap.get(source).contains(destination);
    }

为了展示邻接表是如何工作的,让我们实例化几个节点并用它们填充图:

public class GraphShow {
    public static void main(String[] args) {

        Graph graph = new Graph(true);
        Node a = new Node(0, "A");
        Node b = new Node(1, "B");
        Node c = new Node(2, "C");
        Node d = new Node(3, "D");
        Node e = new Node(4, "E");

        graph.addEdge(a,b);
        graph.addEdge(b,c);
        graph.addEdge(b,d);
        graph.addEdge(c,e);
        graph.addEdge(b,a);

        graph.printEdges();

        System.out.println(graph.hasEdge(a,b));
        System.out.println(graph.hasEdge(d,a));
    }
}

我们得到输出:

The A has an edge towards: B
The B has an edge towards: C D A
The C has an edge towards: E
true
false

注意:当然,这在很大程度上取决于Java如何对待内存中的对象。amain将其添加到图形中之后,我们必须确保对节点的进一步更改反映在图形上!有时这是我们的目标,但有时并非如此。无论哪种方式,我们都应该注意,在这种情况下,a图中的a节点与中的节点相同main

当然,我们可以采用不同的方式来实现。另一种流行的方法是将传出边列表添加到Node对象本身,并Graph适当地更改类:

public class Node {
    int n;
    String name;
    LinkedList<Node> adjacentNodes;

    Node(int n, String name) {
        this.n = n;
        this.name = name;
        adjacentNodes = new LinkedList<>();
    }

    public void addEdge(Node node) {
        if (!adjacentNodes.contains(node))
            adjacentNodes.add(node);
    }
}

两种方法都以它们自己的方式遵循了面向对象的封装概念的精神,所以两种方法都很好。

邻接矩阵与邻接列表

邻接矩阵的查找时间比邻接列表快得多。例如,如果我们想检查节点是否0有一条通向节点的边,4我们可以只检查索引处的矩阵,[0,4]这样可以给我们恒定的执行时间。

另一方面,我们可能需要检查0其邻接列表中的邻居的整个列表,以发现是否存在通向node的边4,这给了我们线性(O(n))查找时间。

在邻接矩阵增加边也快得多-简单地改变位置值[i,j]从节点添加一个边缘i到节点j,而与列表(如果我们没有进入指针最后一个元素)也需要为O(n )时间,尤其是在我们需要检查列表中是否已存在该边缘时。

就空间而言,由于非常简单的原因,邻接表效率更高。大多数现实生活中的图就是我们所说的稀疏图,这意味着边的数量比可能的最大数量少得多。

为什么这很重要?好吧,在邻接矩阵中,我们总是有一个n x n大小的矩阵(其中n是节点数),而不管我们是只有几个边还是几乎是最大数(每个节点相互连接)。

实际上,这占用了很多不必要的空间,因为正如我们所说,大多数现实生活中的图都是稀疏的,并且我们为这些边中的大多数分配了空间以不存在。另一方面,邻接表仅跟踪现有边。

更具体而言,如果我们有一个具有N个节点和E个边的图,则这两种方法的空间复杂度将是:

f20f692fb659c245caef452ae26b854e.png

我应该选择实施哪一个?

简短答案-邻接表。当使用对象时,它们更加简单明了,并且大多数时候,我们不关心与代码维护和可读性相比,邻接矩阵所提供的查询时间略短。

但是,如果我们要处理高度密集的图(与sparse相对),则有必要投资必要的内存以通过邻接矩阵实现图。

因此,例如,如果您最有可能要使用的操作是:

  • 检查边缘是否为图的一部分:邻接矩阵,因为检查边缘是否为图的一部分需要O(1)时间,而在邻接列表中则需要O(lengthOfList)时间
  • 从图中添加或删除边:邻接矩阵,与前面的情况相同
  • 遍历图形:邻接表,需要O(N + E)时间而不是O(N ^ 2)

结论

图形是存储某些类型的数据的便捷方法。该概念是从数学移植而来的,适合于计算机科学的需求。

由于许多事物可以用图形表示,因此图形遍历已成为一项常见的任务,尤其是在数据科学和机器学习中。

用代码表示图的主要两种方法是邻接矩阵邻接表

译者:啊强啊

链接:https://stackabuse.com/graphs-in-java-representing-graphs-in-code/

来源:Stack Abuse

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值