DSA sheet · Linked List · Fast & slow pointer pattern

Remove Nth Node from End of List

This is the fourth fast and slow pointer problem, and it teaches something new about the pattern: "fast" doesn't have to mean "double speed". It can mean "starts ahead". The teacher first solves it in two passes: count the length, then walk to the right spot. Then she uses a picture from athletics, the staggered start on an oval track, to do it in one pass with a fixed gap between two pointers. Along the way she shows why a dummy node saves us from messy special cases when the head itself has to be deleted.

Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Linked list basics

A linked list is a chain of nodes. Each node stores a val and a pointer next to the following node. The last node's next is None. We are handed only the first node, the head.

given by LeetCode, don't write this in the solution
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next
[1] → [2] → [3] → [4] → [5] → None
 ↑
head

How to delete a node (recap)

To delete a node you must stand on the node just before it. Then you make that node skip over it:

before:  [1] → [2] → [3] → [4] → [5] → None
                      ↑
                     prev        (we want to delete 4)

prev.next = prev.next.next

after:   [1] → [2] → [3] ──────→ [5] → None        (4 is cut out)

prev.next is node 4. prev.next.next is what 4 points to, node 5. So 3 now points straight to 5. Nothing points to 4 any more, so it's gone from the list. If 4 were the last node, prev.next.next would be None, and 3 would become the new last node. Works the same.

Doubt: why can't I stand on 4 itself and delete it?
→ Deleting means changing the pointer that leads into 4, and that pointer lives in node 3. From 4 you can't go back to 3. So we always aim for the node one before the target.

The dummy node

A dummy is a fake extra node we put in front of head: dummy = ListNode(0), dummy.next = head.

[0] → [1] → [2] → [3] → [4] → [5] → None
 ↑     ↑
dummy head

Why? Deleting needs a node before the target. Every real node has one, except head. The dummy gives head a "previous node" too, so deleting head works with the exact same line as deleting any other node. No special if. At the end we return dummy.next, which is the real (possibly new) head.

The two-pointer gap

In the earlier fast and slow problems, fast moved 2 steps and slow moved 1. Here both move 1 step at a time, but fast starts some steps ahead. Because they move at the same speed, the gap between them never changes. When fast reaches the end, slow is exactly "gap" nodes behind the end. That's how we measure a distance from the end without knowing the length.

Part A · Brute force: count the length, then walk (two passes)

LeetCode 19

1The question in simple words

You get the head of a linked list and a number n. Remove the n-th node counting from the end (n = 1 means the last node, n = 2 the second-last, and so on). Return the head of the changed list.

n = 2 → remove 4
[1] → [2] → [3] → [4] → [5] → None
                   2nd   1st   (from end)
result: [1] → [2] → [3] → [5] → None
n = 1, one node → empty list
[1] → None
result: None

2What the constraints tell us

3Intuition: turn "from the end" into "from the start"

We can't walk backwards. But if we know the total length L, then "n-th from the end" is the same node as "(L − n + 1)-th from the start". And to delete it, we need to stand on the node before it. So: count the length first, then walk to the node just before the target.

4Building the logic from examples

How many jumps from head to the node before the target?

The teacher guesses and tests a formula on examples, which is a good habit.

Formulajumps from head to the node before the target = L − n − 1, where L is the true number of nodes.

Counting the length, and why the starting value matters

Walk with curr from head and add 1 for each node until curr is None. The teacher warns: be careful what your counter actually counts.

Both are fine. What's not fine is mixing them: one counting style with the other style's formula. Pick one and stay consistent. On this page we count nodes, so L is the real length.

The head case: n = L

If n equals the length, the target is the head itself. Then L − n − 1 = −1: there's no node before it. So handle it first: if n == L, return head.next. The second node becomes the new head (or None if there was only one node).

Doubt 1 (a fix to the video's code): in the video, the head check is written as "position = length − n; if position is 0, return head.next", and then the code takes "position" jumps. Is that right?
→ Only if the meaning of "length" is the same in both places, and that's easy to get wrong. With the true node count L, "L − n == 0" is the right head check, but then the walk needs L − n − 1 jumps, not L − n (L − n jumps lands on the target, and you'd delete the node after it). With the jump count (L − 1), "position" jumps is right, but then the head check must be "position == −1". The code on this page uses the true count L everywhere: head check n == L, walk L − n − 1 jumps. The tests below check every n for many lengths.

The delete step

After the jumps, curr is on the node before the target. curr.next = curr.next.next. In the teacher's example of removing the last node (n = 1) from 1 … 5, we stand on 4. 4's next is 5, and 5's next is None, so 4's next becomes None and 5 is gone.

