构建链表作为图表的基础Null指针错误

时间:2013-12-13 23:53:42

标签: java graph nullpointerexception linked-list

我正在尝试基于链接列表构建图形,其中我构建节点的链接列表,并且每个节点指向边缘的链接列表。我根据输入文件构建图表。

我的输入文件将采用以下方案:

  

图表中的节点数

     

SourceNode1 EndNode1

     

SourceNode2 EndNode2

     

...

例如:

4 //Number of nodes

1 2 //An edge between 1 and 2

1 3 //An edge between 1 and 3

2 4 //An edge between 2 and 4

假设图中的节点将通过节点数编号为1,并且没有节点将具有多于1个“父”(尽管节点可能具有多于1个“子”)。

我的问题是尝试构建包含节点的链表。每个节点有3个字段:来自该节点的边,节点值(1,2,3等)和下一个节点(因为是节点的链接列表)。我尝试解析节点数,手动创建第一个节点,并以迭代方式连接其余节点。

注意:父字段用于与此问题无关的某些外部分析。你可以忽略它。

节点类:

public class Node {
  private Edge firstEdge;
  private Node parent;
  private Node nextNode;
  private int element;

  //Constructor
  public Node() {
    parent = null;
    firstEdge = null;
    nextNode = null;
  }

  //Accsessor and Modifier Methods
  public void setElement(int e) {element = e;}
  public Node getNextNode() {return nextNode;}  
  public Edge getFirstEdge() {return firstEdge;}
  public void setFirstEdge(Edge a) {firstEdge = a;}
  public void setNextNode(Node a) {nextNode = a;}
  public int getElement() {return element;}
  public Node getParent() {return parent;}
  public void setParent(Node p) {parent = p;}

  //Checks for a non-null parent
  public boolean hasParent() { return parent == null; }

  //checks iff node has next edge
  public boolean hasFirstEdge() { return firstEdge == null; }

  //checks if a node has a next node
  public boolean hasNextNode() { return nextNode == null; }

}

边缘类:

public class Edge {
 //Instance Variables
 private Node nextNode;
 private Edge nextEdge;

 //Constructor
 public Edge() {
  nextNode = null;
  nextEdge = null;
 }

 //Accsessor and Modifier Methods
 public void setNextNode(Node a) {nextNode = a;}
 public void setNextEdge(Edge a) {nextEdge = a;}
 public Node getNextNode() {return nextNode;}
 public Edge getNextEdge() {return nextEdge;}

 public boolean hasNextEdge() {
  return nextEdge == null;
 }
}

驱动程序类:

import java.util.Scanner;
import java.io.*;

