AI EngineeringZero to ProductionHome·About·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

All linked lists are nodes joined by pointers; they differ only in which pointers each node carries and whether the tail loops back.

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

The single most useful linked-list idea. A dummy (sentinel) node sits before the real head, so inserting/deleting the first element is no longer a special case — you always have a "previous" node to point from. Return dummy.next at the end.

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

Reversal is the linked-list skill. The core move: for each node, remember its next, flip its pointer backward, advance. Everything else builds on it.

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

Three classics, all one pass, all built on running two pointers at different speeds or offsets — no length precomputation needed.

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

Both stitch a new list together node-by-node behind a dummy head — the bread-and-butter of list construction.

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

These two composite problems show why the earlier patterns matter: both = find middle + reverse second half, then compare or interleave. Classic Meta/Amazon questions.

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

Two problems that look scary but fall to clean tricks.

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