Linked Lists Aren't Hard. These Six Bugs Are.

June 6, 202610 min read
dsaalgorithmsinterview-prepleetcode
TL;DR
  • Save the forward link first: store nxt = curr.next before any pointer gets overwritten, or every node past curr becomes unreachable
  • Three critical edge cases: test empty, single-node, and two-node inputs before every submission — each exposes a different class of linked list bug
  • Dummy node pattern: use a dummy predecessor whenever the head can change; always return dummy.next, not dummy
  • Midpoint does not split the list: after finding midpoint with fast/slow pointers, set slow.next = None to actually sever the two halves before reversing
  • Floyd's null guard: the correct check is fast is not None and fast.next is not None — skipping the middle condition crashes on even-length acyclic lists
  • n+1 gap for deletion: to delete the nth node from the end, advance fast by n+1 steps so slow lands on the predecessor, not the target node

Linked list problems are supposed to be the warm-up round. The interviewer hands you a reversal with the energy of someone offering you a participation trophy, and expects a clean solution in ten minutes. No graph theory. No DP transitions. No twenty-minute derivation of a recurrence relation. Just nodes and the basic ability to not lose your place.

So it is genuinely embarrassing how often the same bugs show up.

The bugs aren't random. They cluster into the same six failure modes, across reversal problems, merge problems, cycle detection, and palindrome checks. Once you see the structure behind each one, you stop being surprised by it. Until then, you're staring at submission four of a three-line reversal wondering what is happening to you.

Save the Forward Link Before You Touch Anything

The most frequent offender in reversals, and the easiest one to write by accident. You have prev, curr, and nxt. You know exactly what they're for. And then you write this:

curr.next = prev # overwrite the forward link nxt = curr.next # too late: curr.next is now prev, not the original next

You have destroyed the only pointer leading forward in the list. curr.next now points backward to prev. Saving nxt afterward gives you prev again. Every node past curr is gone, floating in memory, waiting to be garbage collected while your reversal silently produces a one-node list.

The forward link is gone the moment you overwrite it. The fix is one line moved up:

nxt = curr.next # save the forward link FIRST curr.next = prev # now safe to overwrite prev = curr curr = nxt

Same four operations. Different order. The save must come before any assignment that touches curr.next.

Diagram showing wrong vs. correct pointer save order

Saving nxt after curr.next = prev gives you prev. Not next. Never next.

This mistake has a second form that catches people who actually know recursion: the recursive reversal that forgets to return the new head. The recursion correctly reverses the sublist. The function returns head. The original head is now the tail. You return a pointer to it. The caller gets a one-node list and you get to spend the next three minutes staring at output that looks correct except for everything past the first node.

Both are the same underlying problem: reasoning about what each pointer holds after you have modified things. When in doubt, write it out. Put the state of each pointer on paper before writing any code. It feels slow. It is slow. It is still faster than debugging.

Three Inputs That Break Everything Else

Empty list. Single node. Two nodes.

These three inputs cover almost every class of linked list failure, and most candidates mentally check "edge cases handled" after thinking about the first one.

Empty list is obvious: is head null? Any dereference of head.next crashes immediately. But candidates who know this will often set up the right null check, feel good about themselves, and stop there.

Single node catches operations that assume a predecessor or a successor. Remove-a-node, find-middle, detect-cycle: all behave differently with exactly one node. head.next is null. There is no next. Algorithms that step forward from head have nowhere to go.

Two nodes is the one candidates skip most often, and it is the one that matters most. A two-node list is the minimal case for reversal (the head becomes the tail, and if your code is wrong you will find out immediately), for cycle detection (fast starts one step ahead of slow), and for finding the middle (which node counts as "middle" depends entirely on whether you initialize fast to head or head.next).

The two-node case is not a formality. It is where "basically works" and "actually works" diverge. Run through all three inputs before writing a loop, not as a ritual, but because your loop termination condition and your pointer initialization both depend on whether they handle these correctly.

Use a Dummy Node or Write Twice as Much Code

If your operation might touch the head of the list, use a dummy node.

Without a dummy, you get the branching problem. Inserting before the head and inserting in the middle require separate code paths. Deleting the head and deleting any other node require separate code paths. Every boundary case gets its own if block. Every if block is another place a bug can hide. You end up with more special-case code than regular code, which is a sign you have taken a wrong turn somewhere.

A dummy node gives every position a valid predecessor. You make a node with an arbitrary value, point it at the real head, and now every operation looks the same at every position. No special cases at the front.

dummy = ListNode(0) dummy.next = head curr = dummy # ... do work ... return dummy.next # critical: return dummy.next, not dummy

