HARD Lab · Linked List · 57

19. Remove Nth Node From End of List

Delete the node that is exactly n positions from the tail in a single pass — no length calculation needed — by keeping a fast pointer n+1 steps ahead of a slow pointer so that when fast hits null, slow lands right before the target.

MediumTwo PointersDummy HeadTypeScript

PROBLEM What we're solving

Given the head of a singly linked list and an integer n, remove the node that is the n-th from the end and return the updated head. Example: list 1→2→3→4→5, n=2 → remove node 4 → return 1→2→3→5. Constraint: n is always valid (1 ≤ n ≤ length).

KEY IDEA Maintain a fixed gap of n+1 between two pointers

Insight → if fast is exactly n+1 nodes ahead of slow, then when fast reaches null (past the last node), slow is sitting on the node just before the one to remove — exactly where you need to relink. One pass, no length needed.

RECIPE Dummy head + two-pointer advance

  • 0 · Dummy head. Create dummy.next = head and start both pointers there. The dummy makes “remove the head” just another ordinary unlink — no special case needed.
  • 1 · Open the gap. Advance fast exactly n+1 steps. Now there are n+1 nodes between slow and fast.
  • 2 · Move in tandem. Step both pointers one node at a time until fast === null. The gap stays fixed at n+1, so slow stops on the predecessor of the target.
  • 3 · Unlink. slow.next = slow.next.next removes the target. Return dummy.next.
Classic confusion → why n+1 and not n? You want slow to stop before the node being removed so you can relink. Advancing by only n would leave slow on the target, with no way to splice it out.

COST Complexity & alternatives

Two-pass (compute length)
O(n) × 2
Walk once to count, again to the (len−n)-th node.
Two-pointer (one pass)
O(n)
Single traversal; O(1) extra space.

Both are O(n) time, but the two-pointer approach is a single pass and avoids storing the length. The dummy head is an O(1) allocation that pays back by eliminating all edge-case branches.

Pattern transfer → the fixed-gap trick generalises: Middle of a Linked List (fast moves at 2×, slow at 1×); Linked List Cycle (fast overtakes slow on a cycle); Reorder List (find the midpoint first with the same technique).

RUN IT Two pointers — maintain a gap of n+1, then unlink

step 0 / 8
STARTList: [1, 2, 3, 4, 5], remove node 2 from the end. Create a dummy head; both fast and slow start there. The dummy eliminates the edge case of removing the real head.
Dslow1122334455
State
dummy
slow
dummy
fast
0
gap
init
phase
fast pointerslow pointernode to removefinal result
slowfast

TYPESCRIPT The solution, annotated

removeNthFromEnd.ts
class ListNode {
  val: number;
  next: ListNode | null;
  constructor(val = 0, next: ListNode | null = null) {
    this.val = val;
    this.next = next;
  }
}

function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  const dummy = new ListNode(0, head);   // dummy head → handles removing the real head
  let fast: ListNode | null = dummy;
  let slow: ListNode | null = dummy;

  // Advance fast n+1 steps so the gap between slow and fast is n+1
  for (let i = 0; i <= n; i++) {
    fast = fast!.next;
  }

  // Move both until fast reaches null
  while (fast !== null) {
    fast = fast.next;
    slow = slow!.next;
  }

  // slow is now just before the node to remove
  slow!.next = slow!.next!.next;

  return dummy.next;
}

Reading it block by block

Line 12 — dummy head. new ListNode(0, head) gives us a sentinel node whose next is head. Both pointers start here. When the node to remove is head itself, slow stays on the dummy, and the unlink on line 25 makes the dummy skip the old head — no branch needed.
Lines 17–19 — open the gap. Advancing fast exactly n+1 times (loop runs while i <= n) creates a gap of n+1 nodes between slow and fast. After this loop, there are exactly n nodes between them in the list.
Lines 22–25 — co-move until fast is null. Each iteration advances both pointers by one. When fast reaches null, slow is exactly at the predecessor of the n-th node from the end. The gap invariant keeps this true for every step.
Line 28 — unlink. slow.next = slow.next.next bypasses the target node. Because slow is guaranteed non-null (the dummy always precedes it) and slow.next is the target (guaranteed by the constraints), no null checks are needed here.
Line 30 — return dummy.next. The dummy was never in the original list; its next is the new head — even when the old head was removed.
Complexity → O(L) time where L = list length (single pass). O(1) space — only two pointer variables and the dummy node are allocated, regardless of list size.

INTERVIEWFollow-ups they'll ask

  • “What if you didn't use a dummy head?”You'd need a special case: if n === length, the head is the target, so return head.next. The dummy collapses that branch.
  • “Can you do it in one pass?” The two-pointer gap trick is exactly this — you never need to count the list length first.
  • “What if n could be invalid?” Add a guard: if after advancing fast n+1 times it falls off the end, n exceeds the list length — throw or return head unchanged.
  • “Return the removed node's value too?” Capture slow.next.val before the unlink and return a tuple [newHead, removedVal].
  • “Doubly linked list version?” Same pointer logic; the unlink also updates prev pointers, but the two-pointer positioning is identical.

MNEMONIC The one-liner

"Fast runs n+1 ahead, then both walk together — when fast falls off the cliff, slow is standing right behind the target."

TRIGGERS When you see ___ → reach for ___

"remove k-th / n-th from the end"two pointers, gap = n+1
"single pass, no length"fast/slow with fixed offset
"removing the head is an edge case"dummy head sentinel
"find the predecessor to unlink"stop slow one node early (gap n+1 not n)

SKELETON The reusable shape

skeleton.ts
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
  const dummy = new ListNode(0, head);
  let fast: ListNode | null = dummy;
  let slow: ListNode | null = dummy;

  for (let i = 0; i <= n; i++) fast = fast!.next;

  while (fast !== null) {
    fast = fast.next;
    slow = slow!.next;
  }

  slow!.next = slow!.next!.next;
  return dummy.next;
}

FLASHCARDS Tap to flip

Why advance fast by n+1 instead of n?
You need slow to stop on the predecessor of the target so you can relink. A gap of n+1 achieves this; a gap of n would leave slow on the target itself.
tap to flip
1 / 7
Answer to reveal explanations. No penalty for retries.Score: 0 / 7
QUESTION 01
List [1, 2, 3, 4, 5], n=2. Which node is removed and what is the result?
QUESTION 02
Why do we advance fast by n+1 steps instead of n steps?
QUESTION 03
What is the purpose of the dummy head (new ListNode(0, head))?
QUESTION 04
What is the time complexity of the two-pointer one-pass solution?
QUESTION 05
List [1], n=1. What does the function return?
QUESTION 06
After the gap-opening loop finishes, what is the distance (in nodes) between slow and fast?
QUESTION 07
Which of these problems is not solved by the same fast/slow pointer pattern?