📒 problems 📖 glossary
Mock interview
46:30
ready
of 46:30
Lesson 18 · Intermediate

Linked lists — dummy heads + fast/slow

📖 Walk me through it — plain English

A linked list is a chain of little boxes called nodes. Each node holds a value plus a single arrow (called next) pointing to the following node. The last node's arrow points at None (Python's word for "nothing"), which marks the end. Unlike an array, the boxes are scattered in memory and you can only reach a node by following arrows from the front — you cannot jump to "index 5" directly.

This lesson teaches the single most common operation: reversing the list, so the arrows all point the other way. The trick is that you walk the chain one node at a time and flip each arrow as you pass it. The hard part is that flipping an arrow destroys the path forward — so before you flip, you must save where you were about to go.

Analogy: imagine a line of people, each tapping the shoulder of the person in front of them. To reverse the line, you go person by person and have each one instead tap the person behind them. But the moment someone turns around, they forget who was ahead — so you jot that name on a sticky note first. That sticky note is the variable nxt.

We track three things: prev (the node we just finished, who the current arrow should now point back to), curr (the node we're flipping right now), and nxt (the sticky note holding the node after curr). Let's reverse the tiny list 1 → 2 → 3. The accent box is curr (where we are), and a green box marks the answer at the end.

Step 0 · Start. prev = None (nothing behind yet). curr sits on node 1.
1
2
3
Step 1 · Save nxt = node 2 (sticky note). Flip node 1's arrow to point at prev (None). Now move prev to node 1, curr to node 2.
1
2
3
Step 2 · Save nxt = node 3. Flip node 2's arrow to point back at node 1 (prev). Move prev to node 2, curr to node 3.
1
2
3
Step 3 · Save nxt = None. Flip node 3's arrow to point back at node 2. Move prev to node 3, curr to None.
1
2
3
Step 4 · curr is None, so the while-loop stops. We return prev, which now points at node 3 — the new head. The list reads 3 → 2 → 1.
3
2
1

Why it works: every node gets visited once, and at each visit we do a fixed amount of work — save the next pointer, flip one arrow, slide the two markers forward. The loop ends exactly when curr walks off the end into None, and at that instant prev is sitting on the old last node, which is the new front. That is why we return prev, not curr. Because we touch each of the n nodes a single time and reuse the same three variables, the time is O(n) (grows in step with the list length) and the extra memory is O(1) (just three pointers, no matter how long the list).

The dummy head note at the bottom solves a different headache: some problems (delete a node, merge two lists) might change which node is first. Without a guard, you'd write special "what if the head itself changed?" branches. Instead you glue a throwaway node in front with dummy = ListNode(0, head), do all your work, then return dummy.next — the real first node, whatever it ended up being. The fast/slow pointer idea (one pointer hops two steps, one hops one) lets you find the middle or detect a loop in a single pass using O(1) space; the reversal above is the warm-up for those.

Two tricks cover 80%: a dummy head eliminates edge-case branches, and fast/slow pointers find midpoints and detect cycles in O(1) space.

The on-ramp: what a linked list is, and why bother

An array (Python list) stores its values in one contiguous block of memory, so the machine can compute the address of element i with simple arithmetic — that is why a[5] is instant, or O(1) ("constant time" — the cost does not grow with the size of the list). The price is the opposite operation: inserting or deleting near the front means shifting every later element over by one slot, which is O(n) ("linear time" — cost grows in step with n, the number of elements).

A linked list trades those costs the other way. Because each node is a separate box that merely points to the next, you cannot do address arithmetic — to reach the 5th node you must start at the front and follow arrows five times, which is O(n) access. But if you are already holding the node where you want to splice something in or cut something out, the edit is just a couple of pointer rewires: O(1) insert/delete, with no shifting. That single trade-off — O(n) to find, O(1) to edit once found — is the whole reason linked lists exist and the reason interview problems about them are really pointer-juggling puzzles.

One-line mental model: an array is a row of numbered lockers (jump anywhere fast, but re-arranging is a chore); a linked list is a treasure hunt where each clue names the next location (you must walk the trail, but adding or removing a clue is trivial once you hold the one before it).

Vocabulary, defined inline

Every linked-list problem is built from the same handful of words. Pin them down once and the rest is mechanical.

  • Node — one box in the chain. It holds a value (often called val) and at least one link. A minimal node is just { val, next }.
  • Pointer / reference — the arrow itself: a variable that does not hold a value but holds the location of a node. In Python and TypeScript there are no raw memory addresses; a "pointer" here is simply a variable bound to a node object. Setting a.next = b means "make a's arrow point at the same node b refers to." Two variables can point at the same node — that is the source of most bugs and most cleverness.
  • Head — the variable that points at the first node; your only handle on the whole list. Lose the head with no other reference and the list is unreachable (garbage-collected).
  • Tail — the last node, whose next is None/null. The None at the end is the sentinel value that tells a traversal "stop."
  • Singly linked — each node has exactly one link, next, so you can only travel forward. Doubly linked — each node also carries a prev link back to the node before it, so you can walk both directions and delete a node without first finding its predecessor; the cost is an extra pointer per node to keep in sync.
  • Dummy / sentinel node — a throwaway node you place before the real head so that "the first node" is never a special case. Its value is ignored; only its next matters. Return dummy.next at the end.
  • Traversal — walking the chain from the head, one next at a time, until you hit None. The canonical loop is while node: ... node = node.next.
  • In-place reversal — flipping every arrow to point backward without allocating a new list, reusing the existing nodes. "In place" means O(1) extra space.
  • Two-pointer / runner technique — moving two pointers through the same list at different speeds or with a fixed gap between them. The fast/slow ("tortoise and hare") variant moves one pointer two steps for every one step of the other.

Defining the node

Before any algorithm, you need the box itself. Both languages define a node as a tiny object with a value and a link that defaults to "nothing." This is exactly the ListNode the templates below assume.

# Python — a node is a value plus a forward link
class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val        # the data this box holds
        self.next = next       # arrow to the following node, or None at the tail

To build 1 → 2 → 3 by hand you chain three of them: a = ListNode(1, ListNode(2, ListNode(3))). The outermost node is the head; the innermost node's next is the default None, marking the tail.

Traversal: the loop everything is built on

Every linked-list routine is a variation on one walk. You start at the head and keep hopping to .next until the pointer becomes None. Read the loop as "while there is still a node, do something, then step forward."

# Count the nodes — the simplest possible traversal
def length(head):
    count, node = 0, head
    while node:               # stops when node is None (we walked off the tail)
        count += 1
        node = node.next       # hop to the next box
    return count

Notice we copy head into a throwaway variable node first. If we walked head itself forward, we would lose our handle on the start of the list. Never advance the only pointer you have to the front.

Template — Reverse Linked List

Template — Reverse Linked List
def reverse(head):
    prev, curr = None, head
    while curr:
        nxt = curr.next
        curr.next = prev
        prev, curr = curr, nxt
    return prev
function reverse(head: ListNode | null): ListNode | null {
  let prev: ListNode | null = null, curr = head;
  while (curr) {
    const nxt = curr.next;
    curr.next = prev;
    prev = curr; curr = nxt;
  }
  return prev;
}

A fully traced reversal of 1 → 2 → 3

The walkthrough above showed the boxes; here is the same reversal as a table of the three variables at the top of every loop iteration, so you can see exactly what each line of the template does. Read each row as "this is the state right after that iteration's four statements ran." · means the node it points at; means None.

# Start:  prev = ∅ ,  curr = 1 ,  list is 1 → 2 → 3
#
# iter 1:  nxt = curr.next        -> nxt  = 2   (save the rest BEFORE we cut)
#          curr.next = prev       -> 1.next = ∅  (node 1 now points back at nothing)
#          prev, curr = curr, nxt -> prev = 1 ,  curr = 2
#          chain so far:  ∅ ← 1      2 → 3
#
# iter 2:  nxt = curr.next        -> nxt  = 3
#          curr.next = prev       -> 2.next = 1  (node 2 now points back at 1)
#          prev, curr = curr, nxt -> prev = 2 ,  curr = 3
#          chain so far:  ∅ ← 1 ← 2    3
#
# iter 3:  nxt = curr.next        -> nxt  = ∅
#          curr.next = prev       -> 3.next = 2  (node 3 now points back at 2)
#          prev, curr = curr, nxt -> prev = 3 ,  curr = ∅
#          chain so far:  ∅ ← 1 ← 2 ← 3
#
# curr is ∅ -> loop ends. return prev = 3.  New list: 3 → 2 → 1.

The single most important line is the first one of each iteration. The moment we run curr.next = prev, the original forward arrow is overwritten — so if we had not stashed nxt = curr.next a line earlier, the tail of the list would be orphaned and unreachable. Save first, then cut.

The dummy-node trick, spelled out

Dummy head trick: for problems that might modify the head (remove Nth from end, merge two lists), prefix with dummy = ListNode(0, head) and return dummy.next. No special-cases for "the head changed."

Why the trick earns its keep: in any routine that deletes or inserts nodes, the front of the list is dangerous because it has nothing in front of it. If you want to delete a middle node, you grab the node before it and set before.next = before.next.next — clean. But the head has no "before," so deleting it normally needs a separate branch (head = head.next). A dummy node manufactures a "before" for the real head, so the head stops being special and one piece of code handles every position.

# Remove every node equal to target, including a head that matches.
def remove_all(head, target):
    dummy = ListNode(0, head)   # sentinel sits BEFORE the real head
    prev = dummy
    while prev.next:
        if prev.next.val == target:
            prev.next = prev.next.next  # splice the node out — works even at the head
        else:
            prev = prev.next
    return dummy.next               # the real head, even if the old one was removed

Returning dummy.next instead of head is what makes this robust: if the original head was deleted, head would be a stale pointer to a removed node, but dummy.next always names whatever node is genuinely first when you finish.

Fast/slow pointers: middle and cycle in one pass

The two-pointer runner technique answers two classic questions without a length count and without extra memory. Move slow one step and fast two steps per loop. When fast reaches the end, slow has covered exactly half the distance — so it sits on the middle node. And if the list secretly loops back on itself (a cycle), the fast pointer eventually laps the slow one and they collide — Floyd's cycle detection.

# Middle of the list: when fast falls off the end, slow is the midpoint.
def middle(head):
    slow = fast = head
    while fast and fast.next:   # guard BOTH so fast.next.next is safe
        slow = slow.next         # one step
        fast = fast.next.next    # two steps
    return slow

# Cycle detection: if there is a loop, fast and slow must meet inside it.
def has_cycle(head):
    slow = fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow is fast:        # same node object -> they collided -> cycle
            return True
    return False          # fast hit None -> the list has an end -> no cycle

The condition while fast and fast.next is the linchpin and a frequent crash site. You write fast.next.next inside the loop, so you must first prove that both fast and fast.next exist; checking only fast would let fast.next.next blow up on the last node.

Recognition signals — reach for these patterns when…

  • The problem hands you a head and talks about next pointers, "in place," or "without extra space" — almost always pointer rewiring, not rebuilding.
  • It mentions the middle, the n-th from the end, or a cycle / loop — fast/slow or a fixed-gap two-pointer.
  • It says reverse, or reverse a sub-section, or reverse in groups of k — the prev/curr/nxt flip is the building block.
  • It deletes or merges and the head might change — add a dummy node and return dummy.next.
  • You only need O(1) extra space and a single pass — that constraint is the loudest hint that a clever pointer trick, not an array copy, is intended.

Pitfalls — the bugs that bite everyone

Losing the next pointer

Rewiring curr.next = prev before saving nxt = curr.next orphans the rest of the list. Always stash the forward link first, then cut.

Null / None dereference

Touching node.next when node is None crashes. Order your loop condition so the existence check happens before the dereference — while fast and fast.next, never the reverse.

Empty or single-node lists

head is None and a one-node list are the inputs that break naive code. The reversal template already handles both for free; check that yours does too.

Returning the wrong end

After a reversal, curr is None and prev is the new head — return prev. After a dummy-node edit, return dummy.next, not the now-stale head.

Creating an accidental cycle

When you forget to terminate a tail with None (e.g., after reordering), a later traversal loops forever. End the chain explicitly.

Walking your only handle

Advancing head itself loses the start of the list. Copy it into a working pointer, or keep a dummy out front.

Go deeper (optional): the reversal here is the seed for "reverse nodes in k-group" and "reverse a sublist." Floyd's cycle detection extends to finding the start of the loop (reset one pointer to the head and step both one at a time until they meet). For the underlying proofs, search "Floyd's tortoise and hare" and the classic LeetCode 206 / 141 / 142 / 19 problems.

Takeaway: a linked list is nodes joined by next arrows — O(n) to find a position, O(1) to edit once you hold the node before it. Master three moves: the prev/curr/nxt in-place reversal (save before you cut), the dummy node that makes the head ordinary so deletes and merges need no special case (return dummy.next), and the fast/slow runner that finds the middle or detects a cycle in one O(1)-space pass (guard fast and fast.next). Every other list problem is a remix of these.

Merge two sorted lists — the core pointer dance from this lesson:

→ Going deeper: Linked lists become interesting when you detect cycles with fast and slow pointers. See Fast & slow pointers (cycle detection).