~/data-structures/linked-lists

Linked lists

Reverse a linked list in place with three pointers, then reuse the same habits: a dummy head, and fast and slow pointers.

what

Walk the list once. Save the next node, turn the current node's arrow back to the previous one, then step both pointers forward.

use when

You're handed list nodes and must rewire them in place: reverse, merge, remove, find the middle, spot a cycle.

time

O(n)

space

O(1)

You’ll recognise it when

  • The input is a ListNode (or Node) with a next field, not an array.
  • You must rewire nodes rather than copy them: reverse, reorder, merge, delete.
  • The task says in place or O(1) extra memory, so dumping the values into an array is off the table.
  • You need a position you can’t index directly: the middle, the k-th from the end, where a cycle starts.

Fast and slow pointers on a list are a cousin of two pointers on an array: both walk a sequence with two positions, but on a list you can only ever move forward.

The idea

Picture a line of people, each with a hand on the shoulder of the person in front. To make the line face the other way, you walk down it and ask each person to put their hand on the person behind them instead. The catch: the moment someone lets go of the shoulder in front, nobody in your part of the line is touching the rest of it any more. So before anyone lets go, you point at the person in front so you don’t lose them.

That’s the whole algorithm. A singly linked list only knows “next”. To reverse it you turn every next arrow around, one node at a time, and you need three fingers to do it without dropping anything: one on the node behind (prev), one on the node you’re turning (curr), and one saving the rest of the list (nxt).

How it works

prev starts at None (the reversed part is empty) and curr at the head. Scroll through the steps and the graphic follows along; press play, or edit the list and try one or two nodes.

  1. Start with prev = None and curr = head. prev leads the part that’s already reversed; curr leads the part still to do.
  2. Save the rest of the list: nxt = curr.next. This has to come first, because the next line overwrites curr.next.
  3. Turn the arrow around: curr.next = prev. The first node now points to None, which is right: it’s the tail of the reversed list.
  4. Step forward: prev = curr, then curr = nxt. The order matters, since prev needs the old curr.
  5. Repeat. Halfway through there are two separate lists: prev leads the reversed one and curr leads the rest. No arrow joins them, and nothing is lost because we always hold both ends.
  6. When curr falls off the end (None), every arrow has been turned. prev is on the old tail, which is the new head: return it.
loading linked-lists…

Why it’s correct: at the top of every loop, following arrows from prev gives the nodes already visited in reverse order, and following from curr gives the untouched nodes in their original order. One pass moves exactly one node from the second list to the front of the first, so when the second list is empty, prev holds the whole list reversed.

class Node:
def __init__(self, val, next=None):
self.val = val
self.next = next
def reverse(head):
"""Reverse a singly linked list in place and return the new head."""
prev = None
curr = head
while curr:
nxt = curr.next
curr.next = prev
prev = curr
curr = nxt
return prev
struct Node {
int val;
Node* next;
Node(int val, Node* next = nullptr) : val(val), next(next) {}
};
// Reverse a singly linked list in place and return the new head.
Node* reverse(Node* head) {
Node* prev = nullptr;
Node* curr = head;
while (curr) {
Node* nxt = curr->next;
curr->next = prev;
prev = curr;
curr = nxt;
}
return prev;
}
class Node {
int val;
Node next;
Node(int val, Node next) {
this.val = val;
this.next = next;
}
}
class LinkedList {
// Reverse a singly linked list in place and return the new head.
static Node reverse(Node head) {
Node prev = null;
Node curr = head;
while (curr != null) {
Node nxt = curr.next;
curr.next = prev;
prev = curr;
curr = nxt;
}
return prev;
}
}

Why it’s O(n)

The loop runs once per node, and each pass does three pointer assignments. That’s O(n) time for n nodes. The only extra memory is three pointers, whatever the length, so space is O(1).

Approach Time Extra space
Copy values into an array, write them back reversed O(n) O(n)
Recursive reverse O(n) O(n) call stack
Three pointers (this one) O(n) O(1)

Common mistakes

Losing the rest of the list

If you turn the arrow before saving curr.next, the only link to the rest of the list is gone, and curr can’t move on.

