~/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.
Walk the list once. Save the next node, turn the current node's arrow back to the previous one, then step both pointers forward.
You're handed list nodes and must rewire them in place: reverse, merge, remove, find the middle, spot a cycle.
O(n)
O(1)
You’ll recognise it when
- The input is a
ListNode(orNode) with anextfield, 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.
- Start with
prev = Noneandcurr = head.prevleads the part that’s already reversed;currleads the part still to do. - Save the rest of the list:
nxt = curr.next. This has to come first, because the next line overwritescurr.next. - 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. - Step forward:
prev = curr, thencurr = nxt. The order matters, sinceprevneeds the oldcurr. - Repeat. Halfway through there are two separate lists:
prevleads the reversed one andcurrleads the rest. No arrow joins them, and nothing is lost because we always hold both ends. - When
currfalls off the end (None), every arrow has been turned.previs on the old tail, which is the new head: return it.
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 prevstruct 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
leftandright. Walk to the node just beforeleft, reverse the nextright - left + 1nodes with the same three-pointer loop, then reconnect both ends. Put a dummy head in front first, soleft = 1needs no special case. - Merge two sorted lists (dummy head). Make
dummy = Node(0)and atailpointer at it. Repeatedly attach the smaller of the two front nodes totail.nextand movetailon; when one list runs out, attach the rest of the other in one step. Returndummy.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
fastn steps ahead. Then move both untilfastis on the last node:slowis now just before the node to remove, soslow.next = slow.next.next. Thanks to the dummy, removing the head works too. - Find the middle.
slowmoves one step andfasttwo, whilefast and fast.next. Whenfastruns out,slowis 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
slowandfast. Iffastreaches None there’s no cycle. If there is one, both end up inside it andfastgains 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
Rearrange
L0 → L1 → … → LnintoL0 → Ln → L1 → Ln-1 → …in place, with O(1) extra space. Which plan fits?Three linked-list moves you already know add up to O(n) time and O(1) space. The array and stack versions work but use O(n) memory. Walking to the tail each time is O(n²).
-
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 = valself.next = nextdef build(vals):head = Nonefor v in reversed(vals):head = Node(v, head)return headdef reverse(head):prev, curr = None, headwhile curr:curr, curr.next, prev = curr.next, prev, currreturn prevtry:print(reverse(build([1, 2, 3])).val)except AttributeError:print("AttributeError")curris rebound to the next node beforecurr.next = prevruns, so the arrow that gets written belongs to the wrong node. On the last passcurrbecomes None andNone.next = ...raises. Putcurr.nextbeforecurrin the target list:curr.next, prev, curr = prev, curr, curr.nextworks. -
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 = valself.next = nextdef build(vals):head = Nonefor v in reversed(vals):head = Node(v, head)return headdef middle(head, first):slow = fast = headif first:while fast.next and fast.next.next:slow, fast = slow.next, fast.next.nextelse:while fast and fast.next:slow, fast = slow.next, fast.next.nextreturn slow.valprint(middle(build([1, 2, 3, 4]), False), middle(build([1, 2, 3, 4]), True))while fast and fast.nextstops on the second middle of an even-length list; checkingfast.next and fast.next.nextstops one step earlier, on the first. Splitting a list in half (to reverse the back half) usually wants the first, so you can cut afterslow. -
4
This removes the n-th node from the end. Called as
remove_nth_from_end(build([10, 20, 30]), 3)it crashes withAttributeError. What’s the fix?def remove_nth_from_end(head, n):fast = slow = headfor _ in range(n):fast = fast.nextwhile fast.next:fast, slow = fast.next, slow.nextslow.next = slow.next.nextreturn headn = 3 means removing the head itself.
fastwalks off the end to None, sofast.nextraises, and even if it didn’t, there’s no node before the head forslowto stop on. A dummy gives every real node a predecessor, so the same three lines handle the head. Loopingn - 1times just removes the wrong node. -
5
You reverse a 5,000-node list with a recursive function in CPython, default settings. What happens?
CPython doesn’t optimise tail calls, and
sys.getrecursionlimit()is 1000 by default, so any recursion as deep as the list fails on long inputs. “Works with O(n) stack” would be true where the stack is big enough. The three-pointer loop needs O(1) space and no stack at all.