Remove Nth Node from End — NullPointer on Head Removal
NullPointerException when removing head (n==list length) crashes browser tab's history pruner.
20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.
- 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.
Imagine two runners on a track. You tell the fast runner to start n steps ahead. They both run at the same speed. When the fast runner crosses the finish line, the slow runner is exactly n positions from the end. You then ask the slow runner to skip the next person in line — that skipped person is the one you wanted to remove. A dummy person placed before the start line handles the case where the person to skip is the very first runner.
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.
Why Removing the Nth Node from the End Is a Two-Pointer Problem
Removing the nth node from the end of a singly linked list means deleting the node that is n steps before the tail, where n is 1-indexed (n=1 removes the last node). The core mechanic uses two pointers — fast and slow — with the fast pointer advanced n steps ahead. Then both move together until fast reaches the end; slow now points to the node just before the target. This achieves the removal in one pass, O(n) time and O(1) space.
The critical property: you must handle the case where n equals the list length — that is, removing the head. In that scenario, the fast pointer becomes null after advancing n steps, and the standard loop condition fails. You detect this by checking if fast is null after the initial advance; if so, the head is the target. Without this check, you get a NullPointerException when trying to access fast.next in the loop. The two-pointer technique works because the gap between fast and slow is exactly n, so when fast lands on the last node, slow is at the node before the removal point.
Use this pattern whenever you need to find or modify a node at a known offset from the end without knowing the list length. It's common in streaming data, real-time log processing, and memory-constrained environments where you cannot buffer the entire list. The technique is also the foundation for more complex problems like rotating a list or finding the middle node — mastering it prevents off-by-one errors that crash production pipelines.
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.
- Gap of n: slow lands on the target — cannot delete because you need the predecessor.
- Gap of n+1: slow lands one node before the target — slow.next is the deletion target.
- The dummy node absorbs the edge case where slow would need to be 'before the head'.
- Missing the +1 step causes a silent off-by-one: wrong node gets deleted, no exception thrown.
The Two-Pass Kill: Why You Don't Always Need Two Pointers
Most devs reach for the fast-slow pointer trick the second they hear 'remove nth from end.' It's elegant, it's flashy, and it's what every blog shoves down your throat. But sometimes you just want the brute-force pass that works without edge-case gymnastics.
Here's the ugly truth: a two-pointer solution needs you to manage a dummy node, offset the fast pointer by exactly N, then step both until fast hits null. Miss the offset by one? You've deleted the wrong node or blown a null pointer. Not great when you're debugging at 2 AM.
The alternative is dead simple: traverse once to get the list length, compute the target index from the front, then traverse again to that node's predecessor and cut. O(n) time, O(1) space, zero pointer wizardry. The WHY is obvious: linked lists give you O(n) random access anyway, so two traversals cost the same as one traversal with offset logic. You just trade a bit of clarity for the same big-O.
When would I use this? Production code that needs to be audited by junior devs, or when N is dynamic and the two-pointer offset logic turns into a tangled mess. Don't let the cool-factor of two pointers dictate your implementation.
The Dummy Node: Your Shield Against Null-Pointer Hell
Here's a pattern that separates devs who've burned their fingers from those who haven't: the dummy node. When you remove the Nth node from the end, you might need to delete the head. Without a dummy, that's a special case branch. With one, it's just another node.
Think about it: your two-pointer setup needs a 'slow' pointer that trails N steps behind 'fast.' When 'fast' hits null, 'slow' is pointing at the node just before the one you want to delete. But what if N equals the list length? Then you're deleting the head, and 'slow' never moved. Without a dummy, you either add an if-check or crash.
The dummy node fixes this: create a node that points to head, then start both pointers from the dummy. Now 'slow' always has a predecessor, even when deleting the first real node. Your code becomes uniform—no special cases, no null checks, just clean pointer surgery.
Why does this matter in production? Because linked list operations in legacy codebases are full of 'if (prev == null)' hacks that accumulate tech debt. The dummy node pattern eliminates that entire class of bugs. Use it, and your code reviewers will stop side-eyeing you.
Statement
Given the head of a singly linked list, remove the Nth node from the end of the list and return its head. The list has at least one node, and N is guaranteed to be a valid index (1 ≤ N ≤ list length). This problem appears in coding interviews to test your ability to traverse and modify linked lists without extra space. The naive approach involves two passes: first get the total length, then locate the node before the target. But a single-pass two-pointer technique eliminates the need to know the full length upfront. Understanding this problem solidifies your grasp of pointer manipulation, edge cases like removing the head or tail, and efficient memory use. The signature in Java is: public ListNode removeNthFromEnd(ListNode head, int n).
Why Linked List Removal Differs from Array Removal
Removing an element from an array is trivial: shift all later elements left. But linked lists have no random access or contiguous memory. To delete a node, you must update the previous node's 'next' pointer to skip the target. This means you need a reference to the node before the target. If the target is the head, there is no previous node—so a dummy node becomes essential to unify edge cases. Another difference: arrays require knowing the exact index, but linked lists only provide traversal from head. The 'Nth from end' twist adds complexity because you don't know the tail position without a full scan. Two pointers solve this elegantly by creating an offset from the start, simulating a 'lookahead' to locate the endpoint. This problem teaches you to think about pointer references, null propagation, and linear-time operations without extra memory.
The Browser History Pruner That Deleted the Current Page
TabHistoryManager.pruneOldest(). Crash occurs only when history has exactly one entry and the pruning target is the head.- Always use a dummy sentinel node when the head might change during deletion. It eliminates an entire class of edge-case bugs.
- Head removal is the most common failure point in linked list deletion. If your code has a separate if-branch for head removal, you have a latent bug when that branch is wrong.
- After any linked list mutation, verify that the head reference is correctly updated. A dangling head pointer causes downstream NullPointerExceptions.
- Test with list length 1, 2, and 3 explicitly — these are the boundary cases where off-by-one errors hide.
jcmd <pid> Thread.print (confirm stack trace location)grep -r 'removeNth\|remove_nth' src/ (find all removal implementations)Key takeaways
Practice These on LeetCode
Interview Questions on This Topic
Frequently Asked Questions
20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.
That's Linked List. Mark it forged?
6 min read · try the examples if you haven't