How to Approach Linked List Problems

Golden Rule

Before writing a single line of code, break the problem into micro-operations. Every linked list problem reduces to some combination of: traverse, rewire, insert, delete, split, merge.

Step-by-step framework

  1. Draw it out. Sketch 4–5 nodes. Label head, prev, curr, next, or whatever pointers the problem needs. You will misfire on pointer order if you skip this.

  2. Identify the invariant. What pointer relationships must stay true after every iteration?

    • Reversal → prev always points to the already-reversed portion.
    • Fast/slow → fast is always 2× ahead of slow.
  3. Determine your anchors. Which pointers move and which stay fixed?

    • Classic reversal: all three (prev, curr, next) walk forward.
    • Head-insertion reversal: prev and curr are fixed; only the splice target moves.
  4. Handle edges first. Before the main loop, account for:

    • head is None
    • Single node
    • Operation at head (use a dummy/sentinel node to avoid special-casing)
  5. Write the loop body in CUT → LINK → MOVE order. Every rewiring step is one of:

    • CUT — detach a node from its current position
    • LINK — point it to its new neighbor
    • MOVE — advance your working pointer(s)

Common Pitfall

If you update curr.next before saving a reference to the next node, you lose the rest of the list. Always stash your forward reference first.


Classic Three-Pointer Reversal

The bread-and-butter. Reverses an entire linked list (or a segment if you set boundaries). Walk three pointers — prev, curr, next — through the list. At each step, flip curr.next to point backward.

Template

def reverseList(head):
    prev = None
    curr = head
 
    while curr:
        next_node = curr.next   # 1. STASH forward ref
        curr.next = prev        # 2. FLIP the pointer
        prev = curr             # 3. ADVANCE prev
        curr = next_node        # 4. ADVANCE curr
 
    return prev  # prev is the new head

Head-Insertion Reversal (Sublist Splicing)

Also known as

  • In-place sublist reversal by front insertion
  • Head-insertion method for reversing a segment
  • Iterative sublist reversal / node splicing

This is the technique to reach for when you need to reverse a segment of the list in-place without touching the rest.

Core insight

You keep two anchors fixed:

PointerRoleMoves?
prevThe node before the segment❌ Stays fixed
currThe first node of the segment (becomes the tail after reversal)❌ Stays fixed

Then you repeatedly extract curr.next and splice it to the front of the segment (right after prev).

Template

def reverseBetween(head, left, right):
    dummy = ListNode(0, head)
    prev = dummy
 
    # 1. Walk prev to the node just before position `left`
    for _ in range(left - 1):
        prev = prev.next
 
    curr = prev.next  # first node of the segment (won't move)
 
    # 2. Perform (right - left) front-insertions
    for _ in range(right - left):
        x = curr.next       # CUT target
        curr.next = x.next  # CUT it out of the chain
        x.next = prev.next  # LINK it to the current front of the segment
        prev.next = x       # MOVE the segment front pointer to x
 
    return dummy.next

Why curr never moves

curr is always the original first node of the segment. As new nodes get spliced in front of it, curr naturally drifts to the tail — exactly where it should end up. Meanwhile, curr.next always points to the next node to extract.

x = curr.next       # CUT   — grab the target
curr.next = x.next  # CUT   — snip it out
x.next = prev.next  # LINK  — wire it to the front
prev.next = x       # MOVE  — update the front

When to pick this over the classic reversal

  • Reverse Linked List II (LC 92) — reverse between positions
  • Reverse Nodes in k-Group (LC 25) — apply this in a loop
  • Any time the reversal is a sub-operation inside a bigger problem
  • When you want the connecting nodes (prev, tail after curr) to stay wired correctly with zero fixup

Fast & Slow Pointers

Two pointers moving at different speeds through the list. The asymmetry is what gives you information.

When to use

ProblemFast speedSlow speedWhat you learn
Detect cycle2 steps1 stepIf they meet → cycle exists
Find cycle start1 step (after meeting)1 step (from head)Meeting point = cycle entry
Find middle2 steps1 stepWhen fast hits end, slow is at middle
Kth from endstart k steps ahead, then 1 step1 stepWhen fast hits end, slow is kth from end
Palindrome check2 steps (find middle)1 stepSplit at middle, reverse second half, compare

Template — cycle detection (Floyd’s Algorithm)

def hasCycle(head):
    slow = fast = head
 
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
 
    return False

Template — find cycle start

def detectCycle(head):
    slow = fast = head
 
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            # Phase 2: find entry point
            slow = head
            while slow != fast:
                slow = slow.next
                fast = fast.next
            return slow  # cycle start
 
    return None

Why does phase 2 work?

Let the distance from head to cycle start be a, and the distance from cycle start to the meeting point be b. At the meeting point, slow traveled a + b and fast traveled 2(a + b). The extra distance fast covered is full cycle laps. Resetting slow to head and moving both at speed 1 means they converge at exactly the cycle start after a steps.

Template — find middle

def findMiddle(head):
    slow = fast = head
 
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
 
    return slow  # middle (right-middle for even-length)

Left-middle vs right-middle

The template above gives the right-middle for even-length lists (1→2→3→4 returns 3). If you need the left-middle, use while fast.next and fast.next.next instead.

Template — remove nth from end

def removeNthFromEnd(head, n):
    dummy = ListNode(0, head)
    fast = slow = dummy
 
    # Advance fast by n+1 so the gap is n
    for _ in range(n + 1):
        fast = fast.next
 
    while fast:
        slow = slow.next
        fast = fast.next
 
    slow.next = slow.next.next  # skip the target
    return dummy.next

Quick Reference — Dummy Node Trick

Use a sentinel / dummy node whenever the head might change

dummy = ListNode(0, head)
# ... do your work ...
return dummy.next

This eliminates every if not head or if curr == head edge case. Use it liberally — there’s no cost.


Pattern Cheat Sheet

PatternKey PointersFixed or Walking?Core Problems
Classic reversalprev, curr, nextAll walkLC 206, 234
Head-insertion spliceprev, curr, xprev & curr fixedLC 25, 92
Fast/slow — cycleslow, fastBoth walk (different speeds)LC 141, 142
Fast/slow — middleslow, fastBoth walkLC 876, 143, 234
Fast/slow — kth from endslow, fastBoth walk (staggered start)LC 19, 61
Merge two listsp1, p2, dummyp1, p2 walkLC 21, 23, 148
Dummy sentineldummyFixedAlmost every problem

Parting thought

Every linked list problem is just pointer plumbing. If you can draw the before-and-after state of 4 nodes and write the rewiring steps in order, you can solve it. The patterns above cover ~90% of what LeetCode throws at you.

Linked Map of Contexts