Skip to content

Instantly share code, notes, and snippets.

@Michael0x2a
Last active December 7, 2016 06:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Michael0x2a/49c25de442f2b4e30cd5d2d13ed40dfb to your computer and use it in GitHub Desktop.
Save Michael0x2a/49c25de442f2b4e30cd5d2d13ed40dfb to your computer and use it in GitHub Desktop.
removeMatchingLeaves-overview.md

removeMatchingLeaves

Overview

In section, we didn't quite have enough time to go over the removeMatchingLeaves problem. If you haven't already, I would go visit that link and read through the spec to make sure you understand what that problem is asking you.

To summarize, this problem is asking us to add a method to an IntTree. This method must accept another tree as a parameter, and must modify the current tree by removing all "matching leaves".

Later in this post, I'll talk about how we would write our method if the spec instead asked us to return a new tree where all matching leaves are removed in the new tree

Implementing the method

So, I think part of the challenge of this question is figuring out how exactly you would end up recursing over two trees.

We'll start with the method header:

public void removeMatchingLeaves(IntTree other) {

}

Notice that the type of the parameter is an IntTree, NOT an IntTreeNode. The IntTreeNode class is an internal detail of IntTrees, and so wouldn't be something the client can directly pass in, even if they wanted to.

However, within the method, we do want to access the root, which is an IntTreeNode:

