Remove Nth Node From End of Linked List — Two-Pointer Technique Explained
Linked lists don't support random access. You can't just say 'give me index 7' the way you can with an array. That constraint turns a problem as simple as 'delete the 3rd element from the end' into something that actually requires you to think — and it's exactly why this problem appears so often in technical interviews at companies like Google, Amazon, and Meta. It tests whether you truly understand how linked lists work, not just whether you can copy a solution off the internet.
The naive approach — traverse the list once to count nodes, then traverse again to find the deletion point — works, but it's a two-pass solution. In systems where the list is streamed or very large, making two full passes can be expensive or even impossible. The real insight is realising you can solve this in a single pass by maintaining a fixed-distance gap between two pointers simultaneously. That gap is the key to the whole technique.
By the end of this article you'll understand exactly why the two-pointer approach works, be able to implement it cleanly in Java without debugging edge cases, handle tricky scenarios like removing the head node, and walk into an interview ready to explain not just the code but the reasoning behind every line of it.
Why Two Passes Feel Natural But One Pass Is Smarter
When most people first hit this problem they think: 'Easy — count the nodes first, then delete.' If the list has 7 nodes and you want to remove the 2nd from the end, that's the 6th node from the front (7 - 2 + 1 = 6). Walk to node 5, cut the link. Done in two passes.
That logic is correct. But here's the problem: what if you're processing a linked list that's being built from a network stream and you don't know its length upfront? What if the list has millions of nodes and memory latency makes a second full traversal expensive? The two-pass approach assumes you can rewind — and sometimes you can't.
The two-pointer technique solves this by keeping two pointers, 'leader' and 'follower', separated by exactly N steps. You advance both at the same speed. When the leader hits the end, the follower is sitting right before the node you want to delete. One pass, constant extra space, linear time. It's elegant because it turns a positional problem ('where is the Nth from end?') into a relative distance problem ('maintain a gap of N').
This pattern — maintaining a fixed gap between two pointers — shows up in many other linked list problems too, like finding the middle node or detecting a cycle. Understanding it here pays dividends across the whole category.
public class RemoveNthFromEnd { // --- Node definition --- static class ListNode { int value; ListNode next; ListNode(int value) { this.value = value; this.next = null; } } /** * Removes the Nth node from the END of the list in ONE pass. * * Strategy: Use a 'dummy' head node so we never need special-case * logic for removing the actual head of the list. * * @param head the first node of the list * @param n how many positions from the end to remove (1-indexed) * @return the head of the modified list */ public static ListNode removeNthFromEnd(ListNode head, int n) { // Dummy node points to head — this handles edge cases like // removing the very first node cleanly, without null checks later. ListNode dummy = new ListNode(0); dummy.next = head; ListNode leader = dummy; // moves ahead first to create the gap ListNode follower = dummy; // trails behind; ends up just before target // Step 1: Advance 'leader' by N+1 steps. // Why N+1 and not N? Because we want 'follower' to stop at the // node BEFORE the one we delete (so we can rewire the .next pointer). for (int step = 0; step <= n; step++) { leader = leader.next; } // Step 2: Move both pointers forward together until leader == null. // The gap between them stays exactly N+1 the whole time. while (leader != null) { leader = leader.next; follower = follower.next; } // Step 3: follower.next is now the node to remove. // Skip over it by rewiring the pointer to the node after it. ListNode nodeToRemove = follower.next; // hold reference for clarity follower.next = nodeToRemove.next; // bypass the target node nodeToRemove.next = null; // help the GC: cut outgoing link // dummy.next is always the true head (handles head-removal case). return dummy.next; } // --- Helper: build a linked list from an int array --- static ListNode buildList(int[] values) { ListNode dummy = new ListNode(0); ListNode current = dummy; for (int value : values) { current.next = new ListNode(value); current = current.next; } return dummy.next; } // --- Helper: print a linked list on one line --- static void printList(ListNode head) { ListNode current = head; StringBuilder sb = new StringBuilder(); while (current != null) { sb.append(current.value); if (current.next != null) sb.append(" -> "); current = current.next; } System.out.println(sb.toString()); } public static void main(String[] args) { // Test 1: Remove 2nd from end in a 5-node list → remove node with value 4 ListNode list1 = buildList(new int[]{1, 2, 3, 4, 5}); System.out.print("Before: "); printList(list1); list1 = removeNthFromEnd(list1, 2); System.out.print("After removing 2nd from end: "); printList(list1); System.out.println(); // Test 2: Remove the LAST node (n=1) ListNode list2 = buildList(new int[]{10, 20, 30}); System.out.print("Before: "); printList(list2); list2 = removeNthFromEnd(list2, 1); System.out.print("After removing 1st from end: "); printList(list2); System.out.println(); // Test 3: Remove the HEAD node — n equals list length ListNode list3 = buildList(new int[]{7, 14, 21}); System.out.print("Before: "); printList(list3); list3 = removeNthFromEnd(list3, 3); System.out.print("After removing head (3rd from end): "); printList(list3); System.out.println(); // Test 4: Single-node list — removing the only node ListNode list4 = buildList(new int[]{99}); System.out.print("Before: "); printList(list4); list4 = removeNthFromEnd(list4, 1); System.out.print("After removing only node: "); System.out.println(list4 == null ? "(empty list)" : list4.value); } }
After removing 2nd from end: 1 -> 2 -> 3 -> 5
Before: 10 -> 20 -> 30
After removing 1st from end: 10 -> 20
Before: 7 -> 14 -> 21
After removing head (3rd from end): 14 -> 21
Before: 99
After removing only node: (empty list)
Visualising the Gap: What Actually Happens Step by Step
Let's trace through Test 1 above manually so the pointer movement becomes completely concrete in your mind. The list is 1 → 2 → 3 → 4 → 5, and we're removing the 2nd node from the end (node with value 4).
We create a dummy node, so the full sequence is dummy → 1 → 2 → 3 → 4 → 5. Both pointers start at dummy.
Phase 1 — Advance the leader N+1 steps (N=2, so 3 steps): - Step 1: leader moves to node 1 - Step 2: leader moves to node 2 - Step 3: leader moves to node 3
Now leader is at node 3, follower is still at dummy. The gap is exactly 3.
Phase 2 — Move both together until leader is null: - Move 1: leader → 4, follower → 1 - Move 2: leader → 5, follower → 2 - Move 3: leader → null, follower → 3
Loop ends. Follower sits at node 3, which is exactly one step before node 4 (the target). We set follower.next = follower.next.next, which rewires node 3 directly to node 5, skipping node 4 entirely.
The gap of N+1 guarantees follower always lands one node before the target — no counting, no second pass.
/** * This version prints pointer positions at every step so you can watch * the gap technique in action. Educational use only — not production code. */ public class VisualisePointerMovement { static class ListNode { int value; ListNode next; ListNode(int value) { this.value = value; } } public static ListNode removeNthFromEndVerbose(ListNode head, int n) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode leader = dummy; ListNode follower = dummy; System.out.println("=== Phase 1: Advance leader by " + (n + 1) + " steps ==="); for (int step = 0; step <= n; step++) { leader = leader.next; // Show which node leader is currently on System.out.println(" Step " + (step + 1) + ": leader is now at node [" + (leader != null ? leader.value : "null") + "]"); } System.out.println("\n=== Phase 2: Advance both together ==="); int moveCount = 0; while (leader != null) { leader = leader.next; follower = follower.next; moveCount++; System.out.println(" Move " + moveCount + ": leader=[" + (leader != null ? leader.value : "null") + "], follower=[" + follower.value + "]"); } System.out.println("\nFollower stopped at node [" + follower.value + "] — removing follower.next which is node [" + follower.next.value + "]"); // Perform the deletion follower.next = follower.next.next; return dummy.next; } static ListNode buildList(int[] values) { ListNode dummy = new ListNode(0); ListNode current = dummy; for (int value : values) { current.next = new ListNode(value); current = current.next; } return dummy.next; } static void printList(ListNode head) { ListNode current = head; while (current != null) { System.out.print(current.value + (current.next != null ? " -> " : "")); current = current.next; } System.out.println(); } public static void main(String[] args) { ListNode list = buildList(new int[]{1, 2, 3, 4, 5}); System.out.print("Input list: "); printList(list); System.out.println("Removing 2nd node from the end (n=2):\n"); ListNode result = removeNthFromEndVerbose(list, 2); System.out.print("\nResult: "); printList(result); } }
Removing 2nd node from the end (n=2):
=== Phase 1: Advance leader by 3 steps ===
Step 1: leader is now at node [1]
Step 2: leader is now at node [2]
Step 3: leader is now at node [3]
=== Phase 2: Advance both together ===
Move 1: leader=[4], follower=[1]
Move 2: leader=[5], follower=[2]
Move 3: leader=[null], follower=[3]
Follower stopped at node [3] — removing follower.next which is node [4]
Result: 1 -> 2 -> 3 -> 5
Edge Cases That Will Fail Your Code in an Interview
The core algorithm is clean, but interviews are won and lost on edge cases. There are three scenarios that break naive implementations and every interviewer knows them.
First: removing the head node. If N equals the length of the list, the 'Nth from end' is the very first node. Without a dummy node, you'd need to return head.next manually — a special-case if-block. With the dummy node, this resolves automatically because follower ends up at dummy, and dummy.next = nodeToRemove.next correctly sets the new head.
Second: a single-node list. The list has one node and N=1. Both the leader and follower start at dummy, the leader advances to node 1 and then one more step to null. The while-loop never runs. Follower stays at dummy. follower.next (the only node) is deleted. dummy.next is null. Return null — an empty list. This is the correct answer and the dummy-node approach handles it without any extra code.
Third: N equals exactly 1 (remove the tail). The leader advances to the last node, then one more step to null. The while-loop runs until the leader passes the last node and hits null, at which point the follower is sitting on the second-to-last node. Perfect — we unlink the tail cleanly.
All three of these are handled by the single implementation above with zero extra conditional logic. That's the real elegance of the dummy-node + two-pointer combination.
/** * Comprehensive edge case validation for removeNthFromEnd. * Run this to confirm every boundary condition behaves correctly. */ public class EdgeCaseSuite { static class ListNode { int value; ListNode next; ListNode(int value) { this.value = value; } } public static ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(0); dummy.next = head; ListNode leader = dummy; ListNode follower = dummy; for (int step = 0; step <= n; step++) { leader = leader.next; } while (leader != null) { leader = leader.next; follower = follower.next; } follower.next = follower.next.next; return dummy.next; } static ListNode buildList(int[] values) { ListNode dummy = new ListNode(0); ListNode current = dummy; for (int val : values) { current.next = new ListNode(val); current = current.next; } return dummy.next; } static String listToString(ListNode head) { if (head == null) return "(empty)"; StringBuilder sb = new StringBuilder(); ListNode current = head; while (current != null) { sb.append(current.value); if (current.next != null) sb.append(" -> "); current = current.next; } return sb.toString(); } static void runTest(String label, int[] values, int n, String expected) { ListNode list = buildList(values); ListNode result = removeNthFromEnd(list, n); String actual = listToString(result); String status = actual.equals(expected) ? "PASS" : "FAIL"; System.out.printf("[%s] %s%n Got: %s%n Expected: %s%n%n", status, label, actual, expected); } public static void main(String[] args) { // Edge case 1: Remove head node (n = list length) runTest("Remove head from 3-node list (n=3)", new int[]{5, 10, 15}, 3, "10 -> 15"); // Edge case 2: Remove tail node (n=1) runTest("Remove tail from 4-node list (n=1)", new int[]{1, 2, 3, 4}, 1, "1 -> 2 -> 3"); // Edge case 3: Single-node list (n=1) runTest("Remove only node — single-element list (n=1)", new int[]{42}, 1, "(empty)"); // Edge case 4: Two-node list, remove first (n=2) runTest("Two-node list, remove first node (n=2)", new int[]{100, 200}, 2, "200"); // Edge case 5: Two-node list, remove second (n=1) runTest("Two-node list, remove second node (n=1)", new int[]{100, 200}, 1, "100"); // Edge case 6: Middle node in 5-element list runTest("Remove 3rd from end in 5-node list (n=3)", new int[]{1, 2, 3, 4, 5}, 3, "1 -> 2 -> 4 -> 5"); } }
Got: 10 -> 15
Expected: 10 -> 15
[PASS] Remove tail from 4-node list (n=1)
Got: 1 -> 2 -> 3
Expected: 1 -> 2 -> 3
[PASS] Remove only node — single-element list (n=1)
Got: (empty)
Expected: (empty)
[PASS] Two-node list, remove first node (n=2)
Got: 200
Expected: 200
[PASS] Two-node list, remove second node (n=1)
Got: 100
Expected: 100
[PASS] Remove 3rd from end in 5-node list (n=3)
Got: 1 -> 2 -> 4 -> 5
Expected: 1 -> 2 -> 4 -> 5
| Aspect | Two-Pass Approach | Two-Pointer (One-Pass) Approach |
|---|---|---|
| Number of traversals | 2 full traversals | 1 traversal |
| Time complexity | O(L) — two passes over length L | O(L) — one pass over length L |
| Space complexity | O(1) | O(1) |
| Works on streaming data | No — needs full length first | Yes — length unknown upfront is fine |
| Code complexity | Simple to understand intuitively | Slightly less obvious but cleaner |
| Head-removal handling | Needs a separate if-block without dummy | Handled automatically with dummy node |
| Interview preference | Acceptable but not optimal | Expected answer at senior/mid level |
🎯 Key Takeaways
- The two-pointer gap technique solves 'Nth from end' in one pass by maintaining a fixed distance of N+1 nodes between leader and follower — when leader reaches null, follower is exactly at the predecessor of the target.
- Always use a dummy/sentinel node as the starting position for both pointers. It collapses three special cases (remove head, single-node list, two-node list) into zero extra code.
- Time complexity is O(L) and space is O(1) for both approaches, but the one-pass solution works on streaming or length-unknown data structures where a two-pass approach would fail entirely.
- The gap-of-N pattern isn't just for this problem — it's the foundation of finding the middle node (N = L/2), detecting cycles (Floyd's algorithm), and several other linked list techniques. Master the pattern, not just the problem.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Advancing the leader only N steps instead of N+1 — Symptom: follower stops ON the node to delete instead of BEFORE it, so follower.next.next skips two nodes and corrupts the list — Fix: change the loop condition to
step <= n(runs N+1 times) instead ofstep < n. The follower must stop at the predecessor, not the target. - ✕Mistake 2: Not using a dummy node when removing the head — Symptom: NullPointerException on
follower.next = follower.next.nextbecause follower is still at the original head and follower.next is null after deletion — Fix: always prepend a dummy node and return dummy.next. This eliminates the need for any special-case logic when the head itself is the target. - ✕Mistake 3: Forgetting that n is 1-indexed from the end, not 0-indexed — Symptom: off-by-one errors, e.g., removing node 4 when you meant node 5 in a list of 5 — Fix: confirm your understanding with a small example before writing code. The '1st from end' means the last node. Write that down explicitly and trace one test case by hand before touching the keyboard.
Interview Questions on This Topic
- QCan you solve this in a single pass without knowing the length of the list upfront? Walk me through your pointer strategy and explain why the leader advances N+1 steps and not N.
- QWhat happens when N equals the length of the list — i.e., you need to remove the head node? Does your current implementation handle it without extra conditional logic, and why?
- QHow would you modify this solution if the input was a doubly linked list? Would the two-pointer technique still be necessary, or is there a more direct approach? What are the trade-offs?
Frequently Asked Questions
What is the time and space complexity of removing the Nth node from end of a linked list?
Time complexity is O(L) where L is the length of the list — you visit each node at most once. Space complexity is O(1) because you only use two extra pointer variables regardless of list size. There's no recursion stack or auxiliary data structure involved.
Why do we advance the leader pointer N+1 steps instead of N steps?
Because deletion in a singly linked list requires a reference to the node BEFORE the target — you need to update its .next pointer to skip the target. Advancing N+1 steps ensures the follower lands one position before the node to delete, not on it. Advancing only N steps would leave the follower on the target itself, making deletion impossible without a back-reference.
What happens if N equals the length of the list — does the algorithm still work?
Yes, and this is exactly why the dummy node matters. When N equals the list length, the leader advances all the way past the original head and out of the actual list during Phase 1. When both pointers then advance together in Phase 2, the follower never moves — it stays at the dummy node. So follower.next is the original head, and we correctly remove it by setting dummy.next = head.next. No special-case if-block needed.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.