Appearance
4.9.5 — Copy List with Random Pointer
LeetCode 138 · Medium · ★ Blind 75
The problem
Each node has a next pointer and a random pointer that may point to any node in the list, or to null. Make a deep copy: a completely new set of nodes with the same structure, sharing nothing with the original.
The pattern
Copying next is easy — walk forward and build as you go. The random pointer is the problem: when you are copying node 1 and its random points to node 5, node 5 has not been created yet.
That is the whole difficulty, and it has a name: you need to translate an old node into its new counterpart, before all the counterparts exist.
The Plain-English Breakdown
Imagine you have a linked list of nodes, but it's completely tangled. Not only does each node point to the next one, but it also has a random pointer that can point to literally any node in the list (including itself, or previous nodes, or future nodes). Your job is to create an identical, standalone clone of this entire tangled mess. Modifying the original list is generally frowned upon unless you revert it perfectly.
Constraints to note:
- The list length is between 0 and 1000.
- Node values are integers.
randomcan be null.
Brute Force Analysis
A naive approach would be to first copy all nodes using just their next pointers. Then, for every node in the original list, figure out which index its random pointer points to (by starting at the head and counting steps). Finally, take that same number of steps in the cloned list to set the new random pointer.
- Time Cost: Finding the index of a random pointer takes O(N) time per node. Doing this for N nodes takes O(N^2) time.
- Why it fails: While O(N^2) might technically pass for 1000 nodes, it is incredibly inefficient and entirely misses the structural mapping problem the interviewer is looking to test.
The fix is to do it in two passes.
- First pass: create every new node, and record which new node corresponds to each old node.
- Second pass: now that every counterpart exists, wire up
nextandrandomby looking each old pointer up in that record.
The record is a hash map from old node to new node. The nodes themselves are the keys.
ELI5 Analogy: Imagine you are cloning a group of friends who are all holding hands in a line (next), but they are also pointing at their best friend in the group (random). Pass 1: You make a clone of every person and just stand them in a corner. As you make each clone, you write in a notebook: "Original Bob maps to Clone Bob." Pass 2: You look at Original Bob. He is holding hands with Original Alice, and pointing at Original Charlie. You look up your notebook: "Who is Clone Alice? Who is Clone Charlie?" Then you tell Clone Bob to hold hands with Clone Alice and point at Clone Charlie.
Code Walkthrough (Hash Map Approach)
- Edge Case: If
headis null, there is nothing to copy, returnnull. - The Ledger: We create a dictionary
old_to_new = {}. This is our notebook. - Pass 1 (Creation): We loop through the original list. For each
currnode, we create a brand newNodewith the same.val. We store this mapping:old_to_new[curr] = Node(curr.val). Notice we are NOT setting.nextor.randomyet. - Pass 2 (Wiring): We reset
currback to thehead. We loop through the original list again. - Connecting: For the current original node, we fetch its clone:
copy = old_to_new[curr]. We then setcopy.nextto the clone ofcurr.next(using.get()to safely handle nulls). We setcopy.randomto the clone ofcurr.random. - Return: Finally, we return the clone of the head node:
old_to_new[head].
The solution
python
class Solution:
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
if not head:
return None
old_to_new = {}
# pass 1 — create every node, no pointers yet
curr = head
while curr:
old_to_new[curr] = Node(curr.val)
curr = curr.next
# pass 2 — now every counterpart exists, so wire them up
curr = head
while curr:
copy = old_to_new[curr]
copy.next = old_to_new.get(curr.next) # .get returns None for null
copy.random = old_to_new.get(curr.random)
curr = curr.next
return old_to_new[head]ts
function copyRandomList(head: Node | null): Node | null {
if (!head) return null;
const map = new Map<Node, Node>();
let curr: Node | null = head;
while (curr) {
map.set(curr, new Node(curr.val));
curr = curr.next;
}
curr = head;
while (curr) {
const copy = map.get(curr)!;
copy.next = curr.next ? map.get(curr.next)! : null;
copy.random = curr.random ? map.get(curr.random)! : null;
curr = curr.next;
}
return map.get(head)!;
}old_to_new.get(curr.next) rather than old_to_new[curr.next]. When curr.next is None, the key is not in the map, and [] would raise. .get returns None, which is exactly the value you want to store. One method call replaces two if statements.
Objects as dictionary keys. In Python this works because objects hash by identity by default, which is precisely what you want — two different nodes with the same value must stay different. In JavaScript, Map also compares object keys by identity. Note that this is the opposite of the situation in 4.4.4, where identity comparison was the bug. Here identity is exactly right, because you are tracking specific objects rather than equal values.
Complexity
O(n) time, two passes. O(n) space for the map.
Derivation:
- Time Complexity: O(N). We walk the list of length N exactly twice. Hash map insertions and lookups take O(1) time on average. Total time is 2N \times O(1) = O(N).
- Space Complexity: O(N). We store every single node from the original list as a key in our hash map, mapped to its copy. This requires N entries in the dictionary. (The space of the copied list itself doesn't count against auxiliary space complexity since it's the required output, but the dictionary does).
The O(1)-space version
There is a clever solution that removes the map entirely, by storing the correspondence inside the list itself.
Step 1 — weave each copy in behind its original.
A → B → C
becomes
A → A' → B → B' → C → C'Step 2 — set the random pointers. For any original node curr, its copy is curr.next. And the copy of curr.random is curr.random.next. So:
python
copy.random = curr.random.next if curr.random else NoneThe list structure is now doing the job the hash map was doing. That is the whole idea.
Step 3 — unweave, separating the two lists and restoring the original.
python
class Solution:
def copyRandomList(self, head):
if not head:
return None
# 1. weave
curr = head
while curr:
curr.next = Node(curr.val, curr.next)
curr = curr.next.next
# 2. randoms, using the woven structure as the lookup
curr = head
while curr:
if curr.random:
curr.next.random = curr.random.next
curr = curr.next.next
# 3. unweave
old, new_head = head, head.next
while old:
copy = old.next
old.next = copy.next
copy.next = copy.next.next if copy.next else None
old = old.next
return new_headO(n) time, O(1) extra space. It is three passes instead of two, and it temporarily mutates the input, which is a real objection in production code.
Know it, and know when to write it. The map version is the one to lead with — it is clearly correct and easy to explain. Offer this when asked to remove the extra space, and mention the mutation as its cost.
Where this goes next
- Clone Graph (LeetCode 133) — the same problem on a graph rather than a list, so the traversal becomes DFS or BFS and the map does the same job. Chapter 4.20. If you see these two as one problem, you have understood both.
- Deep copy of any object with cycles — a serialiser,
structuredClone, or a JSON encoder handling shared references. Every one of them keeps an identity map so a node appearing twice is copied once. Naming this is what turns the problem from a puzzle into general knowledge.
The rule: when a copy must preserve references to objects that may not exist yet, build all the objects first and keep an old-to-new map, then wire the pointers in a second pass.
What the interviewer will push on
"Why can you not do it in one pass?" A random pointer may point forwards to a node not yet created. Naming this before writing anything is the sign you understood the problem.
"What are the keys of your map?" The original nodes themselves, compared by identity. Value-based keys would merge distinct nodes with equal values.
"Can you do it without the map?" The weaving trick. Say the cost: it mutates the input temporarily.
"How does this relate to cloning a graph?" Same problem, different traversal.
One thing to volunteer: say that this is what a deep-copy routine does with shared references. It shows you know the problem is real rather than invented.
Next: 4.9.6 Add Two Numbers — arithmetic on a list, and the carry that must survive past the end.