Skip to content
Home DSA Remove Nth Node from End

Remove Nth Node from End

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Linked List → Topic 8 of 10
How to remove the nth node from the end of a linked list in one pass — two-pointer technique, edge cases for head removal, and clean Python and Java implementations.
⚙️ Intermediate — basic DSA knowledge assumed
In this tutorial, you'll learn
How to remove the nth node from the end of a linked list in one pass — two-pointer technique, edge cases for head removal, and clean Python and Java implementations.
  • 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • 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.
🚨 START HERE
Remove-Nth-From-End Triage Cheat Sheet
Fast commands to diagnose common production issues with positional deletion.
🟡NullPointerException on head removal — n equals list length.
Immediate ActionCheck if dummy node is used. Without it, slow is null before the head.
Commands
jcmd <pid> Thread.print (confirm stack trace location)
grep -r 'removeNth\|remove_nth' src/ (find all removal implementations)
Fix NowAdd dummy sentinel node. Return dummy.next as new head. Add test for n=listLength.
🟡Wrong node deleted — list is corrupted after removal.
Immediate ActionVerify the two-pointer gap is n+1, not n.
Commands
Add trace logging to print slow and fast positions at each step
Compare list length before and after deletion — should be exactly 1 less
Fix NowFix cases for removing first, middle, and last nodes.
🟡Memory leak — deleted node not garbage collected.
Immediate ActionHeap dump to check if the deleted node is still reachable.
Commands
jcmd <pid> GC.heap_dump /tmp/heap.hprof
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
Fix NowCheck if the caller holds a reference to the deleted node. Ensure no other pointer references it after deletion.
Production IncidentThe Browser History Pruner That Deleted the Current PageA browser's tab history pruning feature removed the nth oldest entry from the end of the history linked list. When the user had only one page in history, the pruning logic deleted the head without a dummy node, leaving the history reference pointing to a deleted node. The next navigation caused a NullPointerException.
SymptomBrowser tab crashes when navigating after clearing history to a single entry. NullPointerException in TabHistoryManager.pruneOldest(). Crash occurs only when history has exactly one entry and the pruning target is the head.
AssumptionThe team initially suspected a concurrency issue in the tab management layer, because the crash appeared only in specific usage patterns — single-tab sessions with repeated history pruning.
Root causeThe remove-nth-from-end implementation did not use a dummy node. When n equaled the list length (removing the head), the code attempted to set slow.next = slow.next.next where slow was null (no predecessor for the head). The null dereference caused the crash. The method also failed to update the head reference after deletion, leaving a dangling pointer.
Fix1. Introduced a dummy sentinel node before the head — the deletion logic slow.next = slow.next.next now works uniformly for all positions. 2. Always return dummy.next as the new head after deletion. 3. Added null-safety checks: if the list is empty after deletion, return null explicitly. 4. Added unit tests for n=1 on single-node lists, n=listLength on multi-node lists, and n=1 on multi-node lists.
Key Lesson
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.
Production Debug GuideSymptom-driven investigation paths for deletion-related incidents.
NullPointerException when removing the head (n equals list length).Check if a dummy node is used. Without a dummy, slow has no predecessor when the target is the head. Add a dummy sentinel node before the head.
Wrong node removed — off by one position.Verify the gap is n+1, not n. A gap of n positions means slow lands on the target, not before it. The gap must be n+1 so slow can update slow.next.
List appears unchanged after deletion — target node still present.Check if the method returns dummy.next (new head) or the original head reference. If the original head was removed but the caller still holds the old head reference, the list appears intact.
Infinite loop when traversing list after deletion.Check for a cycle introduced by improper pointer rewiring. If slow.next.next points back to an earlier node, a cycle is created. Run Floyd's cycle detection on the modified list.
IndexOutOfBoundsException or validation error on n.Verify that n is within valid bounds (1 <= n <= listLength). If n can exceed the list length, add a guard clause that throws IllegalArgumentException with a descriptive message.

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.

  1. Create a dummy node before the head to handle edge cases like removing the head itself.
  2. Set fast = dummy and slow = dummy.
  3. Advance fast n+1 steps forward (n steps to create the gap, +1 so slow lands before the target).
  4. Move both fast and slow one step at a time until fast reaches None.
  5. Now slow.next is the node to delete. Set slow.next = slow.next.next.
  6. 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.

Mental Model
The Sliding Window Analogy
Why the n+1 gap works
  • 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.