public void removeMatchingLeaves(IntTree other) {
    this.root = this.helper(this.root, other.root);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {

}

Now, what I'll do is start by thinking of our different base cases. The first kind of base case I can think of is when either self or other is null. If either are null, we can't really continue recursing, after all.

The next base case is when self is a leaf node. If self is a leaf node, then we might need to do the "remove matching node" code, so it's a case we need to keep in mind.

Another possible base case is if other is a leaf node. However, if we think about this carefully, we really don't need to do anything special in that case, so we can omit that check.

Using this information, we know we might try structuring our code like so:

public void removeMatchingLeaves(IntTree other) {
    this.root = this.helper(this.root, other.root);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (self == null || other == null) {
        // One of the branches is null; stop recursing
    } else if (self.left == null && self.right == null) {
        // 'self' is a leaf node; do "remove matching" check
    } else {
        // continue recursing
    }
    return self;
}

Let's try filling this out backwards.

First, how do we continue recursing? Well, we can do the x = change(x) pattern thing:

public void removeMatchingLeaves(IntTree other) {
    this.root = this.helper(this.root, other.root);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (self == null || other == null) {
        // One of the branches is null; stop recursing
    } else if (self.left == null && self.right == null) {
        // 'self' is a leaf node; do "remove matching" check
    } else {
        // continue recursing
        self.left = this.helper(self.left, other.left);
        self.right = this.helper(self.right, other.right);
    }
    return self;
}

Next, we can do the "remove matching" check:

public void removeMatchingLeaves(IntTree other) {
    this.root = this.helper(this.root, other.root);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (self == null || other == null) {
        // One of the branches is null; stop recursing
    } else if (self.left == null && self.right == null) {
        // 'self' is a leaf node; do "remove matching" check
        if (self.data == other.data) {
            self = null;
        }
    } else {
        // continue recursing
        self.left = this.helper(self.left, other.left);
        self.right = this.helper(self.right, other.right);
    }
    return self;
}

And finally, we have to handle the "stop recursing" case.

One possible way we could do this is to try setting self to null, just like in the leaf case. However, this wouldn't work -- if other is null and self isn't, we still want to keep whatever subtree self is referring to.

Instead, ultimately what we want to do is to return self. This will end up doing the correct thing in all cases -- if self is null, we still return null, otherwise, we return that entire subtree.

However, as it turns out, our code is already doing this! We already return self, so we'd actually want to leave that branch empty.

This means that our final, working version of the code looks like this:

public void removeMatchingLeaves(IntTree other) {
    this.root = this.helper(this.root, other.root);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (self == null || other == null) {
        // One of the branches is null; stop recursing
    } else if (self.left == null && self.right == null) {
        // 'self' is a leaf node; do "remove matching" check
        if (self.data == other.data) {
            self = null;
        }
    } else {
        // continue recursing
        self.left = this.helper(self.left, other.left);
        self.right = this.helper(self.right, other.right);
    }
    return self;
}

Stylistic cleanup

That said, having an empty branch is pretty bad style. This won't matter for the final, since style doesn't matter there, but if you did feel like cleaning up the code, we would need to modify it to look like this:

public void removeMatchingLeaves(IntTree other) {
    this.root = this.helper(this.root, other.root);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (self != null && other != null) {
        if (self.left == null && self.right == null) {
            // 'self' is a leaf node; do "remove matching" check
            if (self.data == other.data) {
                self = null;
            }
        } else {
            // continue recursing
            self.left = this.helper(self.left, other.left);
            self.right = this.helper(self.right, other.right);
        }
    }
    return self;
}

Or alternatively, we could do something like this:

public void removeMatchingLeaves(IntTree other) {
    this.root = this.helper(this.root, other.root);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (self == null || other == null) {
        // One of the branches is null; stop recursing
        return self;
    } else if (self.left == null && self.right == null) {
        // 'self' is a leaf node; do "remove matching" check
        if (self.data == other.data) {
            self = null;
        } else {
            return self;
        }
    } else {
        // continue recursing
        self.left = this.helper(self.left, other.left);
        self.right = this.helper(self.right, other.right);
        return self;
    }
}

Variation: creating a new tree

The problem as written asked us to modify the original tree. But what if instead, it asked us to return a new tree? What would the code look like then?

The first thing we would need to change is the public pair to look like this:

public IntTree removeMatchingLeaves(IntTree other) {
    IntTreeNode newRoot = this.helper(this.root, other.root);
    return new IntTree(newRoot);
}

Notice that we're now have a return type of IntTree (NOT IntTreeNode!), and that we don't set this.root in any way.

Next, we need to make our private pair contantly create a new node, instead of modifying anything in the original tree.

Just like before, we'll start backwards and handle the recursive case. In that particular case, we want to create an entirely new node, using our helper method to find out what our new left and right branches should be.

public IntTree removeMatchingLeaves(IntTree other) {
    IntTreeNode newRoot = this.helper(this.root, other.root);
    return new IntTree(newRoot);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (self == null || other == null) {
        // One of the branches is null; stop recursing
    } else if (self.left == null && self.right == null) {
        // 'self' is a leaf node; do "remove matching" check
    } else {
        // continue recursing
        return new IntTreeNode(
                self.data,
                this.helper(self.left, other.left),
                this.helper(self.right, other.right));
    }
}

Next, the leaf case. We can handle that in almost the same way:

public IntTree removeMatchingLeaves(IntTree other) {
    IntTreeNode newRoot = this.helper(this.root, other.root);
    return new IntTree(newRoot);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (self == null || other == null) {
        // One of the branches is null; stop recursing
    } else if (self.left == null && self.right == null) {
        // 'self' is a leaf node; do "remove matching" check
        if (self.data == other.data) {
            return null;
        } else {
            return new IntTreeNode(self.data);
        }
    } else {
        // continue recursing
        return new IntTreeNode(
                self.data,
                this.helper(self.left, other.left),
                this.helper(self.right, other.right));
    }
}

And finally, the "what if either branch is null" check. This part is trickier because we can no longer simply return self -- we want to create an entirely new tree without sharing any nodes. (If we did share nodes, that would mean that modifying one tree later on would also modify the original, which seems like the sort of thing that would make the client pretty unhappy).

So now, the new behavior we want is to continue recursing down self if we can, creating new nodes. Or to rephrase, if other is null, we will always create a new node without doing any checks. That said, we will still eventually need to check if self is null or not, since we don't want to continue recursing when self is null.

We can express all this by slightly tweaking our if statement condition and adding a new if/else, like so:

public IntTree removeMatchingLeaves(IntTree other) {
    IntTreeNode newRoot = this.helper(this.root, other.root);
    return new IntTree(newRoot);
}

private IntTreeNode helper(IntTreeNode self, IntTreeNode other) {
    if (other == null) {
        // 'other' is null, copy the 'self' subtree
        if (self == null) {
            // Nothing to copy, return null
            return null;
        } else {
            // Recurse down 'self'
            return new IntTreeNode(
                self.data,
                this.helper(self.left, null),
                this.helper(self.right, null));
        }
    } else if (self.left == null && self.right == null) {
        // 'self' is a leaf node; do "remove matching" check
        if (self.data == other.data) {
            return null;
        } else {
            return new IntTreeNode(self.data);
        }
    } else {
        // continue recursing
        return new IntTreeNode(
                self.data,
                this.helper(self.left, other.left),
                this.helper(self.right, other.right));
    }
}

Stylistically, this code doesn't seem very clean -- having this many if/else conditions and stuff feels a bit messy.

But again, in the final, we don't care about style, so it would be fine to stop here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment