AI EngineeringZero to ProductionHome·About·What’s new·Contact
Appendix · Data Structures & Algorithms · Part 7

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.

⏱️ ~2.5 hours🎯 Basic → Expert🔗 pointer disciplinerunnable

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.
The golden rules of linked-list code(1) Draw the pointers before you type. (2) Use a dummy head so the "first node" is never a special case. (3) When rewiring, save 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.

Singly — one next pointer A B C → ∅ Doubly — prev + next A B C Circular — tail loops to head A B C Three flavours, one idea. Singly walks forward only. Doubly adds a 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).
🗺️ How to read this diagram

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 next pointers — 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 next and a backward prev. 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.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Node definitions
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
▶ How this works

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.

  1. ListNode is one node of a singly list: it stores a value val and a pointer next to the following node (which starts as None — meaning "nothing after me yet"). DListNode is the doubly version — it also keeps a prev pointer backward.
  2. build(vals) turns a normal list like [1,2,3] into a chain. It starts with a throwaway dummy node and a tail that always points at the last real node. For each value it hangs a new node off tail.next, then moves tail onto it — growing the chain one link at a time.
  3. It returns dummy.next — the real first node — because the dummy was only scaffolding.
  4. to_list(head) walks the chain the other way: start at head, append each head.val, then step forward with head = head.next until head becomes None. That while 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.

dummy head … never holds data return dummy.next at the end The dummy head removes the "what if it's the first node?" branch. Every insert/delete has a real predecessor to rewire, so one code path handles head, middle, and tail alike. Used in merge, remove-nth, partition, remove-duplicates, and more.
🗺️ How to read this diagram

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 next pointers: 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.

Remove all nodes with a given value (classic) — dummy head
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
▶ How this works

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.

  1. dummy = ListNode(0, head) creates the fake node whose next already points at the real head. prev = dummy starts our "node before the one we're inspecting" right on the dummy.
  2. while prev.next: keeps going as long as there's a node after prev to look at. We always inspect prev.next, never prev itself.
  3. If prev.next.val == target, we splice it out: prev.next = prev.next.next makes prev skip over the doomed node and point straight at the one after it. The removed node is now unreachable — gone.
  4. Otherwise (else) the current node stays, so we advance prev = prev.next. Note we only advance when we don't delete, so consecutive matches all get removed.
  5. 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.

flip each node's next to point at the previous node 1 2 3 4 prev cur nxt (saved first) Reverse by pointer-flipping. Keep prev, cur, nxt: save nxt = cur.next, set cur.next = prev, then slide all three forward. One O(n) pass, O(1) space.
🗺️ How to read this diagram

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.next so we don't lose the rest of the list, (2) cur.next = prev flips this node's arrow backward, (3) slide all three markers one step forward (prev, cur = cur, nxt).
  • Why save nxt first? The instant you do cur.next = prev you'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.

