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
Here's a bug I once spent an afternoon on. A colleague built a task scheduler on a "linked list" he'd cobbled together, and it worked perfectly — until the day he needed to delete a task and its predecessor. His nodes only pointed forward, so to find the node before the one he was holding, he had to re-walk the entire list from the head every single time. What should have been an O(1) delete had quietly become O(n), and under load the scheduler crawled. The fix wasn't cleverer code — it was the right shape of list. That afternoon is why this section exists: before you touch a single pattern, you need to know that a linked list isn't one thing, it's a family, and picking the wrong member is a whole class of pain.
You already met the node in D2 — a little box holding a value and one or more pointers to other boxes. What changes from list to list is purely which arrows each node carries and whether the last node points at nothing or loops home. Think of it like train couplings. A singly linked list is a train where each car is coupled only to the car behind it: you can walk from the engine to the caboose, but never backward, and to reach a car you always start at the front. A doubly linked list couples each car to both neighbours — now you can walk either direction, and if you're standing in a car you can unhook it and rejoin its two neighbours instantly, because you can see both of them. A circular list couples the caboose back to the engine, so the train is a loop with no end — walk forward long enough and you're back where you started.
The common mistake beginners make is reaching for the fanciest option "to be safe" — a doubly linked list everywhere. But every extra pointer is memory you pay for on every node and a second link you must keep correct on every edit; get a prev out of sync with its next and you have a list that walks fine forward and corrupts going backward. Match the shape to the job: singly when you only ever move forward (most interview problems, and the standard ListNode below); doubly when you need O(1) deletion of a node you're already holding or backward traversal (this is exactly why an LRU cache uses one, as you saw in D2); circular for round-robin turns and ring buffers where "after the last comes the first" is the natural rule. Formally: a linked list is a sequence of nodes where each node stores data plus a reference to the next node (and, in a doubly linked list, the previous); the list is identified by a reference to its head, and a null next marks the tail — unless it's circular, in which case the tail's next is the head. The diagram lays all three side by side.
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
Watch what happens the moment "delete this node" includes the possibility of deleting the first one. Your clean little loop — "point the previous node past the one I'm removing" — hits a wall, because the first node has no previous node. So you write an if: "if it's the head, do this special thing; otherwise do the normal thing." Then insertion at the front needs its own special case too. Then a second edge appears, and a third, and your ten-line function is now thirty lines of branching that you have to get right under interview pressure, at 2am, in code review. Nearly every ugly linked-list bug traces back to that one asymmetry: the head has no predecessor.
The dummy-head trick makes the asymmetry vanish. Picture a train again: normally you can't uncouple the engine by "rejoining its neighbours," because it has no car in front of it. So you bolt on a fake lead car — a placeholder that carries no cargo — purely so the real engine now has something ahead of it. Suddenly the engine is just an ordinary car: to remove it, you re-couple the fake car to the second car, exactly the same move you'd use anywhere in the middle. The fake car (the dummy or sentinel) never holds real data and never appears in your answer; it exists only to give the first real node a predecessor so that one code path handles head, middle, and tail identically.
The mechanics are a two-line habit worth burning into muscle memory: at the start, create dummy = ListNode(0, head) so the sentinel points at the current head; do all your work using the dummy as the node "before the front"; then at the very end return dummy.next, not head — because the real head may have changed (you might have deleted or replaced it), and dummy.next always points at whatever the true first node is now. That last part is the common mistake: people build correctly with a dummy and then absent-mindedly return head, handing back a node that's no longer the front. The dummy costs you one throwaway node — O(1) space — and buys you the deletion of an entire category of edge-case branches. It shows up in almost every pattern that follows: remove-nth, merge, partition, remove-duplicates, and the classic below. The diagram shows the sentinel sitting in front of the real list.
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
If there's one linked-list operation an interviewer will ask you to do live, it's this one — and it's where more candidates lose the pointer than anywhere else. The trap is seductive in its simplicity: "reverse the list" sounds like it should be a one-liner, so people dive in and write node.next = prev as the very first move. And in that instant they've cut the rope they were climbing — the pointer to the rest of the list is gone, unrecoverable, and the loop walks off into None. Reversal isn't hard because the idea is hard; it's hard because the order of three tiny steps is unforgiving.
Here's the analogy that makes it stick. Imagine you're crossing a river on stepping stones, and your job is to flip each stone to face backward as you go. If you flip the stone you're standing on before noting where the next one is, you've stranded yourself — you flipped away your own directions forward. So the discipline is: first look ahead and memorise the next stone (nxt = cur.next), then flip the current stone to face back (cur.next = prev), then step forward onto the stone you memorised. You carry exactly three fingers through the list — prev (the last stone you already flipped), cur (the stone under you now), and nxt (the safety rope to the rest of the list). Miss the order and you fall in.
The common mistake, then, is overwriting cur.next before you've saved it — and once you name that mistake out loud, the fix is obvious and permanent: save-first is golden rule #3 from the box at the top of this page. Note the cost, because it's what makes reversal beloved: a single walk down the list flips every arrow, so it's O(n) time, and you only ever hold three pointers no matter how long the list is, so it's O(1) space — you reverse a million-node list without allocating a second list. Formally, in-place reversal re-points each node's successor link to its predecessor in one pass, and the old tail becomes the new head (that's why the whole-list version returns prev, not head). Everything else in this section — reversing just a middle segment (LC 92), or reversing in fixed blocks of K (LC 25) — is this same three-finger move wrapped in extra bookkeeping about where the reversal starts and stops. Get part (a) into your fingers and the rest is careful stitching. The diagram freezes the flip mid-motion.
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
Try to find the middle of a linked list and your first instinct will be the honest, wasteful one: walk the whole list to count the nodes, divide by two, then walk again to that index. It works, it's correct — and it's two passes, and it forces you to store the length. On an array that's fine; on a linked list, where you can't jump to index n/2 anyway, it feels clumsy, and interviewers use "can you do it in one pass?" precisely to see whether you know the trick. There is a trick, and it's one of the most elegant ideas in this whole track.
Picture two runners on a circular track, one jogging and one sprinting at exactly double speed. When the sprinter has covered the full lap, the jogger — moving half as fast — has covered exactly half of it. That's the entire insight: run a slow pointer one node per step and a fast pointer two nodes per step, and the moment fast reaches the end, slow is sitting on the middle. No counting, no second pass, no stored length — the geometry does the arithmetic for you. This "fast and slow pointers" idea (also called the tortoise-and-hare, which you met for cycle detection in D2) is the parent pattern; the trio in this section are three of its children.
The same two-pointer chassis, retuned, solves two more classics. For nth-from-the-end, don't change the speeds — change the offset: send one pointer n nodes ahead, then walk both at the same pace; when the leader hits the end, the trailer is parked exactly n nodes from the tail, again in one pass. For finding where a cycle begins, you run tortoise-and-hare until they collide inside the loop, then do a second short walk from the head — Floyd's two-phase algorithm, whose "why does the second walk land on the entrance?" is a favourite follow-up (the distance-equality reason is in the note after the lab). The common mistake across all three is the stop condition: writing while fast: instead of while fast and fast.next: will crash with an AttributeError on even-length lists, because fast.next.next dereferences None. Guard both fast and fast.next before you take the double step, every time. All three are O(n) time and O(1) space. The diagram shows the fast/slow pair converging on the middle.
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
Up to now every pattern has rearranged an existing list. This section is about the opposite skill: growing a brand-new list one node at a time when you don't yet know how long it will be. That "don't know the length" part is exactly what makes it awkward. With an array you'd pre-size a buffer or just append and let it grow; with a linked list you're holding a chain by its head, and to add to the end you'd normally have to walk all the way to the tail each time — turning a simple build into a hidden O(n²). The fix is a pattern you already have in hand.
Think of it like laying railway track ahead of a moving train. You keep a tail pointer that always sits on the last piece of track you've laid; each new node clicks onto tail.next, and then you slide tail forward onto it. Because you never search for the end — you always know where it is — every attach is O(1), and building an n-node result is a clean O(n). And where does the track start? Behind a dummy head (section 2 again): dummy = tail = ListNode() gives you a stable anchor to hang the first real node off, so even "the very first output node" needs no special case. This dummy-tail construction is the bread-and-butter of list building, and once you see it in merge_two you'll recognise it everywhere.
The two problems here share that skeleton but differ in the interesting details. Merging two sorted lists (LC 21) is the merge step of merge sort you met in D6, done on links instead of array slots: repeatedly bite off the smaller of the two front nodes, and when one list runs dry, tail.next = a or b attaches the entire remaining tail in a single move — no need to copy it node by node. Adding two numbers (LC 2) stores digits in reverse order, which is a gift, not a nuisance: the ones digit comes first, so you add left-to-right exactly like grade-school arithmetic, propagating a carry. The classic mistake there is the loop condition — people write while a and b and lose the final carry, so 99 + 1 produces 00 instead of 100. The correct guard is while a or b or carry: keep going until both lists are exhausted and no carry is left over. The lab builds both behind the same dummy tail.
tail pointer that always marks the end keeps every append O(1), so the whole merge is O(n+m).
Read it top-to-bottom: the two sorted inputs are at the top, the single merged output grows along the bottom row.
- The top two rows (
aandb) are the two already-sorted input lists. Each step you look only at their front nodes and take the smaller one. - The bottom row is the result being built. The amber
dummybox on the left is a throwaway anchor; the green boxes after it are the real merged nodes, in sorted order. - The green arrows are the
nextpointers being laid down one at a time. The note on the right is the payoff: atailpointer always parks on the last node, so you never walk to find the end — each attach is O(1). - The bottom line is the finishing trick: once one input is empty,
tail.next = a or bhooks on the entire remaining tail in a single move — no node-by-node copying.
In short: Keep a dummy-anchored tail, bite off the smaller front node each step, then attach whatever remains — an O(n+m) merge with no extra list.
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
"Is this list a palindrome?" is where a lot of people quietly cheat — and the interviewer is watching for it. The easy answer is to copy every value into a Python list and check whether it reads the same forwards and backwards. It's correct, it's fast to write, and it earns a raised eyebrow, because it spends O(n) extra memory to sidestep the actual linked-list challenge. The whole point of the question is: can you do it in place, touching only pointers, in O(1) space? And here's the beautiful part — you already know every move required. You just have to see that the problem is a composite of pieces you've already mastered.
Here's the reframe that unlocks it. To check a palindrome without a copy, split the list at its middle and reverse the back half in place, so now you have two half-length chains pointing at each other from the centre; then walk one finger outward-from-the-middle through each half in lockstep, comparing values. If every pair matches, it's a palindrome. Notice what just happened: "find the middle" is the fast/slow trick from section 4, and "reverse the back half" is the three-finger flip from section 3. You didn't learn a new algorithm — you snapped two old ones together. That is the real lesson of this section, and it's the mindset shift that separates people who memorise fifty problems from people who understand the six primitives that generate them.
Reorder (LC 143), which rewrites L0→L1→…→Ln into the woven L0→Ln→L1→Ln-1→…, is the same recipe with a different final step: find middle, reverse the second half, then instead of comparing the halves you interleave them — pull one node from the front, one from the reversed back, one from the front, and so on. Both are staple Meta/Amazon interview questions precisely because they reward composition over memorisation. The common mistake in both is skipping the guard for tiny inputs: an empty or single-node list has no meaningful "second half," and the pointer arithmetic (fast.next.next, slow.next) will crash on it — so both functions bail early with if not head or not head.next: return. Because the reversal is in place and you only ever hold a fixed handful of pointers, the whole thing stays O(n) time and O(1) space — the exact win the naive copy throws away. The diagram shows the split-reverse-compare dance.
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
These are the two problems that make people panic in interviews — and both panics come from the same place: reaching straight for a hash map. "Where do two lists merge?" → your gut says "throw all of list A's nodes in a set, then scan B looking for the first one that's already in the set." "Deep-copy a list whose nodes each have a random extra pointer to anywhere?" → your gut says "map each original node to its clone in a dictionary, then do a second pass wiring up the random links." Both hash-map answers are correct and completely reasonable to state — but each spends O(n) extra memory, and the follow-up question, every time, is "now do it in O(1) space." That follow-up is the whole game, and both have a startlingly clean pointer trick hiding behind the scary description.
For intersection (LC 160), the obstacle is that the two lists can be different lengths, so their shared tail doesn't line up if you naively walk them in parallel. The trick erases the length difference with a tiny sleight of hand: walk pointer p down list A and, when it falls off the end, redirect it to the head of list B; walk q down B and redirect it to A. Each pointer now travels lenA + lenB nodes total — the same distance — so they arrive at the shared node in the very same step (or both hit None together if the lists never join). It's the "swap tracks at the end so both trains run equal total distance" idea; no length math, no set.
For copy-with-random-pointer (LC 138), the difficulty is that a clone's random pointer must point at the clone of some arbitrary node — but during a single pass those clones don't exist yet, which is exactly why the naive solution needs a dictionary to remember "original → clone." The trick sidesteps the dictionary by weaving each new clone directly behind its original: the chain temporarily becomes A→A'→B→B'→…, so a node's clone is always simply node.next — the map is baked into the list's own structure. Then a clone's random is just cur.random.next, and a final pass un-weaves the two chains apart. The common mistake in both is over-engineering: writing the hash-map version and stopping, when the elegant O(1) trick is what's actually being tested. Both run in O(n) time and, thanks to the tricks, O(1) extra space. The lab shows both, and the tip below re-derives why intersection's swap-heads is guaranteed to align.
lenA + lenB and meet at the join. Copy-random: weave each clone behind its original so its address is node.next — no dictionary needed.
The diagram is split by a dashed line: the top half is the intersection trick, the bottom half is the copy-with-random trick. Read each half left-to-right.
- Top (intersection): two lists of different lengths (
a1 a2…andb1…) that join at a shared tail (c1 → c2). Each pointer, on hitting the end of its list, jumps to the other list's head. That makes both cover the same total distancelenA + lenB, so they land on the join nodec1in the same step. - Bottom (copy-with-random): the blue boxes (A, B, C) are the originals; the green boxes (A', B', C') are the fresh clones. Each clone is spliced in right behind its original, so the chain reads
A→A'→B→B'→…. - Why weave? Because a clone now lives at
original.next, so wiring a clone's random pointer is justcur.random.next— the "original→clone" lookup is baked into the list itself, replacing a hash map. - The final line notes the cleanup: a last pass un-weaves the interleaved chain back into the original list and a separate cloned list — both tricks finishing in O(1) extra space.
In short: Swap heads to equalise walking distance (intersection); weave clones behind originals so the map is the list (copy-random). Both dodge the hash map and hit 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 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?