Copy List with Random Pointer
LINKED LIST · MEDIUM
The problem
Section titled “The problem”You’re given the head of a linked list where every node has two pointers instead of one: the usual next, which walks straight through the list (or is null at the end), and a second pointer called random, which can point to any node anywhere in the list — including the node itself, or null. Your job is to build a complete, independent deep copy of the whole list: every new node must have its own next and random pointing to the corresponding new nodes, never back to any node from the original list.
For example, a list represented as [val, randomIndex] pairs — [[7,null],[13,0],[11,1]] — describes three nodes: node 0 has value 7 and a null random pointer; node 1 has value 13 and its random points at node 0; node 2 has value 11 and its random points at node 1. The copy must reproduce that exact same shape — same values in the same order, same random-target relationships — using entirely new node objects. Getting the values right but pointing any next or random pointer back at an original node (instead of its copy) is an incorrect solution, even if it “looks” right when printed.
Solution 1: Hash map, two passes
Section titled “Solution 1: Hash map, two passes”The natural first idea: since random can point anywhere, you need some way to translate “an original node” into “its corresponding copy node” — and a Map keyed by the original node objects themselves is exactly that translation table. Pass 1 walks the original list once, creating a copy of every node (with next/random left unset) and recording original → copy in the map. Pass 2 walks the original list again, and for every node, looks up the map to wire its copy’s next and random to the copies of whatever the original pointed to.
Two passes are required, not one — a node’s random can point forward to a node that hasn’t been copied yet. If wiring happened during the same pass as creation, that lookup could miss (the copy simply wouldn’t exist in the map yet). Splitting into “create everything first, then wire everything” sidesteps that ordering problem entirely.
[[7,null],[13,0],[11,1]] — node 1's random points at node 0; node 2's random points at node 1
Press Play or Step to begin.
Step 0 / 0
The code
Section titled “The code”function copyRandomListHashMap(head) { if (!head) return null; const copyMap = new Map();
// Pass 1: create a copy of every original node. next/random are left // unset for now — some randoms point forward to nodes not copied yet. let cur = head; while (cur) { copyMap.set(cur, { val: cur.val, next: null, random: null }); cur = cur.next; }
// Pass 2: wire next/random using the map to translate original -> copy. cur = head; while (cur) { const copy = copyMap.get(cur); copy.next = cur.next ? copyMap.get(cur.next) : null; copy.random = cur.random ? copyMap.get(cur.random) : null; cur = cur.next; }
return copyMap.get(head);}Line by line:
copyMapis a realMap, keyed by the original node objects themselves (not their values) — that reference-identity keying is exactly what lets pass 2 translate “this original node” into “its copy” reliably, even when several nodes share the sameval.- Pass 1 only ever reads
cur.nextto advance — it never touchesrandom, so the order nodes get copied in is guaranteed to be the same order as the original list, and every original node is guaranteed to have an entry in the map before pass 2 starts. copy.next = cur.next ? copyMap.get(cur.next) : null;— translates the original’s next pointer into the copy’s next pointer via one map lookup. Same pattern forrandomon the next line.return copyMap.get(head);— the map already has every original-to-copy mapping, so the copy of the head is just one more lookup away.
Complexity
Section titled “Complexity”| Time | Space | Approach |
|---|---|---|
| O(n) | O(n) | Hash map translating original nodes to copy nodes, two linear passes |
Solution 2: Optimal — interleaving, O(1) extra space
Section titled “Solution 2: Optimal — interleaving, O(1) extra space”The hash map’s whole job is answering one question over and over: “given this original node, where’s its copy?” The interleaving trick answers that question without a map at all, by exploiting a structural guarantee it builds itself: splice every copy node in immediately after its original, so the list becomes A → A' → B → B' → C → C' → .... Once that’s true, the copy of any node is always just one .next hop away from the original — copyOf(X) === X.next, always, by construction. No lookup structure needed; the list’s own shape is the lookup table.
Pass 1 — interleave. Walk the original list once. For each node, create its copy and insert it right after: the original’s next is reassigned to point at the new copy, and the copy’s next takes over whatever the original used to point to.
Pass 2 — wire random. Walk the (now interleaved) list again, stepping by the original nodes only. For each original node A, its copy’s random pointer is A.next.random = A.random ? A.random.next : null — take one more hop off A.random to land on its copy. This is only correct because pass 1 guarantees “one hop past any node is that node’s copy” for every node in the list, including whatever A.random points to.
Pass 3 — separate. Walk the interleaved list one final time and split it back into two independent lists: each original’s next is restored to skip over its copy (pointing at the next original), and each copy’s next is set to skip over the next original (pointing at the next copy). No array, no map — the whole algorithm uses a constant number of pointer variables no matter how long the list is.
[[7,null],[13,0],[11,1]] — same example, watch the list become interleaved then split back apart
Press Play or Step to begin.
Step 0 / 0
The code
Section titled “The code”function copyRandomListInterleave(head) { if (!head) return null;
// Pass 1: interleave a copy of every node right after the original. let cur = head; while (cur) { const copy = { val: cur.val, next: cur.next, random: null }; cur.next = copy; cur = copy.next; }
// Pass 2: wire each copy's random pointer. The copy of any node X is // always X.next (thanks to pass 1) — so the copy of original.random // is exactly original.random.next. cur = head; while (cur) { const copy = cur.next; copy.random = cur.random ? cur.random.next : null; cur = copy.next; }
// Pass 3: separate the interleaved list back into two independent lists. cur = head; const newHead = head.next; while (cur) { const copy = cur.next; cur.next = copy.next; copy.next = copy.next ? copy.next.next : null; cur = cur.next; }
return newHead;}Line by line:
- Pass 1’s
const copy = { val: cur.val, next: cur.next, random: null }; cur.next = copy;is the entire splice: the copy’s next takes over what the original used to point to, then the original’s next is reassigned to the copy.cur = copy.nextadvances past the copy to the next original. - Pass 2’s
copy.random = cur.random ? cur.random.next : null;is the key trick in one line —cur.randomis some original node (or null), and one more.nextoff of it lands on that node’s copy, because pass 1 guarantees every original is immediately followed by its own copy. - Pass 3’s
cur.next = copy.next;restores the original list — it now points to the next original, skipping the copy sitting between them.copy.next = copy.next ? copy.next.next : null;does the mirror image for the copy list, skipping the next original to reach the next copy. const newHead = head.next;is captured before pass 3’s loop runs — it’s the copy of the head, which is exactly what pass 1 spliced in right afterhead, and it stays correct throughout pass 3 since that line never toucheshead.nextagain until the loop reassigns it on its very first iteration.
Complexity
Section titled “Complexity”| Time | Space | Approach |
|---|---|---|
| O(n) | O(1) | Interleaving trick — three linear passes, no auxiliary map |
Complexity comparison
Section titled “Complexity comparison”| Solution | Time | Space |
|---|---|---|
| Hash map (two passes) | O(n) | O(n) |
| Optimal (interleaving) | O(n) | O(1) |
Both solutions are O(n) time — the interleaving solution runs three linear passes instead of two, so it is not asymptotically (or even practically) faster, and it would be dishonest to claim it is. The real, honest improvement is space: the Map that grows to hold one entry per node is replaced entirely by the list’s own structure, dropping extra space from O(n) down to O(1).
Interview corner
Section titled “Interview corner”Q: Why does interleaving a copy right after every original guarantee that “the copy of node X is always X.next”? What would break if pass 1 inserted copies in a different order, or in a separate pass from creation?
- It’s guaranteed because pass 1 processes nodes in list order and, for each one, immediately does two things back to back: create the copy, then splice it in as that exact node’s
next. There’s no gap in which a node exists without its copy sitting directly after it. - If copies were created in one pass and spliced in during a later, separate pass, pass 2’s random-wiring trick (
original.random.next) couldn’t be trusted — some nodes would have copies that exist but aren’t in position yet, so “one hop past X” would not reliably mean “X’s copy” at the moment pass 2 needs it to. - This is also exactly why pass 1 reassigns
cur.nextto the copy before advancing — the moment a node is processed, its copy is already “live” in the list structure, which is what pass 2 depends on for every single node, not just some of them.
Q: Solution 1 does two separate passes — could you merge pass 1 and pass 2 into a single loop that creates each node’s copy and wires its random pointer at the same time?
- No — a node’s
randomcan point forward to a node that hasn’t been copied yet in the single pass. In the traced example, if node 0 processes first and its random happened to point at node 2,copyMap.get(node2)would come backundefined, because node 2’s copy doesn’t exist yet. - Splitting into “create every copy first” (pass 1) then “wire every pointer” (pass 2) sidesteps the ordering problem completely — by the time pass 2 runs a single lookup, every original node, no matter where it sits in the list, already has an entry in the map.
- The interleaving solution has a similar-looking constraint and handles it the same way: pass 2 (wiring random) only starts after pass 1 (creating and splicing every copy) has fully finished, for exactly the same forward-pointer reason.
Q: What is the actual, honest complexity improvement of the interleaving solution over the hash-map solution — and what would be dishonest to claim?
- Both solutions are O(n) TIME. The interleaving solution runs three linear passes instead of two, so it isn’t asymptotically faster — claiming a time win would be incorrect.
- The real difference is SPACE: the hash map is O(n) because it holds one entry per node, while the interleaving solution is O(1) — a constant number of pointer variables, regardless of how long the list is.
- The honest framing is “same time complexity, different technique, real space win” — not “brute force vs. optimal” in the Big-O sense. The hash map is a completely legitimate, commonly-used solution; the interleaving trick just trades a small amount of extra bookkeeping complexity for dropping the auxiliary memory entirely.
- ↳ Common follow-up: “Which would you actually use in production code?” The hash-map version, almost always — it’s easier to read, easier to get right under interview pressure, and O(n) space for a linked list copy is rarely the bottleneck. The interleaving trick is the kind of thing worth knowing exists (and demonstrating you can derive), not necessarily the default choice.
Q: Trick question — Solution 1 keys a Map by the original node objects themselves, not by an index or a value. Would a plain JavaScript object ({}) work as a drop-in replacement for that Map?
const a = { val: 7 };const b = { val: 13 };const obj = {};obj[a] = 'copy-of-a';obj[b] = 'copy-of-b';console.log(Object.keys(obj).length); // ?console.log(obj[a]); // ?- Answer:
1, then'copy-of-b'— not two separate entries. - Plain object property keys are always coerced to strings. Every plain object stringifies to the exact same value,
"[object Object]"— soobj[a]andobj[b]are actually the same key, and settingobj[b]silently overwrites whateverobj[a]held. Mapkeys use the object’s real reference identity, not a stringified form — socopyMap.set(a, ...)andcopyMap.set(b, ...)are always distinct entries, no matter how similar (or identical) the objects’ own properties look.- ⚠ Trap: for this exact problem, a plain object wouldn’t just be slower — it would be silently wrong. Every original node would collide onto the same
"[object Object]"key, socopyMap.get(cur)in pass 2 would return whichever copy was created last, for every single node — wiring every copy’snext/randomto the same wrong node instead of its actual counterpart. This only surfaces with node-object keys; a plain object is perfectly fine for keys that are already meant to be strings or numbers.