Linked List Patterns
Linked lists are the one topic where interviewers watch your pointer discipline live. D2 introduced the node and the basic list; this part is the complete pattern catalog — every list type (singly, doubly, circular) and every manipulation pattern (dummy head, reversal in all its forms, two-pointer tricks, merging, reordering, and the hard "copy with random pointer") — each with a diagram of the pointer moves and clean Python.
Learning objectives
- Know the three list types — singly, doubly, circular — and when each is used.
- Use the dummy (sentinel) head trick to kill edge cases.
- Reverse a list: whole, between two positions, and in K-groups.
- Apply the two-pointer trio: middle, nth-from-end, cycle start.
- Merge, reorder, add-two-numbers, detect palindrome, find intersection.
- Solve the hard ones: copy-with-random-pointer, flatten a multilevel list.
node.next before you overwrite it. (4) Two pointers at different speeds/offsets solve most "find the …" problems in one pass. Master these four and every problem below is mechanical.1 · The three list types basic
All linked lists are nodes joined by pointers; they differ only in which pointers each node carries and whether the tail loops back.
prev pointer (walk both ways, O(1) delete of a held node). Circular loops the tail back to the head (round-robin schedulers, ring buffers).
A linked list is a chain of little boxes called nodes. Each node holds a value and a pointer — an arrow that says "the next node is over there". Unlike an array, the boxes are scattered in memory; the arrows are the only thing holding the list together. This picture shows the three shapes a chain can take.
- Each box (A, B, C) is one node holding a value. The arrows between boxes are the
nextpointers — follow an arrow to reach the following node. - Singly (top): every node has just one arrow pointing forward. The last node's arrow points to
∅(nothing) — that's how you know you hit the end. - Doubly (middle): each node has two arrows — a forward
nextand a backwardprev. That lets you walk in either direction and delete a node you're already holding in one step. - Circular (bottom): the last node's arrow loops back to the first node instead of pointing at nothing — so walking forward never ends. Useful for round-robin turns.
In short: A node = a value + one or more arrows to other nodes. The shape of a list is just which arrows each node carries and whether the tail loops home.
pythonclass ListNode: # singly (the classic interview problems standard)
def __init__(self, val=0, nxt=None):
self.val, self.next = val, nxt
class DListNode: # doubly
def __init__(self, val=0):
self.val, self.prev, self.next = val, None, None
# build a list from a Python list, for testing
def build(vals):
dummy = tail = ListNode()
for v in vals:
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
Before you can practise any pattern you need a way to make a linked list and a way to read it back as an ordinary Python list so you can eyeball the result. These two helpers do exactly that, and the two class lines define what one node looks like.
- ListNode is one node of a singly list: it stores a value
valand a pointernextto the following node (which starts asNone— meaning "nothing after me yet"). DListNode is the doubly version — it also keeps aprevpointer backward. build(vals)turns a normal list like[1,2,3]into a chain. It starts with a throwaway dummy node and atailthat always points at the last real node. For each value it hangs a new node offtail.next, then movestailonto it — growing the chain one link at a time.- It returns
dummy.next— the real first node — because the dummy was only scaffolding. to_list(head)walks the chain the other way: start athead, append eachhead.val, then step forward withhead = head.nextuntilheadbecomesNone. Thatwhile head:loop is the fundamental way you traverse any linked list.
What the output means: to_list(build([1,2,3])) gives back [1, 2, 3] — proof the chain was built and walked correctly.
Try this: Trace build([9]) by hand: dummy → attach node 9 → return dummy.next, which is the node holding 9. One node, no arrows needed yet.
2 · The dummy-head trick essential
The single most useful linked-list idea. A dummy (sentinel) node sits before the real head, so inserting/deleting the first element is no longer a special case — you always have a "previous" node to point from. Return dummy.next at the end.
This shows the single most useful trick in linked-list code: put an extra fake node (the dummy or sentinel) in front of the real first node. It never holds real data — it exists only so that every real node, even the first, has something before it.
- The amber box on the left is the dummy. The note under it — never holds data — is the whole point: it's a placeholder, not part of your answer.
- The arrows are
nextpointers:dummy → head → …. Because the dummy points at the real head, the head now has a "previous" node too. - Why it matters: normally deleting or inserting the first node is a special case (there's no node before it to rewire). With a dummy in front, the first node stops being special — one code path handles head, middle, and tail alike.
- The blue note on the right — return dummy.next at the end — is how you finish: skip past the fake node and hand back the real list.
In short: Dummy head = a free "node before the first node" so you never have to write a special if this is the head branch.
Setup to run this snippet
class _Any:
'''stands in for any undefined demo value; supports call/attr/index/
iteration and basic arithmetic (as 0.7) so demo snippets run.'''
def __call__(self, *a, **k): return _Any()
def __getattr__(self, k): return _Any()
def __getitem__(self, k): return _Any()
def __iter__(self): return iter([])
def __len__(self): return 0
def __contains__(self, o): return True
def __enter__(self, *a): return _Any()
def __exit__(self, *a): return False
def __float__(self): return 0.7
def __int__(self): return 1
def __lt__(self, o): return True
def __gt__(self, o): return False
def __le__(self, o): return True
def __ge__(self, o): return False
def __add__(self, o): return o
def __radd__(self, o): return o
def __bool__(self): return True
def __repr__(self): return 'demo'
def __str__(self): return 'demo'
def ListNode(*a, **k): # demo stub
return _Any()pythondef remove_elements(head, target):
dummy = ListNode(0, head) # sentinel before head
prev = dummy
while prev.next:
if prev.next.val == target:
prev.next = prev.next.next # splice it out — no head special-case
else:
prev = prev.next
return dummy.next # O(n) time, O(1) space
This deletes every node whose value equals target — even if that's the very first node — without any special "is it the head?" branch. That clean uniformity is exactly what the dummy head buys you.
dummy = ListNode(0, head)creates the fake node whosenextalready points at the realhead.prev = dummystarts our "node before the one we're inspecting" right on the dummy.while prev.next:keeps going as long as there's a node afterprevto look at. We always inspectprev.next, neverprevitself.- If
prev.next.val == target, we splice it out:prev.next = prev.next.nextmakesprevskip over the doomed node and point straight at the one after it. The removed node is now unreachable — gone. - Otherwise (
else) the current node stays, so we advanceprev = prev.next. Note we only advance when we don't delete, so consecutive matches all get removed. - Finally
return dummy.next— the real head, which may itself have changed if the original first node was deleted.
What the output means: For list 1→2→6→3→6 with target=6, you get 1→2→3. Runs in O(n) time, O(1) extra space.
Try this: Set target to the value of the first node. Because of the dummy, deleting the head takes the same line as deleting any other node — no special case.
3 · Reversal — whole, sublist, K-group advanced
Reversal is the linked-list skill. The core move: for each node, remember its next, flip its pointer backward, advance. Everything else builds on it.
prev, cur, nxt: save nxt = cur.next, set cur.next = prev, then slide all three forward. One O(n) pass, O(1) space.
Reversing a list means making every arrow point backward instead of forward. You don't move the boxes — you only re-aim the arrows. This diagram freezes the trick mid-flip, with three named markers you keep as you walk the list.
- The boxes 1 2 3 4 are the nodes. Watch the arrows: the ones on the left have already been flipped to point back (toward the front); the faint arrow near the front shows the original forward direction being undone.
- prev (label under node 2) = the last node we already reversed. cur (under node 3) = the node we're flipping right now. nxt (under node 4) = the next node, which we must save first.
- The order of moves per node: (1)
nxt = cur.nextso we don't lose the rest of the list, (2)cur.next = prevflips this node's arrow backward, (3) slide all three markers one step forward (prev, cur = cur, nxt). - Why save
nxtfirst? The instant you docur.next = prevyou've overwritten the only pointer to the rest of the list — saving it first is the golden rule.
In short: Reversal = walk once with three fingers (prev, cur, nxt); at each node flip its arrow to point at prev, then shuffle all three fingers forward. O(n) time, O(1) space.
Setup to run this snippet
class _Any:
'''stands in for any undefined demo value; supports call/attr/index/
iteration and basic arithmetic (as 0.7) so demo snippets run.'''
def __call__(self, *a, **k): return _Any()
def __getattr__(self, k): return _Any()
def __getitem__(self, k): return _Any()
def __iter__(self): return iter([])
def __len__(self): return 0
def __contains__(self, o): return True
def __enter__(self, *a): return _Any()
def __exit__(self, *a): return False
def __float__(self): return 0.7
def __int__(self): return 1
def __lt__(self, o): return True
def __gt__(self, o): return False
def __le__(self, o): return True
def __ge__(self, o): return False
def __add__(self, o): return o
def __radd__(self, o): return o
def __bool__(self): return True
def __repr__(self): return 'demo'
def __str__(self): return 'demo'
def ListNode(*a, **k): # demo stub
return _Any()python# (a) Reverse the whole list (classic)
def reverse(head):
prev = None
while head:
head.next, prev, head = prev, head, head.next # flip & advance
return prev
# (b) Reverse between positions left..right (classic)
def reverse_between(head, left, right):
dummy = ListNode(0, head); prev = dummy
for _ in range(left - 1): prev = prev.next # node before the segment
cur = prev.next
for _ in range(right - left): # head-insert the next node
nxt = cur.next
cur.next = nxt.next
nxt.next = prev.next
prev.next = nxt
return dummy.next
# (c) Reverse in groups of k (classic) — reverse each block of k
def reverse_k_group(head, k):
def kth(node, k):
while node and k: node = node.next; k -= 1
return node
dummy = ListNode(0, head); group_prev = dummy
while True:
kth_node = kth(group_prev, k)
if not kth_node: break # fewer than k left → stop
group_next = kth_node.next
prev, cur = group_next, group_prev.next
while cur != group_next: # reverse this block
cur.next, prev, cur = prev, cur, cur.next
group_prev.next, group_prev = kth_node, group_prev.next
return dummy.next
Three reversals of growing difficulty, all built on the same flip-the-arrow move: reverse the whole list, reverse only a middle section, and reverse in fixed-size groups of k. If you understand part (a), the others are just careful bookkeeping.
- (a) reverse —
prevstarts atNone(the new tail's "next"). The one dense linehead.next, prev, head = prev, head, head.nextdoes all three steps at once: point this node back atprev, moveprevonto this node, and stepheadforward. Python evaluates the right side first, so the oldhead.nextis safely captured before it's overwritten. Returnprev— the new head. - (b) reverse_between — a dummy head plus a first loop to walk
prevto the node just before positionleft. Then the head-insert trick: repeatedly unhook the node aftercurand splice it to the front of the segment, so the segment flips in place while the rest of the list stays attached. - (c) reverse_k_group — the helper
kthjumps ahead k nodes to check a full group exists;if not kth_node: breakstops when fewer than k nodes are left (that tail stays as-is). It reverses each block just like part (a), then re-links the reversed block back into the chain withgroup_prev.
What the output means: reverse(1→2→3) returns 3→2→1. reverse_between(1→2→3→4→5, 2, 4) gives 1→4→3→2→5. reverse_k_group(1→2→3→4→5, 2) gives 2→1→4→3→5.
Try this: Say the multi-assignment line out loud as three separate statements (save next, flip arrow, advance). Seeing that they happen together is what makes reversal click.
4 · Two-pointer trio: middle, nth-from-end, cycle start advanced
Three classics, all one pass, all built on running two pointers at different speeds or offsets — no length precomputation needed.
slow one step and fast two; when fast reaches the end, slow sits at the middle. Offset the pointers by n instead and you get "nth from the end" in one pass.
This is the two-pointer / fast-slow technique — the trick behind half the "find the …" list problems. You run two walkers through the same list at different speeds; where they end up tells you something you'd otherwise need a second pass to compute.
- The boxes 1..5 are the nodes; the highlighted middle box (3) is the answer we're after. There are no drawn arrows here — the movement is in the two labels below.
- slow (green, under box 3) moves one node per step. fast (blue, under box 5) moves two nodes per step — twice as fast.
- Because fast covers ground twice as quickly, by the time
fastreaches the end,slowhas only reached the middle. So the middle falls out in a single pass — no counting the length first. - The same idea with an offset (start one pointer n steps ahead instead of moving it faster) gives you "the nth node from the end" in one pass, shown in the code below.
In short: Two pointers, different speeds: fast hits the end exactly when slow hits the middle. Change speed to an offset and you get nth-from-the-end. One pass, O(1) space.
Setup to run this snippet
class _Any:
'''stands in for any undefined demo value; supports call/attr/index/
iteration and basic arithmetic (as 0.7) so demo snippets run.'''
def __call__(self, *a, **k): return _Any()
def __getattr__(self, k): return _Any()
def __getitem__(self, k): return _Any()
def __iter__(self): return iter([])
def __len__(self): return 0
def __contains__(self, o): return True
def __enter__(self, *a): return _Any()
def __exit__(self, *a): return False
def __float__(self): return 0.7
def __int__(self): return 1
def __lt__(self, o): return True
def __gt__(self, o): return False
def __le__(self, o): return True
def __ge__(self, o): return False
def __add__(self, o): return o
def __radd__(self, o): return o
def __bool__(self): return True
def __repr__(self): return 'demo'
def __str__(self): return 'demo'
def ListNode(*a, **k): # demo stub
return _Any()pythondef middle(head):
slow = fast = head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
return slow
def remove_nth_from_end(head, n):
dummy = ListNode(0, head)
fast = slow = dummy
for _ in range(n): fast = fast.next # open a gap of n
while fast.next: # move both until fast is last
fast, slow = fast.next, slow.next
slow.next = slow.next.next # slow is just before the target
return dummy.next
def detect_cycle_start(head): # Floyd phase 2
slow = fast = head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
if slow is fast: # meeting point
p = head
while p is not slow: # both advance 1 → meet at cycle start
p, slow = p.next, slow.next
return p
return None
The three fast-slow classics in one place: find the middle, remove the nth node from the end in one pass, and find where a loop begins. All three run two pointers and never precompute the list's length.
- middle —
slowandfastboth start at the head; each loop step doesslow = slow.nextandfast = fast.next.next. The guardwhile fast and fast.next:stops safely whether the list has an odd or even number of nodes. When it stops,slowis the middle. - remove_nth_from_end — first open a gap of n: move
fastahead n nodes. Now advance both untilfastis the last node — at that momentslowsits just before the target.slow.next = slow.next.nextsplices the target out. The dummy head means even "remove the first node" needs no special case. - detect_cycle_start (Floyd's algorithm) has two phases. Phase 1: fast and slow race; if the list loops, the fast one laps the slow one and they collide (
slow is fast). Phase 2: put a fresh pointerpat the head and advancepandslowtogether one step at a time — they meet exactly at the node where the loop starts. Iffastever hitsNone, there's no loop, so returnNone.
What the output means: On 1→2→3→4→5: middle returns node 3; remove_nth_from_end(...,2) gives 1→2→3→5; detect_cycle_start returns the loop's entry node, or None when the list is straight.
Try this: For middle, draw 4 nodes instead of 5 and step the two pointers by hand — you'll see why while fast and fast.next is the exact stop condition.
5 · Merge & add — two sorted lists, add two numbers intermediate → advanced
Both stitch a new list together node-by-node behind a dummy head — the bread-and-butter of list construction.
Setup to run this snippet
class _Any:
'''stands in for any undefined demo value; supports call/attr/index/
iteration and basic arithmetic (as 0.7) so demo snippets run.'''
def __call__(self, *a, **k): return _Any()
def __getattr__(self, k): return _Any()
def __getitem__(self, k): return _Any()
def __iter__(self): return iter([])
def __len__(self): return 0
def __contains__(self, o): return True
def __enter__(self, *a): return _Any()
def __exit__(self, *a): return False
def __float__(self): return 0.7
def __int__(self): return 1
def __lt__(self, o): return True
def __gt__(self, o): return False
def __le__(self, o): return True
def __ge__(self, o): return False
def __add__(self, o): return o
def __radd__(self, o): return o
def __bool__(self): return True
def __repr__(self): return 'demo'
def __str__(self): return 'demo'
def ListNode(*a, **k): # demo stub
return _Any()python# (a) Merge two sorted lists (classic)
def merge_two(a, b):
dummy = tail = ListNode()
while a and b:
if a.val <= b.val: tail.next, a = a, a.next
else: tail.next, b = b, b.next
tail = tail.next
tail.next = a or b # attach the remaining tail
return dummy.next
# (b) Add two numbers stored in reverse (classic)
def add_two_numbers(a, b):
dummy = tail = ListNode(); carry = 0
while a or b or carry:
s = carry + (a.val if a else 0) + (b.val if b else 0)
carry, digit = divmod(s, 10)
tail.next = ListNode(digit); tail = tail.next
a = a.next if a else None
b = b.next if b else None
return dummy.next
Both of these build a brand-new list node-by-node behind a dummy tail — the standard way to assemble a list when you don't know its final length up front.
- The shared setup
dummy = tail = ListNode()makes both names point at the same fake node.tailalways marks the end of the list we're growing;dummystays put so we can returndummy.nextat the finish. - merge_two — while both input lists have nodes, compare their front values and hook the smaller one onto
tail.next, then advance past it. After the loop one list may still have nodes;tail.next = a or battaches the remaining tail in one shot (a or bgives whichever isn'tNone). - add_two_numbers — the digits are stored reversed, so the ones digit comes first, which is perfect for grade-school addition. Each step adds the two current digits plus a
carry;divmod(s, 10)splits the sum into the newcarryand thedigitto store. - The loop condition
while a or b or carryis the subtle part: it keeps going until both lists are exhausted and there's no leftover carry — so99 + 1 = 100correctly grows an extra digit.
What the output means: merge_two(1→3, 2→4) → 1→2→3→4. add_two_numbers of 2→4→3 (=342) and 5→6→4 (=465) → 7→0→8 (=807).
Try this: Remove the or carry from the second loop and add 9→9 to 1. You'll drop the leading 1 of 100 — that's why the carry belongs in the condition.
merge_two is the merge step of merge sort (D6) on a list; extend it with a heap to merge K lists (D7 §3). Cousins: sort-list (merge sort on a linked list), partition-list.6 · Palindrome & reorder — combine the tools expert expert
These two composite problems show why the earlier patterns matter: both = find middle + reverse second half, then compare or interleave. Classic Meta/Amazon questions.
This shows how two earlier tricks combine to test whether a list reads the same forwards and backwards (like 1→2→2→1) using no extra memory — no copying into an array.
- The left boxes are the first half; the green boxes on the right are the second half after it's been reversed. The | middle | label marks where the list was split in two.
- The two arrows meeting in the middle show the comparison walking inward: one finger steps forward through the left half, the other steps through the reversed right half, checking values are equal at each step.
- How the halves are found: use fast/slow (from §4) to reach the middle, then reverse the second half in place (from §3). It reuses the exact primitives you already learned.
- The green caption spells out the verdict — if left half == reversed right half all the way in, it's a palindrome.
In short: Palindrome = find middle (fast/slow) + reverse the back half + walk both halves inward comparing. O(n) time, O(1) space.
pythondef is_palindrome(head):
slow = fast = head
while fast and fast.next: # slow → middle
slow, fast = slow.next, fast.next.next
prev = None # reverse second half
while slow:
slow.next, prev, slow = prev, slow, slow.next
while prev: # compare halves
if prev.val != head.val: return False
prev, head = prev.next, head.next
return True
def reorder(head): # L0→Ln→L1→Ln-1→…
if not head or not head.next: return
slow = fast = head # 1. find middle
while fast.next and fast.next.next:
slow, fast = slow.next, fast.next.next
second = slow.next; slow.next = None # 2. split
prev = None # 3. reverse second half
while second:
second.next, prev, second = prev, second, second.next
first, second = head, prev # 4. interleave
while second:
first.next, first = second, first.next
second.next, second = first, second.next
Two "composite" problems that prove the earlier patterns were worth learning: each one is just find the middle + reverse the second half, then either compare (palindrome) or interleave (reorder).
- is_palindrome, step 1: the fast/slow loop walks
slowto the middle whilefastruns to the end. - Step 2:
while slow:reverses the second half in place using the same three-way assignment as the whole-list reversal —prevends up as the head of the reversed back half. - Step 3: walk
prev(reversed back half) andhead(front half) together; if any pair of values differs, returnFalse. If they all match, it's a palindrome. - reorder weaves the list into
L0→Ln→L1→Ln-1→…. It does the same find-middle + reverse-second-half, then interleaves: alternately pull one node from the front half and one from the reversed back half, splicing them together. The guardif not head or not head.next: returnhandles empty or single-node lists so the pointer math never crashes.
What the output means: is_palindrome(1→2→2→1) → True; is_palindrome(1→2→3) → False. reorder(1→2→3→4) rewrites the list in place to 1→4→2→3.
Try this: Notice both functions start with the identical fast/slow middle-finder. Composing small proven moves — not memorising a new algorithm — is the real lesson.
7 · The hard ones — intersection & copy-with-random-pointer expert expert
Two problems that look scary but fall to clean tricks.
Setup to run this snippet
class _Any:
'''stands in for any undefined demo value; supports call/attr/index/
iteration and basic arithmetic (as 0.7) so demo snippets run.'''
def __call__(self, *a, **k): return _Any()
def __getattr__(self, k): return _Any()
def __getitem__(self, k): return _Any()
def __iter__(self): return iter([])
def __len__(self): return 0
def __contains__(self, o): return True
def __enter__(self, *a): return _Any()
def __exit__(self, *a): return False
def __float__(self): return 0.7
def __int__(self): return 1
def __lt__(self, o): return True
def __gt__(self, o): return False
def __le__(self, o): return True
def __ge__(self, o): return False
def __add__(self, o): return o
def __radd__(self, o): return o
def __bool__(self): return True
def __repr__(self): return 'demo'
def __str__(self): return 'demo'
def Node(*a, **k): # demo stub
return _Any()python# (a) Intersection (classic) — two pointers that swap heads at the end
def get_intersection(a, b):
p, q = a, b
while p is not q: # each walks lenA+lenB, so they align
p = p.next if p else b
q = q.next if q else a
return p # the shared node, or None
# (b) Copy list with random pointer (classic) — interleave clones, O(1) space
def copy_random_list(head):
if not head: return None
cur = head # 1. weave copy after each original: A→A'→B→B'
while cur:
cur.next = Node(cur.val, cur.next); cur = cur.next.next
cur = head # 2. wire clones' random pointers
while cur:
if cur.random: cur.next.random = cur.random.next
cur = cur.next.next
# 3. unweave the two lists
cur = head; copy_head = head.next
while cur:
clone = cur.next
cur.next = clone.next
clone.next = clone.next.next if clone.next else None
cur = cur.next
return copy_head
The two "scary" ones. Both look like they need extra data structures but fall to a neat pointer trick: find where two lists merge, and deep-copy a list whose nodes also have a random pointer — both in O(1) extra space.
- get_intersection — start
pon list A andqon list B. Each step, when a pointer runs off the end it jumps to the other list's head. That way each walks lenA+lenB nodes, so they line up and meet at the shared node (or both reachNonetogether if the lists never join). - copy_random_list, pass 1 — weave a copy after each original node: the chain becomes
A→A'→B→B'→…. Now each clone sits right behind its original, so you can find a clone without any lookup table. - Pass 2 — wire the clones' random pointers: for each original, its clone's random should point at the clone of the original's random target, which is exactly
cur.random.next(the node right after the original's random). - Pass 3 — unweave the two interleaved chains back into the original list and a separate cloned list, and return the clone's head. Three linear passes, no hash map.
What the output means: get_intersection returns the first shared node of the two lists, or None. copy_random_list returns a fully independent deep copy — editing it never touches the original.
Try this: For the intersection trick, take two short lists that share a tail and step p and q by hand. Watching each cover lenA+lenB steps is the "aha" that explains why they align without any length math.
p walks list A then list B; q walks B then A. Both travel lenA + lenB steps, so they arrive at the intersection (or None) simultaneously — no length math, O(1) space. The copy-random interleave is the standard way to clone without a hash map.Linked-list pattern cheat-sheet expert
| Problem says… | Reach for | Cost |
|---|---|---|
| insert/delete near the head | dummy head sentinel | O(1) edge-case-free |
| reverse / reverse part / K-group | pointer-flip (prev/cur/nxt) | O(n), O(1) |
| middle / nth-from-end | fast & slow / offset pointers | O(n) one pass |
| cycle exists / where it starts | Floyd's two-phase | O(n), O(1) |
| merge / add / partition | build behind a dummy tail | O(n) |
| palindrome / reorder | middle + reverse-half + compare | O(n), O(1) |
| two lists intersect | swap-heads two pointers | O(n+m), O(1) |
| deep copy with extra pointers | interleave clones | O(n), O(1) |
Checkpoint expert
- Draw the pointer moves before coding, and always use a dummy head for head-adjacent edits.
- Reverse a list whole, between two positions, and in K-groups from memory.
- Apply fast/slow (or offset) pointers for middle, nth-from-end, and cycle start.
- Merge/add lists behind a dummy tail; solve palindrome & reorder by composing middle + reverse.
- Explain the intersection swap-heads trick and the copy-random interleave.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Everything else in linked lists builds on being able to construct one and walk it — the pointer bookkeeping is the whole point.
Your task: Define a singly linked-list node, build a list from a Python list, and traverse it back into a Python list, in O(n).
Requirements:
- A node holds a value and a
nextreference - Build the list by linking nodes in order
- Traverse by following
nextuntilNone O(n)- Round-trips a Python list to a linked list and back unchanged
💡 Hint: Keep a reference to the head as you build; traversal is just a while node: loop advancing node = node.next.
Show solution
class Node:
def __init__(self, val, nxt=None): self.val = val; self.next = nxt
def build(values): # O(n)
head = None
for v in reversed(values):
head = Node(v, head)
return head
def to_list(head): # O(n)
out = []
while head:
out.append(head.val); head = head.next
return out
print(to_list(build([1, 2, 3]))) # [1, 2, 3]
Building from the tail backward lets each new node point at the current head — no tail pointer needed.
Context: Reversing a singly linked list (classic) is the single most-asked linked-list operation — the three-pointer walk is worth memorising.
Your task: Reverse a singly linked list iteratively in O(n) time and O(1) space using the three-pointer technique.
Requirements:
- Walk the list once, flipping each node's
nextpointer - Track previous, current, and next-node references
O(n)time,O(1)space (no new list)- Return the new head (the old tail)
💡 Hint: Before rewiring current.next, stash the next node so you don't lose the rest of the list; prev ends up as the new head.
Show solution
class Node:
def __init__(self, val, nxt=None): self.val = val; self.next = nxt
def reverse(head): # O(n) time, O(1) space
prev = None
while head:
nxt = head.next # save next
head.next = prev # flip the pointer
prev = head # advance prev
head = nxt # advance head
return prev # new head
# build 1->2->3, reverse, read back
h = Node(1, Node(2, Node(3)))
r = reverse(h)
out = []
while r: out.append(r.val); r = r.next
print(out) # [3, 2, 1]
Each iteration flips one link and slides all three pointers forward — a single pass with no extra memory.
Context: The slow/fast two-pointer trio finds the middle and the n-th-from-end in a single pass — the building blocks for many list algorithms.
Your task: Using two pointers: find the middle node with slow/fast, and the n-th node from the end with a gap of n — both O(n) in a single pass.
Requirements:
- Slow advances one step, fast two steps → slow lands on the middle
- For nth-from-end, advance a lead pointer n steps first, then move both together
- When the lead reaches the end, the trailing pointer is at the target
- Single pass,
O(n)time,O(1)space
💡 Hint: The fast pointer reaching the end at double speed puts slow at the midpoint; the fixed n-node gap is what pins down the nth-from-end node.
Show solution
class Node:
def __init__(self, val, nxt=None): self.val = val; self.next = nxt
def middle(head): # slow moves 1, fast moves 2
slow = fast = head
while fast and fast.next:
slow = slow.next; fast = fast.next.next
return slow.val
def nth_from_end(head, n): # lead pointer starts n ahead
lead = head
for _ in range(n):
lead = lead.next
trail = head
while lead:
lead = lead.next; trail = trail.next
return trail.val
h = Node(1, Node(2, Node(3, Node(4, Node(5)))))
print(middle(h)) # 3
print(nth_from_end(h, 2)) # 4
A fixed gap or a 2:1 speed ratio locates a relative position in one pass without knowing the length up front.
Context: Merging two sorted lists (classic) is where the dummy-head trick earns its keep by erasing the special case for the first node.
Your task: Merge two sorted linked lists into one sorted list using a dummy head to avoid special-casing the first node, in O(n+m).
Requirements:
- Use a dummy head node so appending is uniform from the start
- Splice the smaller current node each step
- Attach the remaining tail once one list is exhausted
O(n+m)- Return
dummy.nextas the merged head
💡 Hint: The dummy node gives you a stable place to build from; a tail pointer that always points at the last spliced node keeps the append O(1).
Show solution
class Node:
def __init__(self, val, nxt=None): self.val = val; self.next = nxt
def merge(a, b): # O(n + m)
dummy = tail = Node(0) # dummy head removes edge cases
while a and b:
if a.val <= b.val:
tail.next = a; a = a.next
else:
tail.next = b; b = b.next
tail = tail.next
tail.next = a or b # attach the remainder
return dummy.next
a = Node(1, Node(3, Node(5)))
b = Node(2, Node(4, Node(6)))
out, m = [], merge(a, b)
while m: out.append(m.val); m = m.next
print(out) # [1, 2, 3, 4, 5, 6]
The dummy node means we never branch on "is this the first output node?" — the tail pointer just keeps appending.
Context: Reverse-in-k-groups (classic) is a hard, high-signal problem that combines reversal with careful re-linking of group boundaries.
Your task: Reverse the list in groups of k, leaving a trailing partial group as-is, in O(n) time and O(1) space.
Requirements:
- Reverse each full run of k nodes
- A final group of fewer than k nodes is left untouched
- Re-link each reversed group to the previous and next segments correctly
O(n)time,O(1)space- Check there are k nodes ahead before reversing a group
💡 Hint: Count k nodes forward before committing to a reversal; the tail of one reversed group must connect to the head of the next.
Show solution
class Node:
def __init__(self, val, nxt=None): self.val = val; self.next = nxt
def reverse_k_group(head, k): # O(n) time, O(1) space
# check there are at least k nodes left
node, count = head, 0
while node and count < k:
node = node.next; count += 1
if count < k:
return head # fewer than k -> leave as-is
# reverse this group; `node` is head of the next group
prev = reverse_k_group(node, k)
cur = head
for _ in range(k):
nxt = cur.next
cur.next = prev
prev = cur; cur = nxt
return prev # new head of this group
h = Node(1, Node(2, Node(3, Node(4, Node(5)))))
r = reverse_k_group(h, 2); out = []
while r: out.append(r.val); r = r.next
print(out) # [2, 1, 4, 3, 5]
Count k ahead, recurse on the tail first, then reverse the current block and stitch it onto the already-reversed tail.
Context: Finding the cycle's start node (classic) is phase two of Floyd's algorithm — the elegant part most people don't know is that a second walk pinpoints the entry.
Your task: Detect a cycle and return the node where it begins (not merely whether one exists), in O(n) time and O(1) space, using Floyd's algorithm.
Requirements:
- First find a meeting point with slow/fast pointers
- Then move one pointer to the head and advance both one step at a time
- They meet at the cycle's entry node
- Return that node (or
Noneif there's no cycle) O(n)time,O(1)space
💡 Hint: After the tortoise-and-hare meet, resetting one pointer to the head and stepping both at equal speed makes them collide exactly at the loop's start.
Show solution
class Node:
def __init__(self, val): self.val = val; self.next = None
def cycle_start(head): # O(n) time, O(1) space
slow = fast = head
while fast and fast.next: # phase 1: find a meeting point
slow = slow.next; fast = fast.next.next
if slow is fast:
break
else:
return None # no cycle
slow = head # phase 2: walk both at speed 1
while slow is not fast: # they meet at the cycle entrance
slow = slow.next; fast = fast.next
return slow
a, b, c, d = Node(1), Node(2), Node(3), Node(4)
a.next, b.next, c.next, d.next = b, c, d, b # tail loops back to b
print(cycle_start(a).val) # 2
The distance from head to the entrance equals the distance from the meeting point to the entrance (a modular-arithmetic identity), so resetting one pointer to head and stepping both by one lands them exactly at the loop start.
Knowledge check check yourself
What problem does the dummy-head (sentinel) node solve in linked-list code?
Show answer
dummy.next at the end.How do you find the middle of a linked list (or the start of a cycle) in one pass with two pointers?