Screwing up the technical interview – common mistakes

In my previous post, I described a simple interview question and the various approaches different types of programmers take. These weren’t mistakes per se, but they revealed biases and were good indicators of potential future problems, or holes in a programmer’s mental map. Like the candidate who sees everything in terms of recursion (or avoids it at all cost), or who doesn’t mind implicit memory allocation for an object when a primitive will do.

These are interesting and all, but at the end of the day they’re color commentary. The real game is with the deal breakers, the mistakes that will cost a candidate a job. Some of these pop up with such depressing regularity that I can feel myself tensing up as a candidate comes to a decision point… and only letting go my breath when they’re safely through (or groaning internally if they don’t). If you fundamentally can’t program (and there are a lot of people like that out there), there’s not much I can do to help. But if your core problem-solving skills are solid, and your code is reasonably clean, then there are some things you can watch out for which will dramatically improve your coding (not just in interviews). Think of it as a checklist you should run down every time you finish a piece of code (or review someone else’s).

Ignoring Requirements

I’ve asked variations of the same question many, many times, to the point that I’ve honed the wording down to a point. There are two requirements, each of which I repeat multiple times. I ask for confirmation that the candidate has understood each requirement after stating them. Understanding the question isn’t meant to be part of the challenge – I’m honestly just interested in seeing how they code it – but somehow a good 50% of the candidates will ignore the first requirement and plow right on to the second. When it’s clear they aren’t going to remember on their own (there’s a point at which it’s obvious), I try to remind them, first gently, then explicitly. At this point, some will slap foreheads with palms, some will gaze at me in confusion, and some will try to convince me that the first requirement either doesn’t make sense, or is so trivial as to be unnecessary for them to demonstrate. (N.B. the last two arguments are just going to annoy your interviewer)

Different interviewers will weight this differently. If you forget a requirement, some will immediately write you off, some won’t care. Some interviewers even intentionally leave the requirements vague and confusing, because they want to see how you’ll react – I strongly dislike this approach, but you don’t get to pick your interviewer. The best thing you can do at this point is to apply palm to forehead, take it in good humor (no matter how you’re feeling inside), make sure you understand the missed requirement, and code it up. The best possible result is that the interviewer decides you’re scrappy, thinks you react well to adversity, and chooses to overlook the mishap. The worst possibility is that they think you’re difficult to work with, ignore requirements, sloppy, and are going to need constant oversight and handholding.

Missing Edge Cases / Off-by-One Errors

First, an aside. When did people stop saying “edge cases” and switch to “corner cases”? I just can’t get myself to switch – “corner cases” sounds way too precious. (Get off my lawn!)

Anyway, every interview, every single one, is going to provide the opportunity for you to miss edge cases, or make off-by-one errors. Did the candidate check to confirm that the initial case works? Did they check all the possible inputs? Do they handle them appropriately? Different interviewers take more or less stringent stands on this, but for the love of Mel, please just make it a standard part of your coding process to make sure you’ve covered all the cases before saying you’re done.

Naming Local Variables or Parameters the Same as Member Variables

There aren’t many absolute rules in programming. We’re a pretty argumentative bunch, and there’s generally someone willing to come up with an example of why a particular construct or idea might occasionally have a place in the toolbox, though perhaps it should typically be avoided. However, I don’t think I’m going out on a limb when I say that there is never, ever a good reason to name a local variable or parameter the same thing as a member variable (ok, ok, except possibly in a constructor or mutator). Even though scoping rules allow this, and even though your code may work, it will be a maintainability problem, and will potentially cause subtle, soul-crushingly difficult to find errors. There’s no good reason to name them this way, and there are plenty of good workarounds. Just name your local variables/parameters something else.

Starting recursion with an object’s child(ren)

In recursion, the typical approach is something like this:

if (base case) {
    // do something special - take care of the base case
}
else {
    // call the recursive method
}

I.e., first take care of the base case, then call the recursive function on the first element. But for some reason, people frequently do this:

if (base case) {
    // do something special - take care of the base case
}
else {
    // treat the first item as a special case, THEN call the recursive method with the second object
}

As an example, here’s (bad) code for a recursive append:

void append(int value) {
    if (head_node == null) {
        head_node = new Node(value);
    }
    else {
        if (head_node.next == null) {
            head_node.next = new Node(value);
        }
        else {
            append_recursive(head_node.next,value);
        }
    }
}