public class Driver {
  public static void main(String[] args)throws FileNotFoundException{
    //Get text file for building the graph
    Scanner console = new Scanner(System.in);

    System.out.print("Please enter the text file name: ");
    String fileName = console.nextLine();
    Scanner in = new Scanner(new File(fileName));
    //in contains the file reading scanner


    int numNodes = in.nextInt(); //first line of the text file

    Node first = new Node(); //first is head of the list
    first.setElement(1);

    int i = 2; //counter

    //Build the nodes list; I get problems in this loop
    while (i <= numNodes) {
      Node head = new Node(); //Tracker node
      head = first; //head is the first node of the list

      /*Loop to end of the list*/          
      while(head.hasNextNode()) {

        //Null check; without it, I get NullPointerExceptions.
        //If it is not needed, or there is a better way, please inform me.
        if (head.getNextNode() == null) {
          break;
        }

        head = head.getNextNode(); //get to the end of the ilst    
      }


      //Next node to add
      Node newNode = new Node();
      newNode.setElement(i); //Because of the 1, 2, 3 nature of the graph 

      head.setNextNode(newNode); //Set the last element as the next node

      i++;
    }

    //Manually check if graph is made (check if the nodes are linked correctly)
    System.out.println("First elem (expect 1): " + first.getElement());
    System.out.println("Second elem (expect 2): " + first.getNextNode().getElement()); //It prints 4 here for some reason

    System.out.println("Third elem (expect 3): " + first.getNextNode().getNextNode().getElement()); //Getting a NullPointerException

    System.out.println("Fourth elem (expect 4): " + first.getNextNode().getNextNode().getNextNode().getElement());

    System.out.println("Expecting null: " + first.getNextNode().getNextNode().getNextNode().getNextNode().getElement());
}

当我检查图表是否构建时,我遇到了问题。我手动检查它(对于这个小图,它可能),只需打印出第一个节点和后续节点的值。我期待1,2,3,4和null(对于过去4的元素,因为它不存在)。第一个节点很好,它打印1.调用first.getNextNode()。getElement()打印4,原因有些奇怪。然后调用节点会产生NullPointerException。有人可以帮我解决这个问题吗?

注意:我还没有添加边缘。我只是想获得构建的链接列表的核心。

这是我关于堆栈溢出的第一篇文章。如果它含糊不清,含糊不清,过于详细,缺乏信息或是一个重复的问题,我会道歉。我无法在其他任何地方找到答案。欢迎并赞赏所有意见。

2 个答案:

答案 0 :(得分:0)

您的大部分命名都非常令人困惑,并且非常需要澄清重构。 Edge的{​​{1}} nextNode应该被称为destinationNode或类似的内容,以明确您从Edge对象而非其他Node取消引用。

现在,让我们深入研究实际的实现。

Node head = new Node(); //Tracker node
head = first; //head is the first node of the list

这里发生了什么?您似乎将本地变量head设置为全新的Node;那很棒。除了下一行之外,您将其丢弃并将其设置为first变量的值。

然后,您使用while循环遍历到列表末尾,然后创建另一个Node(您实际使用的那个)。 (通常如果你想在列表的末尾添加一些内容,你应该使用双向链表,或者至少应该指向第一个最后一个元素......即,{ {1}}总是保持不变,但是当你添加一个新节点时,你只需说first然后从那里配置新元素。你这样做的时候,花费O(N ^ 2)时间用N个元素构建一个单链表,几乎不理想。

我对你的小班的建设也有一些优先的批评......如果你允许价值观自由获取并设置为公共制定者和吸气者的任何价值,而不改变任何行动,你得到的完全相同的功能,只需将这些字段标记为公共,完全取消getter和setter。如果你有任何计划在未来添加更多功能,那就很好了,但是如果它们只是一个愚蠢的链接列表元素,其实际用途是在其他地方实现的那么你最好将类更像是一个结构。

这是一种以您希望的方式构建单链接newNode = new Node(); last.nextNode = newNode; last = newNode;列表的好方法:

Node

请注意,使用int numNodes = in.nextInt(); //first line of the text file // sentinel value indicating the beginning of the list Node header = new Node(); header.setElement(-1); // last node in the list Node last = first; // this loop constructs a singly linked ring from the header for (int i = 1; i <= numNodes; i++) { Node newNode = new Node(); newNode.setElement(i); newNode.setNextNode(header); last.setNextNode(newNode); last = newNode; } // do your debug outputs here // for instance, this loop always outputs every node in the list: for (Node n = header.getNextNode(); n != header; n = n.getNextNode()) { System.out.println("Node " + n.getElement()); } 作为sentinel value可以保证,对于已经构建的headerNode永远不会返回getNextNode()

同样,通过公开nullNode类中的字段并废弃getter和setter,可以使所有这些代码更具可读性。 Edge可以成为header.getNextNode().getNextNode().getNextNode()等等。


第2阶段

现在我们已经解决了这种类型的结构对您的应用程序实际有用的问题。我最关心的是,当在图表上的节点之间应用边缘时,您需要访问任意索引的header.nextNode.nextNode.element s以将边缘附加到它们......并且每个Node已经知道它的元素索引是什么,获取第N个节点需要N步,因为你的整个节点集都在一个链表中。

请记住,使用链接列表的主要优点是,当您单步执行列表时,能够在O(1)时间内删除任意元素。如果您只是构建一个列表并且不会从中删除任何内容,那么阵列通常会更快 - 尤其是如果需要访问任意元素。< / p>

如果您不需要保证他们按照任何特定顺序或通过索引访问它们,但您需要能够添加,按ID访问和删除,该怎么办?他们很快就可以获得更大的数据集Node可能适合您!如果您仍然需要按照添加顺序访问所有内容,该怎么办? HashSet是你最好的朋友。有了这个,你甚至可以轻松地给节点名称提供没有真正减速的字符串。

至于边缘,我觉得你已经做得很好:最好在单链表中为每个LinkedHashSet实现传出边缘,假设你很少会删除边缘或者数量很少每个节点的边数,并将始终一起访问它们。要添加新边缘,只需说出Node即可完成,将新边添加到列表的开头。 (单个链表最容易用作堆栈而不是队列。)

对于额外的花哨点,请在newEdge = new Edge(); newEdge.nextEdge = firstEdge; firstEdge = newEdge;课程中实施Iterable<Edge>并制作一个Node课程,这样您就可以使用extended-for访问每一个边缘,让您的生活更美好更容易!

答案 1 :(得分:0)

正如@Widdershins所说,使用的术语使算法难以理解。

我会建议两件事来重构你的代码:

  1. 查看术语(也许这会有所帮助:http://en.wikipedia.org/wiki/Glossary_of_graph_theory)。我知道这听起来像是一个愚蠢的建议,但使用适当的术语将有助于您审查对象模型。

  2. 使用更好的表示法。在您的代码中,Node会填充多个角色,这会使代码难以理解。

  3. 良好的表现形式将取决于您尝试解决的许多问题。例如,Adjacency ListMatrix对于应用某些图论算法很有用。

    但是,如果您只想使用面向对象的设计,那么从基础开始是很有用的。

    在数学中定义图形:G = (V, E) ...图形是一组节点和这些节点之间的一组边缘,并将其转换为代码:

    (该示例使用字段简洁)

    class DirectedGraph {
        final Set<Node> nodes = new HashSet<Node>();
        final Set<Edge> edges = new HashSet<Edge>();
    }
    

    现在您需要扩展此定义。你可以一步一步做。我做同样的事情以这种表示结束:

    class DirectedGraph {
        final Set<Node> nodes = new HashSet<Node>();
        final Set<Edge> edges = new HashSet<Edge>();
    
        public Node addNode(Object value) { 
            Node newNode = new Node(value);
            nodes.add(newNode); 
            return newNode; 
        }
    
        public Edge addEdge(Node src, Node dst) {
            Edge newEdge = new Edge(src, dst);
            edges.add(newEdge); 
            return newEdge; 
        }
    
        private assertValidNode(Node n) {
            if (n.graph != this)
                throw new IllegalArgumentException("Node " + n + " not part of the graph");
        }
    
        public Set<Node> successorsOf(Node n) {
            assertValidNode(n);
            Set<Node> result = new HashSet<Node>();
            for (Edge e : edges) {
                if (e.src == n) { result.add(e.dst); }
            }
            return result;
        }
    
        class Node { 
            final graph = DirectedGraph.this;
            final Object value;
            Node(Object v) {
                this.value = v;
            }
            public String toString() { return value.toString(); }
    
            public Set<Node> successors() {
                return graph.successorsOf(this);
            }
            // useful shortcut
            public Node connectTo(Node... nodes) {
                for (Node dst : nodes) {
                    graph.addEdge(this, dst);
                }
                return this;
            }
        }
    
        class Edge {
            final graph = DirectedGraph.this;
            final Node src; final Node dst; 
            Edge(Node src, Node dst) {
                graph.assertValidNode(src);
                graph.assertValidNode(dst);
                this.src = src; this.dst = dst;
            }
            public String toString() { return src.toString() + " -> " + dst.toString(); } 
        }
    
    }
    
    DirectedGraph g = new DirectedGraph();
    DirectedGraph.Node one = g.addNode(1);
    DirectedGraph.Node two = g.addNode(2);
    DirectedGraph.Node three = g.addNode(3);
    DirectedGraph.Node four = g.addNode(4);
    
    one.connectTo(two, three)
    two.connectTo(four);
    
    System.out.println(g.edges);
    System.out.println(one.successors());
    System.out.println(two.successors());
    

    这种在“1对1”映射中表示域模型的策略总是帮助我“发现”对象模型。然后,您可以根据您的特定需求改进实现(即使用邻接列表可以提高successorsOf的运行时间。)

    请注意,在此表示中,NodeEdge只能作为图表的一部分存在。此限制不是直接从数学表示中推导出来的......但有助于维护正确图形的约束。

      

    注意您可以通过使用父图表引用构建NodeEdge来提取内部类。