3

I have this method that uses recursion to find a node that holds a specified String in a binary tree. The issue is that it returns null, when it should return the node that holds the specified name, and I'm not sure why.

Here's the method:

public Node getNode(Node currentNode, String name) { 
    Node retrieved = null;
    if (currentNode.getName().equals(name)) { retrieved = currentNode; }
    else 
    {
        if (currentNode.right != null) {
            getNode(currentNode.right, name);
        } 
        if (currentNode.left != null) {
            getNode(currentNode.left, name);
        }
    }
    return retrieved;
}

Any insight into what may be the problem would be appreciated.

Neuron
  • 5,141
  • 5
  • 38
  • 59

3 Answers3

1

You need to capture the return value of your two recursive calls. Otherwise you're doing recursion "for nothing" and throwing away the result of the recursion.

public Node getNode(Node currentNode, String name){ 
    Node retrieved = null;
    if (currentNode.getName().equals(name)) { retrieved = currentNode; }
    else 
    {
        if (currentNode.right != null){
            retrieved = getNode(currentNode.right, name);
        } 
        if (retrieved == null && currentNode.left != null){
            retrieved = getNode(currentNode.left, name);
        }
    }
    return retrieved;
}

The following solution is arguably better style because you leave null checks for a base case. Notice how you no longer need to check currentNode.right != null or currentNode.left != null, as they're covered by the base case after one more recursive step.