void append_recursive(Node n, int value) {
    if (n.next == null) {
        n.next = new Node(value);
    }
    else {
        append_recursive(n.next,value);
    }
}

In this example, there’s no reason to special case head_node.next, but for some reason this tends to pop up a lot. This ends up creating a lot of duplicate code (which is a maintainability problem), and indicates that the candidate is uncomfortable with recursion. It can also create bugs, as the candidate frequently forgets to handle the first object, or writes two versions of the same code, but only remembers to change one when debugging later. You almost always want to start on your first object, not its children.

Forgetting else

It seems like such a small thing, but recently I’ve been seeing a lot of people forget to separate if statements with else, ending up with:

if (condition1) { ... }
if (condition2) { ... }

Furthermore, this is usually when condition1 == !condition2, which makes this doubly strange – why not just use else? In addition to being harder to maintain and less efficient, this frequently causes significant problems, as in the following code.

while (node != null) {
  if (x != node.value) {
    node = node.next;
  }
  if (x == node.value) { // the opposite of the above condition
    return node;
  }
}
return null;

In this code, we’re searching for the node with value x in a linked list – if we don’t find the value, we want to return null. However, this code will always cause a null pointer exception when the value isn’t found, because node will be null when it gets to the second if statement. This is the kind of error that smacks of a very junior programmer – someone who hasn’t been coding for so long that writing else is almost like a nervous tic. Or, in other words, something you can explain away for an entry-level candidate, but which indicates concerns about a more senior candidate’s coding skills.

Misunderstanding Pointers

The most common error I see is also one of the most serious, as it indicates a serious misunderstanding on the part of the candidate about how pointers work. Consider the following code for appending a value to the end of a linked list (pretending for the moment that we didn’t keep a pointer to the end of the list):

if (head == null) {
    head = new Node(value);
    return;
}
Node n = head;
while (n.next != null) {
    n = n.next;
}
n.next = new Node(value);

First we take care of the base case, then traverse to the last node, and finally append a new value. This is pretty simple code, and should be a gimme, but a disturbingly large percentage of candidates will try to do something like this:

Node n = head;
while (n != null) {
    n = n.next;
}
n = new Node(value);

Or this:

Node n = findInsertLocation(head);
n = new Node(value);

...
private static Node findInsertLocation(Node n) {
    if (n == null)
        return n;
    else
        return findInsertLocation(n.next);
}

All of the searching code collapses to “n = null”, at which point its relationship to the data is gone. However, some candidates will persist in trying to convince me that the local variable (or parameter) maintains a mystical connection to the variable it was originally set to, and that setting it to a new value will affect the other variable. This is particulary frustrating,

I remember all the handwringing about the shift from teaching C++ to Java, and how this was going to damage programmers’ understanding of how pointers worked. I’m here to tell you that everyone, even C and C++ programmers, make this mistake. This doesn’t make any sense to me, but there you have it.

Getting Creative

And then there are the “special” solutions. These are the ones your interviewer gets to tell stories about years later. For instance, I once had someone represent a nullable value in a node class by giving every node a linked list member variable – an empty linked list represented null, a linked list with one element represented a non-null value. Wow. Just, wow. But seriously, when you feel like you’re going to have to do something unutterably bizarre to solve a problem, think twice. A good interview question is easy for a great candidate, impossible for a bad one. There just isn’t that much time in an interview to do big questions requiring lots of code, especially since interviewers frequently like to build from easier to more difficult questions, and also leave time for your questions. Solutions that require massive amounts of code, or development of completely unrelated features before even getting started, are probably not what the interviewer is looking for. This is a guideline, not a rule, but when your code starts getting crazy, it’s probably an indication that you’re going off the rails.

Next up: what if my resume sucks, and I can’t get an interview?

One thought on “Screwing up the technical interview – common mistakes

  1. Great Post!

    I’ve seen all of these above, but I frequently see more basic problems:
    * didn’t enumerate test cases. Makes me wonder if they know about TDD or even unit testing
    * didn’t actually debug the problem before claiming to be done

    And a little more advanced:
    * was not prepared to talk about the performance of the solution, and whether it would scale for massive inputs (one of my favorite follow on questions) or extremely limited memory, etc.

    And just scary:
    * spending forever on problem without asking any clarifiying questions. One candidate spent more than 2 hours on a (admittedly hard) problem, refused help and then admitted they had no solution at all, nothing.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s