5Approach steps

  1. Count the nodes: L.
  2. If n == L → the head must go → return head.next.
  3. Otherwise put curr back at head and jump L − n − 1 times.
  4. curr.next = curr.next.next.
  5. Return head (it didn't change).

6Code (Python)

Remove Nth from End, two passes
class Solution:
    def removeNthFromEnd(self, head, n):
        length = 0
        curr = head
        while curr is not None:          # pass 1: count the nodes
            length += 1
            curr = curr.next

        if n == length:                  # the head itself must go
            return head.next

        curr = head
        for _ in range(length - n - 1):  # pass 2: stand before the target
            curr = curr.next
        curr.next = curr.next.next       # skip over the target
        return head

7Code line by line

linewhat it means
length = 0 curr = headA counter and a walking copy of head.
while curr is not None: length += 1 curr = curr.nextCount one for every node we stand on. At the end, length = L.
if n == length: return head.nextThe n-th from the end is the first node. There's no node before it, so we just start the answer from the second node.
curr = headRestart from the beginning for the second pass.
for _ in range(length - n - 1): curr = curr.nextExactly L − n − 1 jumps puts curr on the node before the target. (0 jumps is possible: then the target is the second node and curr stays on head.)
curr.next = curr.next.nextRewire: the node before now points past the target.
return headThe head was not removed, so it's still the start.

8Dry run

List 1 → 2 → 3 → 4 → 5, n = 2.

steppointerswhat changeslist after this stepanswer so far
countcurr walks 1 … 5 → Nonelength = 51 → 2 → 3 → 4 → 5—
check—n = 2 ≠ 5 → not the headunchanged—
jump 1curr = 25 − 2 − 1 = 2 jumps neededunchanged—
jump 2curr = 3—unchanged—
deletecurr = 33.next: 4 → 51 → 2 → 3 → 5return head (1)
after the jumps:
[1] → [2] → [3] → [4] → [5] → None
             ↑
            curr

after curr.next = curr.next.next:
[1] → [2] → [3] ──────→ [5] → None

Head case: 1 → 2, n = 2 → length 2 = n → return node 2. Result: 2 → None ✓. One node, n = 1 → return head.next = None ✓.

9Complexity & remember

Remember two-passCount L · if n == L return head.next · walk L − n − 1 jumps · curr.next = curr.next.next. Keep the counting style and the formula consistent.

Part B · Optimal: a gap of n + 1 between two pointers, with a dummy node (one pass)

1The question (same as Part A)

Same input and output. New goal: only one pass over the list, no separate counting.

2What the constraints tell us

3Intuition: the staggered start on a running track

On an oval track, runners in the outer lanes start further ahead than runners in the inner lanes. Why? The outer lane is longer, so giving those runners a head start makes the race fair: everyone runs the same distance to the finish line.

Here we do the same thing on purpose. We give fast a head start of a fixed number of nodes, then move both pointers at the same speed. The gap stays constant. When fast crosses the finish line (None), slow is exactly that gap behind the end.

Doubt 1: is this still "fast and slow pointers" if both move 1 step?
→ Yes. The teacher stresses this: "fast" doesn't always mean double speed. It just means fast is ahead of slow. The pattern is about two pointers whose distance tells you something.

4Building the logic from examples

How big should the gap be? n + 1 jumps

We want slow to finish on the node before the target, because that's where deleting happens. The target is n nodes from the end; the node before it is n + 1 nodes from the end. So fast should be n + 1 jumps ahead of slow.

Teacher's first example, without a dummy yet: 1 → 2 → 3 → 4 → 5, n = 2. Fast takes 3 jumps: 1 → 2 → 3 → 4. Slow stays on 1. Now both move 1 at a time:

Slow is on 3, the node before 4 ✓. Delete with slow.next = slow.next.next.

Her bigger example: 1 → 2 → … → 8, n = 3 (target is 6). Fast takes 4 jumps to node 5; slow on 1. Then (2, 6), (3, 7), (4, 8), (5, None) → stop. Slow is on 5, one before 6 ✓.

Doubt 2: why n + 1 jumps and not n?
→ With a gap of n, slow would stop on the target itself. That's what you'd want if the question said "return the n-th node from the end". But we must delete it, and deleting needs the node before it. One extra jump of head start puts slow one node earlier.

The ugly case, and the dummy node

Now try one node [1] with n = 1. Fast needs 2 jumps from head, but after 1 jump it's already None, and the second jump would crash. You could patch it with checks like "if head.next is None and n == 1, return None", and also the general "target is head" case, but the teacher says that's a lot of messy if-else.

Instead: put a dummy node (value 0) in front of head, and start both slow and fast on the dummy.

[0] → [1] → None
 ↑
slow, fast (both on dummy)
Doubt 3: why does the dummy fix every head case, not just the one-node case?
→ With the dummy, the list is one node longer, and the dummy is "the node before head". Fast's n + 1 jumps from the dummy are always possible: the list from the dummy has L + 1 nodes, and n + 1 ≤ L + 1. When n = L, fast lands exactly on None and slow stays on the dummy, which is precisely the node before the head. So deleting the head is the same line as deleting anything else.
Doubt 4: why return dummy.next and not head or dummy?
→ dummy is our fake node with value 0. It must not appear in the answer, so not dummy. And head may have just been deleted (when n = L), so it could be the wrong start. dummy.next always points at the real first node of the final list.

5Approach steps

  1. Make dummy = ListNode(0), dummy.next = head.
  2. Put slow and fast on the dummy.
  3. Move fast n + 1 times.
  4. While fast is not None: move both one step.
  5. Now slow is just before the target: slow.next = slow.next.next.
  6. Return dummy.next.

6Code (Python)

Remove Nth from End, one pass with a gap and a dummy
class Solution:
    def removeNthFromEnd(self, head, n):
        dummy = ListNode(0)              # fake node before head
        dummy.next = head
        slow = dummy
        fast = dummy

        for _ in range(n + 1):           # head start: n + 1 jumps
            fast = fast.next

        while fast is not None:          # same speed until fast falls off
            slow = slow.next
            fast = fast.next

        slow.next = slow.next.next       # slow is just before the target
        return dummy.next                # the real head (may be new)

7Code line by line

linewhat it means
dummy = ListNode(0) dummy.next = headA fake node in front, so even the head has a "previous node".
slow = dummy fast = dummyBoth start at the same place, before the real list.
for _ in range(n + 1): fast = fast.nextGive fast a head start of n + 1 nodes. (The video's loop "i from 0 to n, inclusive" is the same n + 1 jumps.) Never crashes, because n ≤ L.
while fast is not None:Run until fast falls off the end.
slow = slow.next fast = fast.nextSame speed, so the gap stays exactly n + 1.
slow.next = slow.next.nextFast is on None, n + 1 steps ahead of slow, so slow.next is the n-th node from the end. Skip it.
return dummy.nextThe first real node of the final list (None if the list is now empty).

8Dry run

List 1 → 2 → 3 → 4 → 5, n = 2. With the dummy: 0 → 1 → 2 → 3 → 4 → 5 → None.

stepslowfastwhat changesanswer so far
startdummydummydummy.next = 1—
head start 1dummy1——
head start 2dummy2——
head start 3dummy3gap is now n + 1 = 3—
move 114——
move 225——
move 33Noneloop stops—
delete3None3.next: 4 → 5return dummy.next = 1 → 2 → 3 → 5
after the head start (gap = 3):
[0] → [1] → [2] → [3] → [4] → [5] → None
 ↑                 ↑
slow              fast

when fast falls off:
[0] → [1] → [2] → [3] → [4] → [5] → None
                   ↑                  ↑
                  slow               fast

after slow.next = slow.next.next:
[0] → [1] → [2] → [3] ──────→ [5] → None
       ↑
  dummy.next (returned)

Removing the head: 1 → 2 → 3, n = 3

stepslowfastwhat changes
head start (4 jumps)dummy1, 2, 3, Nonefast is already None
while loopdummyNonedoesn't run
deletedummyNonedummy.next: 1 → 2
returndummy.next = 2 → 3 ✓ (head removed, no special case)

9Complexity & remember

Remember the gap trickDummy in front · slow = fast = dummy · fast jumps n + 1 · move both until fast is None · slow.next = slow.next.next · return dummy.next.

Part C · Revision page

Two passes (brute force)Gap + dummy (optimal)
ideacount L, then walk L − n − 1 jumpsfast starts n + 1 ahead; same speed; slow ends before target
head case (n = L)special if: return head.nexthandled by the dummy, no if
passes21
returnsheaddummy.next
time / spaceO(2n) / O(1)O(n) / O(1)
fast & slow so farspeedsstartwhat it finds
Middle of list1 and 2both at headthe middle
Cycle I / II1 and 2both at heada meeting point / the loop start
Nth from end1 and 1fast n + 1 aheadthe node before the n-th from end
If you remember only 5 lines 1. To delete a node you must stand on the node before it: prev.next = prev.next.next.
2. Brute force: count L, jump L − n − 1 times; if n == L, return head.next.
3. "Fast" can mean "ahead", not "double speed".
4. Gap of n + 1 (not n), both from a dummy, same speed until fast is None.
5. Return dummy.next. One pass, O(1) space.
Mistakes to avoid ✗ mixing two counting styles with one formula (off by one)
✗ gap of n instead of n + 1 (slow lands on the target, not before it)
✗ starting slow/fast at head without a dummy (crash when n = length)
✗ returning head instead of dummy.next (head may be the deleted node)
✗ returning dummy (the fake 0 shows up in the answer)
test it yourself (paste under either solution above)
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

def build(values):
    dummy = ListNode()
    tail = dummy
    for v in values:
        tail.next = ListNode(v)
        tail = tail.next
    return dummy.next

def to_list(head):
    out = []
    while head:
        out.append(head.val)
        head = head.next
    return out

s = Solution()
print(to_list(s.removeNthFromEnd(build([1, 2, 3, 4, 5]), 2)))   # [1, 2, 3, 5]
print(to_list(s.removeNthFromEnd(build([1]), 1)))               # []
print(to_list(s.removeNthFromEnd(build([1, 2]), 1)))            # [1]
print(to_list(s.removeNthFromEnd(build([1, 2]), 2)))            # [2]
print(to_list(s.removeNthFromEnd(build(list(range(1, 9))), 3))) # [1, 2, 3, 4, 5, 7, 8]

Based on this video: Remove Nth Node From End of List | Fast and Slow Pointer