The most common dummy-node mistake is returning dummy instead of dummy.next at the end. You built the real list starting one node past the dummy. Returning dummy hands the caller a phantom zero-valued node at the front. The list is wrong, it is subtle, and the candidates who make this mistake are often the same ones who were quietly pleased with themselves for remembering the dummy node technique at all.

The technique applies everywhere: merge sorted lists, remove nth from end, partition a list, flatten a multi-level list. Any time you find yourself typing if curr == head: special case, reach for a dummy instead. Two lines to set it up. One line to return correctly.

Finding the Midpoint Doesn't Split the List

This one bites experienced candidates. You have seen the pattern before. You find the midpoint, reverse the second half, compare both halves. You get an infinite loop on even-length inputs, or a wrong answer on the palindrome check, and you genuinely cannot figure out why because you have definitely solved this problem before.

Here is what is happening. In LeetCode 234, after finding the midpoint:

1 -> 2 -> 2 -> 1
          ^
         slow (midpoint for 4-node list)

You reverse starting from slow.next. The reversed second half correctly points to null at its tail. But slow.next still points forward into the original second half. The two halves are still connected. When you traverse both halves in parallel, you eventually follow that stale pointer and revisit nodes from the first half. On even-length lists, you cycle.

The fix is one line: slow.next = None before you start reversing. You are splitting the list into two independent halves. Without that cut, you have two pointers into one continuous structure, and the reversal creates a tangle that is unpleasant to reason through under normal conditions and genuinely painful under time pressure.

The same bug appears in LeetCode 143 (reorder list) and anywhere you find the midpoint and then operate on both halves independently. Finding the midpoint is step one. Nulling the forward link from the first half's tail is step two. Both steps.

Floyd's Algorithm: The Guard Is Probably Wrong

Cycle detection with Floyd's is well-known enough that most candidates get the structure right: slow moves one step, fast moves two, they meet inside the cycle if one exists.

The bug is in the guard. This one is particularly unfair because the algorithm is correct. The implementation throws not on a cyclic list, but on a clean list with no cycle at all.

The wrong guard:

while fast is not None and fast.next.next is not None:

If fast is the last node in an even-length list, fast.next is null. Accessing fast.next.next throws. Not a wrong answer. An actual runtime error on the exact input where the algorithm should trivially return "no cycle." Correctly implemented cycle detection failing to detect the absence of a cycle is a special kind of bug.

The correct guard:

while fast is not None and fast.next is not None: slow = slow.next fast = fast.next.next

Check fast, then fast.next, then advance. Both conditions protect a different step. The first stops you from dereferencing null when you've fallen off the end. The second stops you from dereferencing the next pointer of the last node. Skip either one and you get a runtime error on the easiest possible test case.

The same guard applies to any problem that advances a pointer two steps at a time. Always fast is not None and fast.next is not None. For a deeper look at why Floyd's works at all, the Floyd's cycle detection algorithm post walks through the math.

Nth From End: The Gap Is n+1, Not n

Remove the nth node from the end (LeetCode 19). The standard approach: advance fast by n steps, then advance both fast and slow together until fast hits null. slow ends up at the target node.

This finds the target. It does not delete it. To delete a node in a linked list, you need its predecessor. With a gap of n, slow sits at the node you want to remove. You can read it. You cannot unlink it from the list without a pointer to the node before it.

The gap should be n+1, not n.

dummy = ListNode(0) dummy.next = head fast = dummy slow = dummy for _ in range(n + 1): # n+1, not n fast = fast.next while fast is not None: fast = fast.next slow = slow.next slow.next = slow.next.next return dummy.next

When the gap is n+1, slow lands one step earlier than the target. That is exactly where you need to be to execute slow.next = slow.next.next. The dummy node here does double duty: it gives slow a valid starting position and handles the case where the head itself is the node to delete.

Thirty seconds to reason through once. An unclear amount of time to discover by trial and error. The two pointer technique covers the gap pattern in more depth.


If you want to practice these patterns under actual interview conditions, SpaceComplexity runs voice-based DSA mock interviews where you walk through pointer logic out loud. Explaining n+1 versus n to an AI interviewer that asks follow-up questions is a reliable way to find out whether you understand it or just memorized it.

Six Linked List Bugs to Check Before You Submit

  • Save the forward link before any pointer gets overwritten
  • Test empty, single-node, and two-node cases explicitly
  • Use a dummy node whenever the head might be affected; return dummy.next
  • Null out slow.next after finding the midpoint before reversing
  • Guard cycle detection as fast is not None and fast.next is not None
  • For nth-from-end deletion, the gap is n+1, not n

Further Reading