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.
Table of contents
- Before we start
- Picture this first (no code yet)
- The actual problem
- First, the slow way (so you feel the pain)
- The turning point
- The one idea to remember
- Approach 1: Hashmap (two-pass)
- Approach 2: Interleave (O(1) space, three passes)
- Watch it happen, frame by frame
- Which approach to use?
- Common traps
- Check yourself
- Practice problems
- Chapter 4 complete!
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, andrandom: Node | None.randomcan 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:
old_to_new = {}— dictionary: keys are original node objects (by identity, not value), values are their clones.- Pass 1 — create every clone with only
valset; no pointers yet. - Pass 2 — for each original, its clone already exists for every node, so
old_to_new[cur.next]andold_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. 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):
1 → 1' → 2 → 2' → 3 → 3' → 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: 1 → 2 → 3 → None
Clone: 1'→ 2'→ 3'→ None with randoms 1'→3', 2'→1', 3'→None ✅
Which approach to use?
| Criterion | Hashmap (Approach 1) | Interleave (Approach 2) |
|---|---|---|
| Code clarity | High — two clean passes | Medium — three passes, tricky rewiring |
| Space | O(n) | O(1) |
| Mutates input? | No | Yes (temporarily) |
| Interview signal | Good | Excellent — 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. Ifcur.nextis None,old_to_new[None]raises KeyError. Useold_to_new.get(cur.next)or guard withif 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'sclone.nextis None.clone.next.nextwould crash. Guard:clone.next = clone.next.next if clone.next else None. - Advancing by
cur.next.nextin Pass 2 but not guarding for None. In Pass 2,cur = cur.next.next. Since the list ends with a clone (C') whose.nextis None,cur.nextafter the last original is the last clone, andcur.next.nextis 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), notcur.val. Python dictionaries use object identity for non-hashable types... actually Node objects are hashable by default (usingid()). Just use the node object as the key — nevercur.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
| Problem | Difficulty | What to notice | Link |
|---|---|---|---|
| Copy List with Random Pointer | Medium | Hashmap O(n) space; interleave O(1) space | LC #138 |
| Clone Graph | Medium | Same idea: DFS + hashmap of original→clone | LC #133 |
| Clone N-ary Tree | Medium | DFS + hashmap; clone children list | LC #1490 |
Chapter 4 complete!
You have now covered all ten Linked List patterns:
| # | Pattern | Key idea |
|---|---|---|
| 4.01 | Fast & Slow Pointers (Floyd) | Two speeds; O(1) cycle detection |
| 4.02 | Find Middle | Slow reaches middle when fast reaches end |
| 4.03 | Reverse a Linked List | Three-pointer prev, curr, nxt |
| 4.04 | Merge Two Sorted Lists | Dummy head; pick smaller node each step |
| 4.05 | Remove Nth from End | N+1 gap between fast and slow |
| 4.06 | Intersection of Two Lists | Switch lists; equal total distance |
| 4.07 | Reorder List | find-middle → reverse → merge-alternately |
| 4.08 | Flatten a Linked List | Stack saves deferred continuations |
| 4.09 | Add Numbers as Linked List | Dummy head; carry; or carry loop |
| 4.10 | Clone with Random Pointer | Hashmap or interleave trick |
Next chapter: Stacks & Queues — monotonic stacks, deque sliding window, and more.