Problem overview
Each node of the list has a val, a next pointer and a random pointer. The random pointer can point at any node of the same list, including the node itself, or at null. The method copyRandomList receives the head and must return the head of a deep copy: exactly n brand new nodes with the same values, where both next and random of the new nodes point to new nodes. Nothing in the copy may point back into the original list.
The input is written as pairs [val, random_index]. In [[1, 1], [2, 1]], the first node has the value 1 and its random pointer goes to index 1, the second node. The second node's random pointer also goes to index 1, so it points at itself. The copy has the same shape, built from new nodes. An empty list gives null.
Approach: a map from each original to its copy
Copying the values and the next chain is easy: walk the list and build new nodes one by one. The hard part is random. When you create a copy of node A, its random target may be a node further down the list, one that has no copy yet. And a random pointer may also point backwards, to a node you copied earlier, but you have lost track of which copy that was.
The standard answer is to split the job in two passes with a lookup table from each original node to its copy:
- Pass one creates one copy per original node, carrying only the value, and records the pair in the table.
- Pass two visits every original node again. All copies now exist, so for any original
node, the copy ofnode.nextand the copy ofnode.randomare one lookup away. Use them to wire the copy.
The table needs the node objects themselves as keys. That is what a Map allows. A plain object would not work here: object keys are turned into strings, and every node becomes "[object Object]", so all nodes would share one entry.
This takes O(n) time and O(n) extra space, which is what the statement allows. The idea is easy to get right, and a later lesson on the same problem removes the table.
Step by step
- Create an empty
Mapnamedcopies. Its keys are original nodes and its values are their copies. - Declare the walking variable
nodewithletand start it athead. The head itself stays untouched, because the second pass and the return both need it. - Loop while
nodeis truthy for the first pass, so every original node is visited. - Store a new
ListNodebuilt fromnode.valin the map, keyed bynode. Only the value is copied now. The copy's links come later, when every copy exists. - Advance
nodetonode.next. Without this the loop never ends. - Reset
nodetohead, since the first loop left it atnull. - Start a second loop with the same condition. Now every original has a copy in the map.
- Wire
next: set the copy'snextto the copy ofnode.next. For the last node,node.nextisnull, which has no entry, sogetreturnsundefinedand|| nullturns that intonull. - Wire
randomthe same way, withnode.randomas the key. - Advance
nodealong the original list'snextlinks. The copies' links are what this loop is building. - Return
copies.get(head) || null. For an empty list,headisnull, the lookup givesundefined, and the fallback makes the answernull.
The solution in TypeScript
class Solution {
copyRandomList(head: ListNode | null): ListNode | null {
let copies = new Map<ListNode, ListNode>();
let node = head;
while (node) {
copies.set(node, new ListNode(node.val));
node = node.next;
}
node = head;
while (node) {
copies.get(node).next = copies.get(node.next) || null;
copies.get(node).random = copies.get(node.random) || null;
node = node.next;
}
return copies.get(head) || null;
}
}
The TypeScript details worth knowing:
- Type arguments.
new Map<ListNode, ListNode>()says the keys are nodes and the values are nodes. An empty map gives the compiler nothing to infer from, so the arguments are written out. They are checked by the compiler only. When TypeScript compiles to JavaScript they are erased, and the map at run time is an ordinaryMap. getand missing keys.Map.prototype.getreturnsundefinedwhen the key is absent. Looking upnullis allowed and just finds nothing. The pointers in the copy must benull, notundefined, so the code adds|| null. The expression?? nullworks too and says "only forundefinedornull".- Keys are compared by identity. Two different nodes holding the same value are two different keys. This matters for
[[3, null], [3, 0], [3, null]], where all three nodes have the value 3 but get three separate copies. - Reassigning
node.let node = headgivesnodethe typeListNode | null. After the first loop it isnull, which is why it has to be reset tohead.
Complexity
Time: O(n). Each of the two passes visits every node once. Map operations with object keys take constant time on average.
Space: O(n). The map holds one entry per node. The copy itself is also n nodes, but that is the required output, not extra working space.
Edge cases and common mistakes
- An empty list.
headisnull, both loops are skipped, and the method returnsnull. - A random pointer to itself. In
[[1, 1], [2, 1]], the second node's copy points at itself, not at the original second node. The map lookup does that correctly. - Random pointers going backwards or forwards. Both are handled, because the second pass starts only after every copy exists.
- Equal values. The map is keyed by node, not by value. A table keyed by
valwould merge the three nodes in[[3, null], [3, 0], [3, null]]. - A plain object as the table. Every node turns into the same string key, so the entries overwrite one another.
- Pointing into the original. Writing
node.randomdirectly into the copy would satisfy a value check but would break the rule that no pointer of the copy leads to the original list. - Returning a copy with
undefinedlinks. Skipping|| nullwould leaveundefinedin the last node'snextand in every nullrandom.
Testing
[[7, null], [13, 0], [11, 4], [10, 2], [1, 0]]gives the same shape, with all new nodes.[[1, 1], [2, 1]]gives[[1, 1], [2, 1]].[[3, null], [3, 0], [3, null]]gives[[3, null], [3, 0], [3, null]].- An empty list gives
null.