📊 Production Insight
The dummy node is not an optimization — it is a correctness guarantee. Without it, head removal requires a separate code path that is easy to get wrong. With it, every deletion is the same operation: slow.next = slow.next.next. This uniformity reduces bug surface area.
🎯 Key Takeaway
The n+1 gap ensures slow lands on the predecessor of the target node. The dummy node ensures slow always has a predecessor, even when the target is the head. Together, they make the deletion logic uniform and correct for all positions.

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.

Advance fast by n+1=3 steps
  • Step 1: fast=pos1 (node 1)
  • Step 2: fast=pos2 (node 2)
  • Step 3: fast=pos3 (node 3)
Now move both one step at a time until fast reaches None
  • 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.

RemoveNthFromEndDemo.java · JAVA
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
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));
    }
}
▶ Output
Before: 1 -> 2 -> 3 -> 4 -> 5
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
⚠ Watch Out: The n+1 Gap Is Non-Negotiable
Advancing fast only n steps (not n+1) means slow lands ON the target node when fast reaches the end. But you need to update slow.next, so slow must be BEFORE the target. The extra +1 step ensures slow is the predecessor. This is the single most common off-by-one bug in this algorithm.
📊 Production Insight
The bounds check during the fast-advance phase is critical in production. LeetCode guarantees valid n, but production code does not. Without this check, advancing fast past the end causes a NullPointerException that is harder to diagnose than a descriptive IllegalArgumentException. Always validate n or add a null guard during the advance loop.
🎯 Key Takeaway
The two-pointer gap solution is O(L) time, O(1) space, one pass. The dummy node makes head removal uniform. The n+1 gap ensures slow is the predecessor of the target. Always add bounds checking for production code.
Choosing Between One-Pass and Two-Pass
IfInterview context — asked for one-pass
UseUse two-pointer gap. Mention O(L) time, O(1) space.
IfProduction code — clarity over micro-optimization
UseTwo-pass (count then walk) is acceptable if the list is short. The constant factor difference is negligible for lists under 10K nodes.
IfHigh-throughput system — millions of deletions per second
UseUse one-pass. The 2x reduction in pointer hops matters at scale.
Ifn might exceed list length
UseTwo-pass is safer — you know the length before computing the position. One-pass requires bounds checking during the advance phase.
🗂 Two-Pass vs One-Pass Remove-Nth-From-End
Trade-offs for production and interview contexts.
AspectTwo-Pass (Count + Walk)One-Pass (Two-Pointer Gap)
Passes over list2 full passes1 pass
Time ComplexityO(L)O(L) — same class, fewer total pointer hops
Space ComplexityO(1)O(1)
Code complexitySimple — count then walkModerate — gap arithmetic
Bounds checkingEasy — compare n to counted lengthRequires null check during fast advance
Interview preferenceAcceptable as starting answerExpected as optimized answer
Handles head removalRequires separate if-branchDummy node handles uniformly
Handles tail removalWorks naturallyWorks naturally
Off-by-one riskLow — direct index arithmeticMedium — gap must be n+1, not n
Total pointer hops2L (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

    Advancing fast only n steps instead of n+1
    Symptom

    slow lands ON the target node instead of before it, so slow.next.next skips the wrong node —

    Fix

    advance fast n+1 steps. The extra step ensures slow is the predecessor of the target.

    Not using a dummy node
    Symptom

    head removal (n equals list length) requires a separate if-branch that returns head.next. If this branch is wrong or missing, the head is never removed —

    Fix

    always use a dummy sentinel node. The deletion logic slow.next = slow.next.next works uniformly for all positions.

    Returning the original head instead of dummy.next
    Symptom

    after removing the head, the caller receives the old head reference (which now points to itself or a deleted node) —

    Fix

    always return dummy.next after deletion.

    No bounds checking on n
    Symptom

    if n exceeds the list length, the fast-advance loop throws NullPointerException when fast becomes null —

    Fix

    check fast == null during the advance phase and throw IllegalArgumentException with a descriptive message.

    Not testing with single-node and two-node lists
    Symptom

    code works for lists of length 5+ but crashes on length 1 or 2 —

    Fix

    always write explicit test cases for list lengths 1, 2, and 3.

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.

🔥
Naren Founder & Author

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.

← PreviousFind Middle of Linked ListNext →LRU Cache Implementation
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged