Learn/DSA Patterns
DSA PatternsLinked Listsmedium10 min read

Clone a List with Random Pointers

Deep-copy a linked list where each node has a next pointer and a random pointer to any node (or None). Master the O(n) hashmap approach, then learn the brilliant O(1)-space interleave trick — copy nodes woven between originals — used in top-tier interviews.

#linked-list#clone#deep-copy#random-pointer#hashmap#interleave#beginner#interview
Table of contents

Before we start

A normal linked list clone is trivial — walk and copy. The challenge here is the random pointer, which can point to any node, including nodes you haven't created yet. This forces you to think about the order in which you create and wire nodes. By the end you will be able to:

  • Explain why a hashmap naturally solves the forward-reference problem.
  • Write the O(1)-space interleave solution — the approach interviewers love — from memory in three clean passes.
  • Trace both solutions on a 4-node example.

Picture this first (no code yet)

A real-life story

You are photocopying an organizational chart. Each box has a "reports to" arrow (next in the chain) and a "buddy" arrow that points to any other box in the org (the random pointer). You can't fill in the buddy arrows on the photocopy until you've made all the photocopied boxes — otherwise you'd be pointing back to the original boxes, not the copies.

Approach 1: keep a sticky note mapping "original box → copied box." Once all copies exist, go through the chart again and translate every buddy arrow using the sticky note.

Approach 2 (clever): interleave copies directly after originals in the same chart — A → A' → B → B' → C → C'. Now original A's buddy points to original X; the copy A' should point to X', which is simply X.next. No sticky note needed, just pointer arithmetic.


The actual problem

Copy List with Random Pointer (LC #138):

Each node has val: int, next: Node | None, and random: Node | None. random can point to any node in the list or None. Return a deep copy — a completely new list where no node is shared with the original.

Example:

Original:
  1  2  3  4  None
  random: 1→3, 2→1, 3→4, 4→2

After cloning, every node must be a new object; cloned random pointers point to cloned nodes, not originals.


First, the slow way (so you feel the pain)

Naive approach: walk the list, create a copy of each node with only val set. Keep the copies in an array. Walk again: for each original node, look up its random in the array using its index. This requires O(n) space for the array and O(n) time per random lookup (to find the index of the random target). Total: O(n²) time. Unacceptable for a list of 10,000 nodes — 10⁸ operations.


The turning point

Pause & think

The core difficulty: when you clone node A and need to set cloneA.random = ???, the clone of A.random may not exist yet.

Solution: separate the process into two passes:

  • Pass 1: create all clone nodes (just val, no pointers wired).
  • Pass 2: wire all pointers using a lookup from original → clone.

What data structure maps original node object → its clone object? A hashmap (dictionary) keyed on the original node.

Can you reduce space beyond O(n) for the hashmap? The interleave trick does this — it encodes the original→clone mapping into the list itself using the next pointer positions.


The one idea to remember

The entire pattern in one sentence

Either map each original node to its clone with a hashmap (clean, O(n) space), or interleave clones into the original list so that clone_of(node) = node.next, then wire randoms, then separate the two lists (O(1) space).


Approach 1: Hashmap (two-pass)

def copyRandomList(head):
    if not head:
        return None

    old_to_new = {}   # maps original node → its clone

    # Pass 1: create all clone nodes
    cur = head
    while cur:
        old_to_new[cur] = Node(cur.val)
        cur = cur.next

    # Pass 2: wire next and random pointers
    cur = head
    while cur:
        clone           = old_to_new[cur]
        clone.next      = old_to_new.get(cur.next)    # None if cur.next is None
        clone.random    = old_to_new.get(cur.random)  # None if cur.random is None
        cur = cur.next

    return old_to_new[head]

Line-by-line narration:

  1. old_to_new = {} — dictionary: keys are original node objects (by identity, not value), values are their clones.
  2. Pass 1 — create every clone with only val set; no pointers yet.
  3. Pass 2 — for each original, its clone already exists for every node, so old_to_new[cur.next] and old_to_new[cur.random] are both safe to look up (they exist). .get() returns None when the key is None, which is the correct behaviour.
  4. return old_to_new[head] — the clone of the original head.

Complexity: O(n) time (two passes), O(n) space (hashmap with n entries).


Approach 2: Interleave (O(1) space, three passes)

def copyRandomList(head):
    if not head:
        return None

    # Pass 1: interleave clones between originals
    # Original: A → B → C
    # After:    A → A' → B → B' → C → C'
    cur = head
    while cur:
        clone     = Node(cur.val)
        clone.next = cur.next
        cur.next  = clone
        cur       = clone.next   # advance to next original

    # Pass 2: set random pointers of clones
    # clone_of(X) = X.next
    # clone_of(X.random) = X.random.next
    cur = head
    while cur:
        if cur.random:
            cur.next.random = cur.random.next
        cur = cur.next.next   # skip the clone, go to next original

    # Pass 3: restore original list and extract clone list
    old_head = head
    new_head = head.next
    cur = head
    while cur:
        clone      = cur.next
        cur.next   = clone.next            # restore original's next
        clone.next = clone.next.next if clone.next else None  # wire clone's next
        cur        = cur.next              # advance to next original
    return new_head

Pass 1 narration: Insert each clone directly after its original. The clone captures the original's old next as its own next. The original's next is updated to point to the clone.

Pass 2 narration: cur.next is the clone of cur. cur.random.next is the clone of cur.random. So cur.next.random = cur.random.next sets the clone's random to the clone of the original's random — with O(1) lookup using the interleaved structure.

Pass 3 narration: Disentangle the two interleaved lists by resetting each original's next to skip over the clone, and each clone's next to skip over the next original.


Watch it happen, frame by frame

List: 1 → 2 → 3, random: 1→3, 2→1, 3→None

After Pass 1 (interleave):

11' → 2 → 2'33' → None

Node pointers: 1.next=1', 1'.next=2, 2.next=2', 2'.next=3, 3.next=3', 3'.next=None.

Pass 2 (set randoms):

cur=1: random=3, 3.next=3'. So 1'.random = 3'. ✅
cur=2: random=1, 1.next=1'. So 2'.random = 1'. ✅
cur=3: random=None → skip.

Pass 3 (separate):

cur=1:
  clone=1', cur.next = 1'.next = 2 (original restored)
  clone.next = 2.next = 2' (clone chain wired)
  cur = cur.next = 2

cur=2:
  clone=2', cur.next = 2'.next = 3
  clone.next = 3.next = 3'
  cur = 3

cur=3:
  clone=3', cur.next = 3'.next = None
  clone.next = None (clone.next is None → guarded)
  cur = None

Original restored: 123None
Clone:             1'→ 2'3'→ None  with randoms 1'3', 2'1', 3'None

Which approach to use?

CriterionHashmap (Approach 1)Interleave (Approach 2)
Code clarityHigh — two clean passesMedium — three passes, tricky rewiring
SpaceO(n)O(1)
Mutates input?NoYes (temporarily)
Interview signalGoodExcellent — shows pointer mastery

Start with Approach 1 if you're asked in a time-pressured setting. Offer Approach 2 as a follow-up when the interviewer asks "can you do it with O(1) space?"


Common traps

Watch out for these

  • Using .get() vs [] on the hashmap for None keys. If cur.next is None, old_to_new[None] raises KeyError. Use old_to_new.get(cur.next) or guard with if cur.next: clone.next = old_to_new[cur.next]. The .get() pattern returns None by default, which is exactly what you want.
  • In Pass 3 of interleave: forgetting to guard clone.next. The last clone's clone.next is None. clone.next.next would crash. Guard: clone.next = clone.next.next if clone.next else None.
  • Advancing by cur.next.next in Pass 2 but not guarding for None. In Pass 2, cur = cur.next.next. Since the list ends with a clone (C') whose .next is None, cur.next after the last original is the last clone, and cur.next.next is None — so the loop naturally exits. No extra guard needed, but think through it.
  • Confusing node identity with node value. Two different nodes can have the same val. The hashmap key is the node object itself (by reference/identity), not cur.val. Python dictionaries use object identity for non-hashable types... actually Node objects are hashable by default (using id()). Just use the node object as the key — never cur.val.

Remember this forever

Clone with Random — hashmap (clean)

d = {}
cur = head
while cur: d[cur] = Node(cur.val); cur = cur.next
cur = head
while cur:
    d[cur].next   = d.get(cur.next)
    d[cur].random = d.get(cur.random)
    cur = cur.next
return d[head]

Clone with Random — interleave (O(1) space, 3 passes)

Pass 1: insert clone after each original.
Pass 2: cur.next.random = cur.random.next  (if cur.random).
Pass 3: separate originals and clones, fix next pointers.

Key insight: clone_of(node) = node.next during passes 2 and 3.


Check yourself

In Approach 2, why does `cur.random.next` give us the clone of `cur.random`?

After Pass 1, every original node has its clone inserted immediately after it. So for any original node X, X.next is X's clone. Therefore, cur.random.next is the clone of cur.random. This is the central insight of the interleave trick: the clone of any node is encoded as that node's immediate next successor — no hashmap needed.

After Pass 3 in Approach 2, is the original list fully restored?

Yes. During Pass 3, for each original node cur, we set cur.next = clone.next (where clone = cur.next). clone.next was the next original before interleaving, and we wired it correctly in Pass 1 (clone.next = cur.next — the old next of cur). So Pass 3 correctly restores cur.next to the next original node, and the original list is fully intact after the function returns.

What if two nodes have the same value? Does the hashmap still work correctly?

Yes. The hashmap keys are node objects (by reference), not node values. Two nodes with the same val are different Python objects with different id() values. Python dictionaries use == for key comparison only when objects define __eq__; the default __eq__ for objects compares identity (same as is). So {node_A: cloneA, node_B: cloneB} correctly maintains separate entries even if node_A.val == node_B.val.


Practice problems

ProblemDifficultyWhat to noticeLink
Copy List with Random PointerMediumHashmap O(n) space; interleave O(1) spaceLC #138
Clone GraphMediumSame idea: DFS + hashmap of original→cloneLC #133
Clone N-ary TreeMediumDFS + hashmap; clone children listLC #1490

Chapter 4 complete!

You have now covered all ten Linked List patterns:

#PatternKey idea
4.01Fast & Slow Pointers (Floyd)Two speeds; O(1) cycle detection
4.02Find MiddleSlow reaches middle when fast reaches end
4.03Reverse a Linked ListThree-pointer prev, curr, nxt
4.04Merge Two Sorted ListsDummy head; pick smaller node each step
4.05Remove Nth from EndN+1 gap between fast and slow
4.06Intersection of Two ListsSwitch lists; equal total distance
4.07Reorder Listfind-middle → reverse → merge-alternately
4.08Flatten a Linked ListStack saves deferred continuations
4.09Add Numbers as Linked ListDummy head; carry; or carry loop
4.10Clone with Random PointerHashmap or interleave trick

Next chapter: Stacks & Queues — monotonic stacks, deque sliding window, and more.