Reverse whole list, a sublist (LC 92), and in K-groups (LC 25)
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
▶ How this works

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.

  1. (a) reverse — prev starts at None (the new tail's "next"). The one dense line head.next, prev, head = prev, head, head.next does all three steps at once: point this node back at prev, move prev onto this node, and step head forward. Python evaluates the right side first, so the old head.next is safely captured before it's overwritten. Return prev — the new head.
  2. (b) reverse_between — a dummy head plus a first loop to walk prev to the node just before position left. Then the head-insert trick: repeatedly unhook the node after cur and splice it to the front of the segment, so the segment flips in place while the rest of the list stays attached.
  3. (c) reverse_k_group — the helper kth jumps ahead k nodes to check a full group exists; if not kth_node: break stops 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 with group_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.

🔗 Builds onThe pointer-flip from D2 §6. Same skill: reverse-nodes-in-even-length-groups, swap-nodes-in-pairs, rotate-list.

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 +1, fast +2 → when fast hits the end, slow is at the middle 1 2 3 4 5 slow (middle) fast (end) Speed-based two pointers. Move 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.
🗺️ How to read this diagram

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 fast reaches the end, slow has 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.

Middle, remove nth-from-end (LC 19), cycle start (LC 142)
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
▶ How this works

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.

  1. middle — slow and fast both start at the head; each loop step does slow = slow.next and fast = fast.next.next. The guard while fast and fast.next: stops safely whether the list has an odd or even number of nodes. When it stops, slow is the middle.
  2. remove_nth_from_end — first open a gap of n: move fast ahead n nodes. Now advance both until fast is the last node — at that moment slow sits just before the target. slow.next = slow.next.next splices the target out. The dummy head means even "remove the first node" needs no special case.
  3. 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 pointer p at the head and advance p and slow together one step at a time — they meet exactly at the node where the loop starts. If fast ever hits None, there's no loop, so return None.

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.

🔗 Builds onFloyd's tortoise & hare from D2 §8. The "why they meet at the cycle start" is a classic follow-up — advancing one pointer from the head and one from the meeting point, both at speed 1, lands them exactly on the entry.

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.

two sorted inputs 1 3 a 2 4 b take the smaller front node each step → output grows behind a dummy tail dummy 1 2 3 4 tail always sits on the last node → O(1) append when one input empties: tail.next = a or b attaches the whole rest in one move Build, don't rearrange. Compare the two front nodes, splice the smaller onto the dummy tail, advance. A tail pointer that always marks the end keeps every append O(1), so the whole merge is O(n+m).
🗺️ How to read this diagram

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 (a and b) 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 dummy box 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 next pointers being laid down one at a time. The note on the right is the payoff: a tail pointer 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 b hooks 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.

Merge two sorted lists (LC 21) + add two numbers (LC 2)
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
▶ How this works

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.

  1. The shared setup dummy = tail = ListNode() makes both names point at the same fake node. tail always marks the end of the list we're growing; dummy stays put so we can return dummy.next at the finish.
  2. 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 b attaches the remaining tail in one shot (a or b gives whichever isn't None).
  3. 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 new carry and the digit to store.
  4. The loop condition while a or b or carry is the subtle part: it keeps going until both lists are exhausted and there's no leftover carry — so 99 + 1 = 100 correctly 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.

🔗 Builds onmerge_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.

1 → 2 → 2 → 1 : split at middle, reverse right, compare halves 1 2 1 2 | middle | left half == reversed right half → palindrome ✓ Compose the primitives. Find the middle (fast/slow §4), reverse the second half (§3), then walk both halves inward. O(n) time, O(1) space — no extra array needed.
🗺️ How to read this diagram

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.

Palindrome list (LC 234) + reorder list (LC 143)
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
▶ How this works

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).

  1. is_palindrome, step 1: the fast/slow loop walks slow to the middle while fast runs to the end.
  2. Step 2: while slow: reverses the second half in place using the same three-way assignment as the whole-list reversal — prev ends up as the head of the reversed back half.
  3. Step 3: walk prev (reversed back half) and head (front half) together; if any pair of values differs, return False. If they all match, it's a palindrome.
  4. 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 guard if not head or not head.next: return handles 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.

Intersection — swap heads so both walk lenA + lenB a1 a2 c1 c2 b1 shared tail c1 → c2 p: A then B, q: B then A → equal distance → meet at c1 Copy-with-random — weave each clone right behind its original A A' B B' C C' clone of X is always just X.next — no map a clone's random = cur.random.next · then un-weave into two clean chains green = the freshly-made copies; both tricks reach O(1) extra space Two "scary" problems, two pointer tricks. Intersection: swap each walker to the other list's head at the end so both cover lenA + lenB and meet at the join. Copy-random: weave each clone behind its original so its address is node.next — no dictionary needed.
🗺️ How to read this diagram

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… and b1…) 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 distance lenA + lenB, so they land on the join node c1 in 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 just cur.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.

Intersection of two lists (LC 160) + copy list with random pointer (LC 138)
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
▶ How this works

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.

  1. get_intersection — start p on list A and q on 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 reach None together if the lists never join).
  2. 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.
  3. 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).
  4. 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.

Why the intersection trick worksPointer 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 forCost
insert/delete near the headdummy head sentinelO(1) edge-case-free
reverse / reverse part / K-grouppointer-flip (prev/cur/nxt)O(n), O(1)
middle / nth-from-endfast & slow / offset pointersO(n) one pass
cycle exists / where it startsFloyd's two-phaseO(n), O(1)
merge / add / partitionbuild behind a dummy tailO(n)
palindrome / reordermiddle + reverse-half + compareO(n), O(1)
two lists intersectswap-heads two pointersO(n+m), O(1)
deep copy with extra pointersinterleave clonesO(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.

Exercise 1 · Build and traverseBeginner

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 next reference
  • Build the list by linking nodes in order
  • Traverse by following next until None
  • 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.

Exercise 2 · Reverse a linked listIntermediate

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 next pointer
  • 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.

Exercise 3 · Middle node & nth-from-endAdvanced

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.

Exercise 4 · Merge two sorted lists (dummy head)Expert

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.next as 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.

Exercise 5 · Reverse in k-groupsProfessional

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.

Exercise 6 · Detect the cycle's start nodeIndustry scenario

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 None if 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

✓ Knowledge check

What problem does the dummy-head (sentinel) node solve in linked-list code?

Show answer
A dummy node placed before the real head gives every node — including the first — a predecessor, so insertions and deletions at the head need no special-case code. You return dummy.next at the end.
✓ Knowledge check

How do you find the middle of a linked list (or the start of a cycle) in one pass with two pointers?

Show answer
Advance a slow pointer one step and a fast pointer two steps per iteration; when fast reaches the end, slow is at the middle. The same fast/slow trick, with a reset after they meet, locates a cycle's start.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in