Problem overview
Each ListNode in the input has a val, a next pointer and an extra random pointer, which may point at any node of the list or be null. The function copyRandomList receives the head and must return the head of a deep copy: exactly n new nodes with the same values, where every next and random of the copy points to a node of the copy, never to a node of the original.
The statement writes a list as pairs [val, random_index]. In [[7,null],[13,0],[11,4],[10,2],[1,0]], the node 13 has its random pointer on node index 0, which is the node 7. The copy must have the same shape: its second node's random must be the copy's first node. An empty list gives null.
The statement allows O(n) time and O(n) extra space.
Approach: map every original node to its copy
Copying the values and the next chain is easy. The difficulty is the random pointers: when you create a copy, the node its random pointer targets might not be copied yet, and it may be far away from the current node. You need a way to ask, for any original node, "which node is its copy?"
A hash map answers that question in constant time. The plan has two passes:
- Walk the list and create a copy of each node, with only its value set. Store the pair original to copy in the map.
- Walk the list again. Now every original has a copy, so for each node you can look up the copy of
nextand the copy ofrandomand wire them into the node's own copy.
Splitting the work into two passes removes the ordering problem: by the second pass, every lookup succeeds.
Step by step
- Create the map as a
val copiesof typeHashMap<ListNode?, ListNode?>. The map itself is never reassigned, but its contents change. Making the key type nullable meanscopies[node.next]compiles whennode.nextisnull, and it simply returnsnull. - Start a walker as
var node = head, so thatheadis still available for the second pass and the return. - Loop while
nodeis notnull. - Store a new node under the key
node, built withListNode(node.`val`). Only the value is copied now. The targets ofnextandrandommay not have copies yet. - Advance with
node = node.next. Without it the loop never ends. - Reset with
node = head, because the first loop left the walker at the end. - Loop again over the originals.
- Wire
next:copies[node]!!.next = copies[node.next]. The copy of the current node is found withcopies[node]!!, and the copy of its successor withcopies[node.next]. For the last node that lookup givesnull, which ends the copied chain. - Wire
randomthe same way, withcopies[node.random]. - Advance along the original list.
- Return
copies[head], the copy of the head. For an empty list it isnull.
The solution in Kotlin
class Solution {
fun copyRandomList(head: ListNode?): ListNode? {
val copies = HashMap<ListNode?, ListNode?>()
var node = head
while (node != null) {
copies[node] = ListNode(node.`val`)
node = node.next
}
node = head
while (node != null) {
copies[node]!!.next = copies[node.next]
copies[node]!!.random = copies[node.random]
node = node.next
}
return copies[head]
}
}
The Kotlin details worth knowing:
- Keys compare by identity here. A
HashMapusesequalsandhashCodeof its keys. A plain class that doesn't override them inherits the versions that compare by reference, so two nodes with equal values are still different keys. IfListNodewere adata class, whoseequalscompares fields, nodes with equal contents could be treated as the same key. map[key]returns a nullable. Reading from a map givesV?, which isnullfor a missing key. Because the value type is alreadyListNode?, the result ofcopies[node.next]can be assigned tonext, which is alsoListNode?, without any conversion.!!aftercopies[node]. The lookup returns a nullable, and the compiler can't know that every original was stored in the first pass.!!asserts it; if the value werenull, it would throw aNullPointerException. Here it never is.- Backticks for
val. The field is named like a keyword, so it is writtennode.`val`. - Smart cast. Inside
while (node != null), the localvar nodecounts as non-null, sonode.nextandnode.randomneed no?.. - Reusing
node. The variable is inferred asListNode?fromhead, so assigningnode = headfor the second pass is allowed.
Complexity
Time: O(n). There are two passes over the list. Each step does a constant number of hash map operations, which take constant time on average.
Space: O(n). The map holds one entry per node, and the copy itself has n new nodes. Both are linear.
Edge cases and common mistakes
- Empty list. The loops never run and
copies[head]isnull, so the function returnsnull. - Random pointer to itself or to an earlier node. Both are fine, because all copies exist before any pointer is wired.
[[1,1],[2,1]]gives the same shape back, with both random pointers on the second node. - Equal values. In
[[3,null],[3,0],[3,null]]every node holds 3. The map must be keyed by the node, not by its value, or the copies would be confused. - Linking to the original. Assigning
node.nextornode.randomto the copy would make the new list point into the old one, which the statement forbids. - Wiring in the first pass. The target of a pointer might not have a copy yet, so the lookup would give
nullfor a pointer that should be set. - Forgetting to reset
node. After the first loop the walker isnull, so the second loop would do nothing and every copy would havenullpointers.
Testing
[[7,null],[13,0],[11,4],[10,2],[1,0]]gives a list with the same pairs, made of new nodes.[[1,1],[2,1]]gives[[1,1],[2,1]].[[3,null],[3,0],[3,null]]gives the same pairs.- An empty list gives
null.
