Remove Nth Node from End
- Two-pointer gap: advance fast n+1 steps, then move both together until fast is null.
- Use a dummy node to eliminate the edge case where the head itself must be removed.
- One pass solution: O(n) time, O(1) space.
- Core idea: advance fast pointer n+1 steps ahead of slow. Move both until fast reaches null. Slow is now before the target node.
- Deletion: set slow.next = slow.next.next to skip the target node.
- Dummy node: eliminates the head-removal edge case — slow always has a predecessor.
- Time: O(L) one pass. Space: O(1) — two pointer variables only.
- Why n+1 not n: slow must land on the node BEFORE the target, not on the target itself.
- Production risk: forgetting the dummy node means head removal requires a separate code path.
NullPointerException on head removal — n equals list length.
jcmd <pid> Thread.print (confirm stack trace location)grep -r 'removeNth\|remove_nth' src/ (find all removal implementations)Wrong node deleted — list is corrupted after removal.
Add trace logging to print slow and fast positions at each stepCompare list length before and after deletion — should be exactly 1 lessMemory leak — deleted node not garbage collected.
jcmd <pid> GC.heap_dump /tmp/heap.hprofjmap -dump:format=b,file=/tmp/heap.hprof <pid>Production Incident
TabHistoryManager.pruneOldest(). Crash occurs only when history has exactly one entry and the pruning target is the head.Production Debug GuideSymptom-driven investigation paths for deletion-related incidents.
Removing the nth node from the end of a linked list is a positional deletion problem — you need to find a node by its distance from the tail without random access. The naive approach counts the length first (two passes), then walks to position L-n. The optimal approach uses a two-pointer gap in a single pass.
The critical edge case is removing the head itself (when n equals list length). Without a dummy sentinel node, this requires a separate if-branch. The dummy node makes the deletion logic uniform — slow always has a predecessor, so slow.next = slow.next.next works for every position including the head.
This pattern generalizes: any problem requiring 'find the kth element from the end' uses the same two-pointer gap technique. It is the complement of Floyd's fast/slow pattern used for cycle detection and middle-finding.
How Remove Nth Node From End Works — Step by Step
The classic approach uses two pointers separated by exactly n nodes so that when the fast pointer reaches the end, the slow pointer is at the (n+1)th node from the end — the node just before the target.
- Create a dummy node before the head to handle edge cases like removing the head itself.
- Set fast = dummy and slow = dummy.
- Advance fast n+1 steps forward (n steps to create the gap, +1 so slow lands before the target).
- Move both fast and slow one step at a time until fast reaches None.
- Now slow.next is the node to delete. Set slow.next = slow.next.next.
- Return dummy.next as the new head.
For list 1→2→3→4→5, remove 2nd from end (target: node 4): 1. Advance fast 3 steps: fast=node3. 2. Move together: slow=node1,fast=node4; slow=node2,fast=node5; slow=node3,fast=None. 3. Delete: node3.next = node3.next.next = node5. List: 1→2→3→5.
- Imagine a window of fixed width n+1 sliding across the list.
- The left edge of the window is slow. The right edge is fast.
- When the right edge hits the end, the left edge is exactly n+1 positions from the end.
- The node immediately after the left edge (slow.next) is the nth from the end — the deletion target.
Worked Example — Tracing the Two-Pointer Approach
List: 1→2→3→4→5 (length 5). Remove n=2 (2nd from end = node with value 4).
Initial state: dummy→1→2→3→4→5→None. fast=slow=dummy.
- Step 1: fast=pos1 (node 1)
- Step 2: fast=pos2 (node 2)
- Step 3: fast=pos3 (node 3)
- Iteration 1: slow=pos1 (node 1), fast=pos4 (node 4). fast is not None.
- Iteration 2: slow=pos2 (node 2), fast=pos5 (node 5). fast is not None.
- Iteration 3: slow=pos3 (node 3), fast=None. fast is None. Stop.
slow is at node 3. slow.next is node 4 (the target). Delete: slow.next = slow.next.next (node 5).
Result: dummy→1→2→3→5→None. Return dummy.next → 1→2→3→5.
package io.thecodeforge.list; /** * One-pass remove-nth-from-end with trace output for learning and debugging. */ public class RemoveNthFromEndDemo { static class ListNode { int val; ListNode next; ListNode(int val) { this.val = val; } } static ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode slow = dummy; ListNode fast = dummy; // Advance fast n+1 steps to create the gap for (int i = 0; i <= n; i++) { fast = fast.next; } // Move both until fast reaches the end while (fast != null) { slow = slow.next; fast = fast.next; } // Delete: skip the target node slow.next = slow.next.next; return dummy.next; } static String listToString(ListNode head) { StringBuilder sb = new StringBuilder(); ListNode current = head; while (current != null) { sb.append(current.val); if (current.next != null) sb.append(" -> "); current = current.next; } return sb.toString(); } static ListNode buildList(int[] values) { ListNode dummy = new ListNode(0); ListNode tail = dummy; for (int val : values) { tail.next = new ListNode(val); tail = tail.next; } return dummy.next; } public static void main(String[] args) { // Test 1: Remove middle node ListNode list1 = buildList(new int[]{1, 2, 3, 4, 5}); System.out.println("Before: " + listToString(list1)); ListNode result1 = removeNthFromEnd(list1, 2); System.out.println("After (remove 2nd from end): " + listToString(result1)); // Test 2: Remove head (n == listLength) ListNode list2 = buildList(new int[]{1, 2, 3}); ListNode result2 = removeNthFromEnd(list2, 3); System.out.println("Remove head (3rd from 3): " + listToString(result2)); // Test 3: Remove tail (n == 1) ListNode list3 = buildList(new int[]{1, 2, 3}); ListNode result3 = removeNthFromEnd(list3, 1); System.out.println("Remove tail (1st from end): " 4: Single node list ListNode list4 = buildList(new int[]{42}); ListNode result4 = removeNthFromEnd(list4, 1); System.out.println("Single node removed: " + (result4 == null ? "(empty)" : listToString(result4))); // Test 5: Two node list, remove first ListNode list5 = buildList(new int[]{10, 20}); ListNode result5 = removeNthFromEnd(list5, 2); System.out.println("Two nodes, remove head: " + listToString(result5)); } }
After (remove 2nd from end): 1 -> 2 -> 3 -> 5
Remove head (3rd from 3): 2 -> 3
Remove tail (1st from end): 1 -> 2
Single node removed: (empty)
Two nodes, remove head: 20
| Aspect | Two-Pass (Count + Walk) | One-Pass (Two-Pointer Gap) |
|---|---|---|
| Passes over list | 2 full passes | 1 pass |
| Time Complexity | O(L) | O(L) — same class, fewer total pointer hops |
| Space Complexity | O(1) | O(1) |
| Code complexity | Simple — count then walk | Moderate — gap arithmetic |
| Bounds checking | Easy — compare n to counted length | Requires null check during fast advance |
| Interview preference | Acceptable as starting answer | Expected as optimized answer |
| Handles head removal | Requires separate if-branch | Dummy node handles uniformly |
| Handles tail removal | Works naturally | Works naturally |
| Off-by-one risk | Low — direct index arithmetic | Medium — gap must be n+1, not n |
| Total pointer hops | 2L (count + walk) | L + n (advance + walk) |
🎯 Key Takeaways
- Two-pointer gap: advance fast n+1 steps, then move both together until fast is null.
- Use a dummy node to eliminate the edge case where the head itself must be removed.
- One pass solution: O(n) time, O(1) space.
- The dummy node makes the code uniform — no if-head-removal special case.
- n is guaranteed to be valid (LeetCode constraint) — no bounds checking needed for competitive problems.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QHow do you remove the nth node from the end of a linked list in one pass?
- QWhy is the two-pointer gap n+1 and not n?
- QHow does a dummy node simplify linked list problems?
- QWhat happens when n equals the list length? Walk me through the pointer positions step by step.
- QHow would you modify this to remove ALL nodes that are the nth from the end (if duplicates exist)?
- QCan you extend this to remove the nth node from the end of a doubly linked list? What changes?
Frequently Asked Questions
Why advance fast n+1 steps instead of n?
Advancing n steps would put fast at the node to be removed when fast reaches the last node. Advancing n+1 steps puts slow at the node before the one to remove — exactly where you need to be to update the next pointer. The dummy node absorbs the off-by-one for the head case.
What is the edge case where n equals the list length?
This is the head removal case. With a dummy node, slow ends up pointing at the dummy node itself, and slow.next is the head. Setting slow.next = slow.next.next removes the head cleanly. Without the dummy, you need a separate if check.
Why use a dummy node for removing the Nth from the end?
When the node to delete is the head (n equals the list length), slow would need to be 'before the head'. A dummy node placed before head gives slow a valid position — dummy.next = head — so the deletion logic slow.next = slow.next.next works uniformly without a special case.
What is the two-pass approach and why is one-pass better?
Two-pass: first count the length L, then traverse to position L-n+1. One-pass: use the two-pointer gap trick described above. Both are O(L) time and O(1) space, but one-pass makes exactly one traversal versus two, which is preferred in interviews.
Why use a dummy node at the head?
A dummy (sentinel) node simplifies edge cases. Without it, removing the actual head node requires a special branch: if n equals list length, return head.next. With a dummy, the slow pointer can be positioned before the head, and the deletion logic slow.next = slow.next.next works uniformly for all positions.
Can this be done with just one pass instead of first counting the length?
Yes, the two-pointer technique is a one-pass O(n) solution. Counting the length first is also O(n) but requires two passes. The two-pointer approach reads the list only once while maintaining the invariant that the two pointers are always exactly n+1 apart.
What if n equals the list length (remove the head)?
The dummy node handles this. When n equals the list length, slow ends up at the dummy node itself after the traversal. Then dummy.next = dummy.next.next removes the original head. Returning dummy.next gives the second original node as the new head.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.