Understand
30-second explanation
Build the interview-ready mental model before comparing implementations.
Use two pointers separated by a gap of n nodes, both starting from a dummy node placed before the head. Advance fast by n steps first, then move fast and slow together until fast hits the end. Slow now sits exactly one node before the target, so slow.next = slow.next.next splices it out. The dummy node makes removing the head itself a non-special case. One pass, O(n) time, O(1) space.
Interview contextWhat is testedWhere candidates slipHow to stand outExplore interview contextHide interview context
What's being tested
Whether you can do it in one pass with the gap-of-n two-pointer trick, and whether you use a dummy node to handle removing the head without a special branch.
Where candidates slip
Stopping slow on the target instead of one before it, or not using a dummy node and crashing/returning wrong when the head is removed.
How to stand out
Do it in a single pass, justify the dummy node by walking through the 'remove the head' case, and confirm the loop condition leaves slow exactly one node before the target.
Choose
Compare the available approaches
Use constraints and trade-offs to select the route you can defend in an interview.
Interactive decision treeInteractiveChoose the right approachFirst decision previewRemove nth node from the endOne pass required?YesNo / simplerExplore the full treeCollapse the tree
Learn & trace
Build each solution step by step
Move from the core idea to a visual trace, implementation, proof, and edge cases.
Approach 1 of 2
Two Passes — Compute Length, Then Delete
1Hint 1Explore hintHide hint
If you knew the list's length L, which position from the front is the nth node from the end?
2Hint 2Explore hintHide hint
The nth-from-end node is at index L - n from the front (0-based). To delete it you need the node just before it, at index L - n - 1.
3Hint 3Explore hintHide hint
Pass 1: walk the whole list to get L. Pass 2: from a dummy, step L - n times to reach the predecessor, then splice.
A · Core idea
We turn 'nth from the end' into 'a known index from the front' by first measuring the list.
Pass 1 — measure. Walk from head to null counting nodes; call the total L. Now the node to remove is the (L − n)-th from the front (0-based), and its predecessor is at index L − n − 1.
Pass 2 — delete. Start at a dummy node placed before the head and step forward L − n times. That lands us on the predecessor of the target (the dummy contributes the −1 so the arithmetic is clean even when the head is removed). A single prev.next = prev.next.next unlinks the target.
Why the dummy still matters here. If the head is the node to remove (n == L), then L − n = 0 and we step zero times, leaving us on the dummy whose next is the head — exactly the predecessor we need. Without the dummy we'd special-case head = head.next.
Why we still prefer one pass. This is perfectly correct and easy to reason about, but it touches the list twice. The follow-up explicitly asks for a single pass, which the gap-based two-pointer method delivers — same complexity class, one traversal.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
head = [1, 2, 3, 4, 5], n = 2pass 1
count nodes
L = 5[1, 2, 3, 5]C · Code with teaching comments
Reference implementation previewpublic class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; }Explore the full codeHide the full code
D · Why it works and complexityO(n) timeO(1) spaceOpen the invariant and proofExplore the proofHide the proof
Converting an end-relative index into a front-relative one via the length is the most direct reading of the problem. It's the baseline that motivates the slicker single-pass trick, and the dummy node already earns its keep on the head-removal case.
ComplexityO(n) time, O(1) space — Two linear scans (length, then predecessor) → O(2n) = O(n) time. Only a counter and a couple of pointers → O(1) space.
E · Edge cases and pitfalls3Off-by-one: stepping length - n - 1 or length - n + 1 instead of length - n from the dummy.Explore edge casesHide edge cases
Pitfalls
- Off-by-one: stepping length - n - 1 or length - n + 1 instead of length - n from the dummy.
- Not using a dummy and mishandling removal of the head node.
- Computing length but forgetting the head could be the target.
Approach 2 of 2 · Optimal
One Pass — Two Pointers with a Gap of n (Optimal)
1Hint 1Explore hintHide hint
You want the node n from the end, but you only discover the end by walking there. What if a second pointer were already n nodes ahead of the first?
2Hint 2Explore hintHide hint
Put both pointers on a dummy node before the head. Advance fast n steps to create a gap of exactly n.
3Hint 3Explore hintHide hint
Now move fast and slow together until fast.next is null. The gap is preserved, so slow stops exactly one node before the target — splice with slow.next = slow.next.next.
A · Core idea
We maintain a constant gap of n nodes between two pointers so that the moment fast reaches the end, slow is automatically parked at the right spot.
Set up the dummy and the gap. Both slow and fast start at a dummy node placed before head. We advance fast exactly n times. Now fast is n nodes ahead of slow.
March in lockstep. Move both pointers one step at a time while fast.next != null. Because the gap never changes, when fast lands on the last node, slow is sitting exactly n + 1 nodes from the end counting from the dummy — i.e. on the node immediately before the nth-from-end target. That predecessor position is precisely what we need to splice.
Why `fast.next != null` and not `fast != null`. We want slow to stop on the predecessor, not on the target itself. Stopping fast on the last node (rather than walking it to null) leaves slow one short of the target — exactly one node before it. Using fast != null would overshoot slow by one and delete the wrong node.
Why the dummy is essential. Consider n == length (remove the head). The gap setup puts fast on the last node right away while slow is still on the dummy; the lockstep loop doesn't run, and slow.next = slow.next.next neatly drops the head. Returning dummy.next then yields the new head. No special-casing — the dummy absorbs the edge.
B · Visual dry run
Interactive dry run
Select a frame to connect the decision with its visual state.
head = [1, 2, 3, 4, 5], n = 2Array state
gap opened
fast advanced n=2 from dummy
[1, 2, 3, 5]C · Code with teaching comments
D · Why it works and complexityO(n) timeO(1) spaceOpen the invariant and proofExplore the proofHide the proof
A fixed gap between two pointers is a portable trick: it answers 'nth from the end' without ever computing the length. Pair it with a dummy node and head deletion stops being a special case — the two ideas together make linked-list editing branch-free.
ComplexityO(n) time, O(1) space — fast traverses the list once (n steps to open the gap plus the rest), slow trails it — a single combined pass of O(n). Two pointers and a dummy → O(1) space.
E · Edge cases and pitfalls6head = [1], n = 1 — fast steps to node 1; loop skipped (fast.next null); slow on dummy; slow.next = null → returns empty list.Explore edge casesHide edge cases
Edge cases handled
head = [1], n = 1fast steps to node 1; loop skipped (fast.next null); slow on dummy; slow.next = null → returns empty list.head = [1, 2], n = 2fast steps 2 from dummy to node 2; loop skipped since fast.next is null; slow stays on dummy → removes the head, returns [2].head = [1, 2, 3], n = 3Remove the head: fast ends on last node immediately, slow stays on dummy, dummy.next becomes node 2.
Pitfalls
- Using while (fast != null) instead of fast.next — slow overshoots and deletes the wrong node.
- Omitting the dummy and crashing when the head must be removed.
- Advancing fast n+1 or n-1 times, breaking the gap arithmetic.
Practice
Try the solution yourself
Edit the optimal implementation, test another input, and observe the result.
Editable code previewpublic ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(0, head); ListNode slow = dummy, fast = dummy; for (int i = 0; i < n; i++) { fast = fast.next;Explore the playgroundClose the playground
Avoid mistakes
Recognize the interview traps
Understand why common attempts fail before reviewing the corrected implementation.
1Wrong loop condition lands slow on the target
To splice you need the node BEFORE the target. Walking fast all the way to null moves slow one node too far.
Explore the code comparisonHide the code comparison
Wrong loop condition lands slow on the target
To splice you need the node BEFORE the target. Walking fast all the way to null moves slow one node too far.
Don't write this
for (int i = 0; i < n; i++) fast = fast.next;
while (fast != null) { // overshoots
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next; // slow is now ON the targetWrite this instead
for (int i = 0; i < n; i++) fast = fast.next;
while (fast.next != null) {
slow = slow.next;
fast = fast.next;
}
slow.next = slow.next.next;2No dummy node breaks head removal
If the head is the nth-from-end node, there's no predecessor to splice from. A dummy gives the head a predecessor.
Explore the code comparisonHide the code comparison
No dummy node breaks head removal
If the head is the nth-from-end node, there's no predecessor to splice from. A dummy gives the head a predecessor.
Don't write this
slow = fast = head
for _ in range(n):
fast = fast.next
while fast.next:
slow = slow.next
fast = fast.next
slow.next = slow.next.next # fails / wrong when head is the target
return headWrite this instead
dummy = ListNode(0, head)
slow = fast = dummy
for _ in range(n):
fast = fast.next
while fast.next:
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return dummy.nextRevise
Carry the pattern into the interview
Pattern to remember
Two pointers with a fixed gap of n + a dummy node
Code pattern to memorize
dummy = ListNode(0, head)
slow = fast = dummy
for _ in range(n): fast = fast.next # open the gap
while fast.next: slow = slow.next; fast = fast.next
slow.next = slow.next.next # unlink target
return dummy.nextRules to remember
- 1Anchor both pointers at a dummy before head so deleting the first node uses the same code path.
- 2Advance
fastexactly n steps to open the gap, then move both untilfast.nextis null —slowlands just before the target. - 3Return
dummy.next, nothead— the head may have been the node you removed. - 4Loop condition is
fast.next != null(notfast != null) soslowstops one node before the target, not on it. - 5The gap of n between slow and fast is the whole trick: it converts 'nth from the end' into 'nth ahead of slow'.
Use this pattern when
- Delete or locate a node a fixed distance from the end in a single pass
- Any 'nth from the end' query on a singly linked list without precomputing the length
- Sliding a fixed-width window of pointers along a list
- Problems where a dummy node removes head-deletion edge cases
Choose another pattern when
- You already know the length cheaply → a single index from the front is simpler
- It's a doubly linked list with a tail pointer → walk back n from the tail directly
- You need every node's distance-from-end, not just one → a length pass may be clearer
Interview takeaway
Opening a fixed gap between two pointers turns a two-pass length-then-delete into one clean pass. The dummy-node trick that neutralises head deletion shows up across linked-list editing problems.
Continue
Transfer the pattern to another problem
Use a direct variation or pattern sibling to prove the idea is reusable.
Transferable patternFixed-gap two pointers plus the dummy-node idiom. Reach for the gap whenever a query is relative to the end of a list, and the dummy whenever a deletion might hit the head. Same toolkit as Remove Linked List Elements and Delete the Middle Node.
Practice queueDirect variations and pattern siblings
Direct follow-ups
- Middle of the Linked ListSame two-pointer idea but with fast moving twice as fast instead of a fixed gap.
- Delete the Middle Node of a Linked ListComing soonSlow/fast to find the middle, keep a prev pointer, then splice it out.
- Remove Linked List ElementsComing soonDummy node + single pass, removing every node matching a value.
- Remove Duplicates from Sorted List IIComing soonDummy node and a prev pointer to splice out runs of duplicate values.