public static Node getNode(Node currentNode, String name){
    // Base case: currentNode is null, nothing left to search
    if (currentNode == null) {
        return null;
    }

    Node retrieved = null;
    if (currentNode.getName().equals(name)) {
        retrieved = currentNode;
    } else {
        // Try to search right subtree
        retrieved = getNode(currentNode.right, name);

        // If not found in right subtree, then search left subtree
        if (retrieved == null){
            retrieved = getNode(currentNode.left, name);
        }
    }
    return retrieved;
}
k_ssb
  • 6,024
  • 23
  • 47
  • This will crash due a null pointer exception. Can you see why this will happen? – Tim Biegeleisen Apr 22 '18 at 01:35
  • @TimBiegeleisen Are you sure? I tested this and it hasn't crashed for me (yet) – k_ssb Apr 22 '18 at 01:36
  • Obviously this can't be called like `getNode(null, "foo")` but that isn't part of OP's question. – k_ssb Apr 22 '18 at 01:38
  • Suppose you _don't_ find the `name`, i.e. the name is not in the tree. Then how does your recursion know to stop? – Tim Biegeleisen Apr 22 '18 at 01:38
  • It returns `null` without NPE. If you think a base case is missing... it's not. Might not be the best style, but this is the simplest "fix" using OP's current solution. – k_ssb Apr 22 '18 at 01:39
  • 1
    Please don't use "Edit:" blocks: https://meta.stackexchange.com/questions/127639/when-is-edit-update-appropriate-in-a-post – Neuron Apr 22 '18 at 02:15
  • @pkpnd thanks. Although I would argue if the second solution is better than the first, why not remove the first? – Neuron Apr 22 '18 at 02:18
  • Because the first solution is more helpful to the OP (in the sense that it's more similar to their existing approach). When learning, it's helpful to first see a "minimal" fix before trying to understand a more advanced fix. – k_ssb Apr 22 '18 at 02:22
  • @pkpnd but then just mentioning the solution in a single line like: `retrieved = getNode(currentNode.right, name);` would be enough. Then you can present the cleaner and better solution – Neuron Apr 22 '18 at 02:27
  • The fix is slightly more complicated than one line. Notice you have to add a check `retrieved == null` to the following if-statement. Hence IMO it was better to just have the entire solution for context and clarity. – k_ssb Apr 22 '18 at 02:29
1

Solution

getNode(currentNode.right, name);

You call the getNode(...) method but you don't do anything with it.

A better solution

If you are willing to use googles Guava (must-have for every project in my opinion) and java 8, you can do the following:

public static final Traverser<Node> TREE_TRAVERSER = 
        Traverser.forTree((SuccessorsFunction<Node>) node ->
                Stream.of(node.right, node.left)
                        .filter(Objects::nonNull)
                        .collect(Collectors.toList()));

And then call it where ever you want to traverse the tree:

for (Node n : TREE_TRAVERSER.depthFirstPreOrder(root)) {
    if (n.getName().equals("foo")) {
        // todo: do stuff with node foo
    }
}

The java 8 way of traversing the tree would then be:

Iterable<Node> nodes = TREE_TRAVERSER.depthFirstPreOrder(root);
Optional<Node> result = StreamSupport.stream(nodes.spliterator(), false)
        .filter(n -> n.getName().equals("foo")) // find node with name "foo"
        .findAny(); // we assume there is <= 1 node, so we take any.
// node.isPresent() to check if you found a Node and result.get() to get the Node

How does this work?

Well, Guava has this nice class called a Traverser<N>. You just give it one parameter, which is the SuccessorsFunction<N>. It takes any object N and returns a Iterable<? extends N>, which are the child nodes.

We use Streams to do this. First we create a Stream of the two child nodes. We then filter them to only have a Stream of nonNull Nodes and collect them in a List (since the SuccessorsFunction<Node> wants to return a Iterable<Node>).

This Traverser<N> only has to be created once, so we made it public static final. You can now choose an iteration order. We chose depthFirstPreOrder, which returns an Iterable<N> we can iterate over


If you haven't heard of Streams before, I would recommend this turorial.

Community
  • 1
  • 1
Neuron
  • 5,141
  • 5
  • 38
  • 59
  • 1
    Well written answer! (+!) – Rann Lifshitz Apr 22 '18 at 04:03
  • Does your solution not omit nodes that are not connected to other nodes, eg the first node? – Kamil Tomasz Jarmusik Apr 22 '18 at 17:03
  • @KamilTomaszJarmusik what makes you think so? – Neuron Apr 22 '18 at 17:13
  • Stream.of(node.right, node.left) - It probably does not include the first node. What do you think about Stream.of(node, node.right, node.left) ? – Kamil Tomasz Jarmusik Apr 22 '18 at 17:19
  • @KamilTomaszJarmusik no, a `SuccessorsFunction` only takes a node and returns its successors. If it were to return itself as a successor, you'd have a graph, not a tree. And as [`Iterable breadthFirst(N startNode)`](https://google.github.io/guava/releases/snapshot-jre/api/docs/com/google/common/graph/Traverser.html#breadthFirst-N-) notes: `Traversal not terminating (if the graph has cycles)` – Neuron Apr 22 '18 at 17:22
  • @KamilTomaszJarmusik If you are curious on how this works, I would suggest you read the [`Traverser` java doc](https://google.github.io/guava/releases/snapshot-jre/api/docs/com/google/common/graph/Traverser.html) :) – Neuron Apr 22 '18 at 17:27
  • It works so that starting from the first node(root) creates a structure(Iterable) of nodes? Is not lazy? – Kamil Tomasz Jarmusik Apr 22 '18 at 17:38
  • @KamilTomaszJarmusik it is lazy. The `Iterable`s will only be created when the `Traverser` visits a `Node`s children. Starting with a `root`: The `Traverser` returns just that node. If the `Iterator` calls `next()` it will fetch the children of `root` with the provided `SuccessorsFunction`. It takes the first element from the `SuccessorsFunction` and returns it. The next `next()` call will get the children of the last child which was returned and so forth. Until a leaf is reached, when it will go up one level and inspect the second child and so forth.. – Neuron Apr 22 '18 at 17:44
  • If I understand correctly what the author wanted to achieve, the general case is: (1) "no" and (2) "yes", neither will work, but if you transform your node objects into a non-recursive form, you can use forGraph(). (from java doc) – Kamil Tomasz Jarmusik Apr 22 '18 at 17:46
  • for (Node n : TREE_TRAVERSER.depthFirstPreOrder(root)) structure is unmodifiable during iteration, so the full structure must be loaded? Forgive my inquisitiveness, but I check my knowledge and intuition. – Kamil Tomasz Jarmusik Apr 22 '18 at 17:55
  • @KamilTomaszJarmusik the nice thing about an `Iterable` is, that it doesn't need to know about all the objects ahead of time. It just provides an `Iterator` which will provide one object after another. It doesn't need to precompile the whole tree ahead of time! You can test this by setting up a simple example where you put a `print` statement into the `SuccessorsFunction`. You'll see it will only get called when that element is reached while traversing the tree! – Neuron Apr 22 '18 at 17:59
  • In for (Object object: objects) then during iteration I can not add or remove an item in the objects (eg List)... – Kamil Tomasz Jarmusik Apr 22 '18 at 18:05
  • @KamilTomaszJarmusik I can't explain to you how `Iterable`s and `Iterator`s work in detail here. You might want to find some tutorial online. For now, just compile and execute [this code](https://www.writeurl.com/text/1v9lf9kzkp66i539h94v/z0b5p1q8jihmf9fdgita). You will see that the nodes are only expanded as the are iterated over – Neuron Apr 22 '18 at 18:18
  • try: for (Node n : tree.depthFirstPreOrder(root)) { root.left = null; System.out.println("iterated over "+n.getName()); } for (Node n : tree.depthFirstPreOrder(root)) { System.out.println("_iterated over "+n.getName()); } – Kamil Tomasz Jarmusik Apr 22 '18 at 20:53
  • @KamilTomaszJarmusik what are you trying to show me? – Neuron Apr 22 '18 at 21:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/169546/discussion-between-lonely-neuron-and-kamil-tomasz-jarmusik). – Neuron Apr 22 '18 at 21:11
0

I would suggest taking tail recursions into account, since performance-wise this is a major factor :

public static Node getNode(Node currentNode, String name){
    // Base case: currentNode is null, nothing left to search
    if (currentNode == null) {
        return null;
    }

    Node retrieved = null;
    if (currentNode.name.equals(name)) {
        return currentNode;
    } else {
        // Tail recursions
        if(currentNode.left == null) {
            return getNode(currentNode.right, name);
        }
        else if(currentNode.right == null) {
            return getNode(currentNode.left, name);
        }
        // Non Tail recursion
        else {
        retrieved = getNode(currentNode.left, name);

            // If not found in left subtree, then search right subtree
            if (retrieved == null){
                retrieved = getNode(currentNode.right, name);
            }
        }
    }
    return retrieved;
}

Attached is the full code which was executed on an online compiler:

public class MyClass {
    static class Node {
    public String name;
    public Node left;
    public Node right;

    Node(String name) {
        this.name = name;
        right = null;
        left = null;
    }

    @Override
    public String toString() {
        return "name = " + name + " hasLeft = " + (left != null) + " hasRight = " + (right != null);
    }

}

static class Tree {

    Node root;

    public Node getRoot() {
        return root;
    }

    private Node addRecursive(Node current, String value) {
    if (current == null) {
        return new Node(value);
    }

    if (value.compareTo(current.name) < 0) {
        current.left = addRecursive(current.left, value);
    } else if (value.compareTo(current.name) > 0) {
        current.right = addRecursive(current.right, value);
    } else {
        // value already exists
        return current;
    }

    return current;
    }

    public Tree add(String value) {
        root = addRecursive(root, value);
        return this;
    }

    public void traverseInOrder(Node node) {
        if (node != null) {
            traverseInOrder(node.left);
            System.out.print(" " + node.name);
            traverseInOrder(node.right);
        }
    }

    public void traverseInOrder() {
        traverseInOrder(root);
         System.out.println("");
    }

}

public static void main(String args[]) {
    Tree tree = new Tree();
    tree.add("a").add("ab").add("bbb").add("cc").add("zolko").add("polip").traverseInOrder();

    Node found = getNode(tree.getRoot(),"vv");
    System.out.println(found);
    found = getNode(tree.getRoot(),"ab");
    System.out.println(found);
    found = getNode(tree.getRoot(),"polip");
    System.out.println(found);
    found = getNode(tree.getRoot(),"java");
    System.out.println(found);
    found = getNode(tree.getRoot(),"zolko");
    System.out.println(found);

}

public static Node getNode(Node currentNode, String name){
    // Base case: currentNode is null, nothing left to search
    if (currentNode == null) {
        return null;
    }

    Node retrieved = null;
    if (currentNode.name.equals(name)) {
        return currentNode;
    } else {
        // Tail recursions
        if(currentNode.left == null) {
            return getNode(currentNode.right, name);
        }
        else if(currentNode.right == null) {
            return getNode(currentNode.left, name);
        }
        // Non Tail recursion
        else {
        retrieved = getNode(currentNode.left, name);

            // If not found in left subtree, then search right subtree
            if (retrieved == null){
                retrieved = getNode(currentNode.right, name);
            }
        }
    }
    return retrieved;
}
}

And the outputs of the main method execution:

 a ab bbb cc polip zolko
null
name = ab hasLeft = false hasRight = true
name = polip hasLeft = false hasRight = false
null
name = zolko hasLeft = true hasRight = false
Rann Lifshitz
  • 4,040
  • 4
  • 22
  • 42