curr.next = prev; curr = curr.next # ✗ curr.next is prev now: walks backwards
nxt = curr.next; curr.next = prev; curr = nxt # ✓ save first, then rewire

Returning the wrong head

When the loop ends, curr is None and head still points at the old first node, which is now the tail. Its next is None, so returning head gives a one-node list. The new head is prev.

return head # ✗ the old head is now the last node
return prev # ✓ the old tail, now first

Moving curr before prev

prev = curr has to see the old curr. Swap the two lines and prev lands on the next node, so the following flip points a node at itself.

curr = nxt; prev = curr # ✗ prev skips ahead to nxt
prev = curr; curr = nxt # ✓ prev takes the node we just turned

Recursing on a long list

A recursive reverse is short and neat, but it uses one stack frame per node. CPython stops at about 1,000 frames, so a 5,000-node list raises RecursionError, and C++ and Java overflow their stack on long enough lists. The loop has no such limit.

Variations

  • Reverse between positions left and right. Walk to the node just before left, reverse the next right - left + 1 nodes with the same three-pointer loop, then reconnect both ends. Put a dummy head in front first, so left = 1 needs no special case.
  • Merge two sorted lists (dummy head). Make dummy = Node(0) and a tail pointer at it. Repeatedly attach the smaller of the two front nodes to tail.next and move tail on; when one list runs out, attach the rest of the other in one step. Return dummy.next. The dummy means the first node is handled like every other. For k lists, pick the smallest front with a heap.
  • Remove the n-th node from the end. Start two pointers at a dummy head and move fast n steps ahead. Then move both until fast is on the last node: slow is now just before the node to remove, so slow.next = slow.next.next. Thanks to the dummy, removing the head works too.
  • Find the middle. slow moves one step and fast two, while fast and fast.next. When fast runs out, slow is on the middle (the second of the two middles on an even-length list). This is the first move of “reorder list” and “is it a palindrome”: find the middle, reverse the second half, compare or interleave.
  • Detect a cycle (Floyd). The same slow and fast. If fast reaches None there’s no cycle. If there is one, both end up inside it and fast gains one node per step, so they meet within one lap: O(n) time, O(1) space. To find where the cycle starts, move one pointer back to the head and step both by one; they meet at the cycle’s first node.

Check yourself

5 quick questions. Pick an answer to see why it's right or wrong.

  1. 1

    Rearrange L0 → L1 → … → Ln into L0 → Ln → L1 → Ln-1 → … in place, with O(1) extra space. Which plan fits?

  2. 2

    A one-line reversal with tuple assignment. Python evaluates the right side first, then assigns the targets left to right. What does this print?

    class Node:
    def __init__(self, val, next=None):
    self.val = val
    self.next = next
    def build(vals):
    head = None
    for v in reversed(vals):
    head = Node(v, head)
    return head
    def reverse(head):
    prev, curr = None, head
    while curr:
    curr, curr.next, prev = curr.next, prev, curr
    return prev
    try:
    print(reverse(build([1, 2, 3])).val)
    except AttributeError:
    print("AttributeError")
  3. 3

    Two loop conditions for finding the middle of 1 → 2 → 3 → 4. What does this print?

    class Node:
    def __init__(self, val, next=None):
    self.val = val
    self.next = next
    def build(vals):
    head = None
    for v in reversed(vals):
    head = Node(v, head)
    return head
    def middle(head, first):
    slow = fast = head
    if first:
    while fast.next and fast.next.next:
    slow, fast = slow.next, fast.next.next
    else:
    while fast and fast.next:
    slow, fast = slow.next, fast.next.next
    return slow.val
    print(middle(build([1, 2, 3, 4]), False), middle(build([1, 2, 3, 4]), True))
  4. 4

    This removes the n-th node from the end. Called as remove_nth_from_end(build([10, 20, 30]), 3) it crashes with AttributeError. What’s the fix?

    def remove_nth_from_end(head, n):
    fast = slow = head
    for _ in range(n):
    fast = fast.next
    while fast.next:
    fast, slow = fast.next, slow.next
    slow.next = slow.next.next
    return head
  5. 5

    You reverse a 5,000-node list with a recursive function in CPython, default settings. What happens?

Practice problems

Further reading

esc