Thank you for posing this delightfully tricky interview question. Before writing this review, I felt compelled to try coding it myself — on paper. It took me about an hour, and I made three mistakes.1 I've posted my solution below, with comments added after the fact. (In an interview, commentary would probably have been presented verbally.)
Evaluation criteria
If I were asking this question as an interviewer, here's what I would look for in a candidate's response. Of course, these criteria reflect my own abilities and preferences.
Reasonable syntax. If writing the solution on a computer, the code must compile with no warnings and no suppressed warnings. If writing the solution on a whiteboard, it should "visually compile" with only minor syntax errors.
Your code compiles with warnings about Generics.
One-pass O(n) solution. The solution, whether correct or not, must attempt to complete the task by traversing the list just once. If the candidate fails to produce code that accomplishes the task in one pass, I would be lenient if he/she spontaneously apologized for using a suboptimal algorithm. (In that case, I might ask the candidate to try again at the end of the interview, schedule permitting.)
For an interview, I would value a slightly buggy one-pass O(n) algorithm over a bug-free but inefficient algorithm. I believe that the candidate should quickly recognize that a one-pass solution is possible. Minor bugs are inevitable, but picking an inefficient strategy would just be going off in the wrong direction altogether.
In my opinion, a candidate who produces an inefficient algorithm should not be hired to develop production code: his/her work would probably require subsequent intervention by other developers to fix performance problems. However, such a programmer could still be useful for maintenance work or writing code for internal use (such as QA tests).
Your findSize()
and findEnd()
helper methods both walk the list to the end. Since your findEnd()
is called from within a loop, your algorithm must not be O(n), much less a one-pass O(n).
Intelligent interaction. I would like to see that the candidate asks questions to seek clarification before and during the coding process. Some of the questions I would ask are listed below.
General cluefulness. A solution should be reasonably concise, and each chunk of code should be purposeful. By purposeful, I mean that I should be able to point to a chunk of code, and the candidate should be able to explain, preferably using diagrams, what the data structure looks like before executing that chunk, and the result after executing that chunk.
To be honest, your solution is too complex for me to follow in my head. If presented with your solution, I'd just challenge you with some questions instead:
- Is this an iterative or recursive solution? Why did you pick that over the alternative?
- What kind of running time would you expect?
- You have a
cursor
and a current
. Isn't current
a kind of cursor as well? Could you come up with more purposeful names?
- What is the
Queue
for?
- Before entering the first while-loop, there appear to be four base cases. Is that reasonable number of base cases? Could you get away with fewer base cases?
- Can you explain the loop condition
while (cursor != null || cursor.next != null)
? Would there ever be a situation where cursor
is null
but cursor.next
is not?
Hopefully, those questions would trigger some "Aha!" or "Oops!" moments and result in some improvements. To be frank, though, your code is way too complex and confusing to be salvageable through a Socratic dialogue.
Interviewee strategy
For this question, I would prefer recursion to iteration. Recursive solutions are often more succinct and involve fewer variables. Recursion enforces some rigour in your thinking process, by making you think about preconditions, postconditions, and invariants. Recursion works well for this linked-list question precisely because invariants exist: walk k nodes down the list, and the situation looks similar.
Production Java code often avoids recursion due to practicalities such as the function-call overhead and worries about stack overflow. However, interview questions are naturally an academic exercise. Just in case the interviewer is prejudiced against recursion, ask two questions:
- "Will the input be small enough such that the stack overflow is not a concern?"
- "How about a recursive solution? I can rewrite it as an iterative solution afterwards, if you prefer."
Keep it simple. There are other things you can get away when coding in an interview, especially if coding on a whiteboard. Just keep talking.
import java.util.*;
"Of course, in production code, I would list individual classes."
- Naked linked list nodes. "Normally, I'd hide the nodes within the linked list. To save time, we'll just say that a node is a list. OK?"
- Public fields. "Normally, there would be getters and setters. I'll take a shortcut and expose
.value
and .next
. However, to enforce your rule that nodes should have their pointers and not values manipulated, I'll make .value
final
. OK?"
Think out loud. Draw diagrams. Ask questions, especially questions that clarify the requirements. Score some points before writing a single line of code.
My ideal
Here's my solution, for comparison. The heart of the implementation, the kReverse()
helper, is only about a dozen lines, if you strip out assertions and comments.
// Import * not advisable in production code, but good enough for interview code
import java.util.*;
import static java.lang.System.out;
public class LinkedListNode<T> {
public final T value;
private LinkedListNode<T> next;
public LinkedListNode(T value, LinkedListNode<T> next) {
this.value = value;
this.next = next;
}
public static <T> LinkedListNode<T> makeList(List<T> list) {
LinkedListNode<T> head = null;
for (int i = list.size() - 1; i >= 0; i--) {
head = new LinkedListNode<T>(list.get(i), head);
}
return head;
}
/**
* Stringifies entire list for diagnostics.
*/
public String toString() {
StringBuilder sb = new StringBuilder(this.value.toString());
for (LinkedListNode<T> n = next; n != null; n = n.next) {
sb.append(" -> ").append(n.value.toString());
}
return sb.toString();
}
public static <T> LinkedListNode<T> kReverse(LinkedListNode<T> head, int k) {
if (k < 1) throw new IllegalArgumentException("k = " + k);
return kReverse(head, k, head, null, k);
}
/**
* Sets the <tt>.next</tt> pointer of the <tt>head</tt> node and all
* of its successors to perform k-group reversal of a list.
*
* @param head The current node being processed
* @param k Chunk size, as specified in the problem
* @param group The original head of this k-group
* @param prev The original predecessor of head
* @param remain The number of nodes remaining to be processed in this
* k-group, including head
*
* @return The new leading node of the current k-group (which could be null
* if the current k-group is empty)
*/
private static <T> LinkedListNode<T> kReverse(LinkedListNode<T> head, int k,
LinkedListNode<T> group,
LinkedListNode<T> prev,
int remain) {
assert remain > 0;
if (null == head) {
// Incomplete or empty group.
return group;
} else if (1 == remain) {
// head is the last node of the k-group, and will become the
// leading node of the reversed k-group.
// Reverse the next k-group (and by recursion, all subsequent
// k-groups). n is the leading node of the next k-group after
// its reversal is complete (possibly null).
LinkedListNode<T> n = kReverse(head.next, k, head.next, null, k);
// The next two statements must be in this order, because if
// k == 1, head and group are the same.
head.next = prev;
group.next = n;
return head;
} else {
// head not the last node of the k-group. n is the leading node of
// this k-group after reversal: the same as the old leading node if
// this k-group is incomplete, or the same as the old trailing node
// of this k-group if reversal succeeds.
LinkedListNode<T> n = kReverse(head.next, k, group, head, remain - 1);
// group != n means this k-group has k members and is being reversed.
if (group != n && head != group) {
// Don't set head.next, though, if head is the old leading node,
// i.e., the new trailing node, since it was already set in
// the (1 == remain) case above.
head.next = prev;
}
return n;
}
}
/**
* Tests
*/
public static void main(String[] args) {
List<Integer> oneFive = Arrays.asList(new Integer[] {1, 2, 3, 4, 5});
// No-op expected...
out.println(kReverse(LinkedListNode.makeList(oneFive), 1));
out.println(kReverse(LinkedListNode.makeList(oneFive), 2));
out.println(kReverse(LinkedListNode.makeList(oneFive), 3));
out.println(kReverse(LinkedListNode.makeList(oneFive), 4));
out.println(kReverse(LinkedListNode.makeList(oneFive), 5));
out.println(kReverse(LinkedListNode.makeList(oneFive), 6));
// IllegalArgumentException expected...
out.println(kReverse(LinkedListNode.makeList(oneFive), 0));
}
}
As stated above, comments and JavaDoc were added just before posting the code; I wouldn't have produced such nice comments in a time-limited situation.
This is, of course, not the only good solution possible. However, as you can see, your implementation is quite far off.
1 The three mistakes I made were:
- Wrote
static LinkedListNode<T> function(…)
instead of static <T> LinkedListNode<T> function(…)
- Forgot the
head != group
condition near the end of kReverse()
- Silly mistake when writing
main()
.