Problem overview
Each ListNode has a val, a next pointer and a random pointer that can point at any node of the list or at nothing. The function copyRandomList receives the head and returns the head of a deep copy: n brand-new nodes with the same values, where every pointer of the copy leads to a node of the copy and none leads back into the original. The original list must be left as it was.
In the statement's notation, [[7,null],[13,0],[11,4],[10,2],[1,0]] describes five nodes. The node 13 has its random pointer on index 0, the node 7. The copy must have the same shape. An empty list gives null.
Approach: weave each copy into the list
The usual solution keeps a hash map from each original node to its copy, which costs O(n) extra memory. This lesson does without the map by using the list itself to remember which copy belongs to which original. The idea has three passes:
- Interleave. Put a copy of every node directly after the node it copies. The list
A -> B -> CbecomesA -> A' -> B -> B' -> C -> C'. - Set the random pointers. If
A.randomisX, the copy ofXis simplyX.next, soA'.random = A.random.next. No map is needed. - Separate. Unweave the two lists, restoring the original and building the copy's own
nextchain.
The new nodes only ever need a position, not a lookup, so the extra memory is just the copy itself.
Step by step
- Handle the empty list. If
headisnull, returnnull. Later steps readhead.next, which would fail otherwise. - Declare
var node: ListNode? = headwith the type written out. After the guard,headis known to be non-null, butnodewill holdnullonce the walk passes the last node, so the variable is declared as nullable. - First pass: loop while
nodeis notnull. - Save the successor in
nextNode, because inserting the copy overwritesnode.next. - Create
copyNodewith the value ofnode. The random pointer isn't set yet. - Splice it in:
node.next = copyNode, thencopyNode.next = nextNode. - Jump to
nextNode, not tonode.next. The latter is the copy just inserted, and the loop would keep making copies of copies forever. - Reset
node = headfor the second pass. - Second pass: loop over the originals. Each
node.nextis its copy, so take it ascopyNode. - If
node.randomis set, assigncopyNode.random = node.random.next, the copy of the target. - Jump over the copy:
node = copyNode.next, the next original. - Keep
newHead = head.nextbefore the third pass, since that pass rewireshead.next. - Third pass: reset
node = head, then for each original takecopyNode = node.nextandnextNode = copyNode.next(the next original) before rewiring anything. - Link the copies: if
nextNodeexists,copyNode.next = nextNode.next, the copy that follows it. - Restore the original:
node.next = nextNode, then advancenode = nextNode. - Return
newHead.
The solution in Kotlin
class Solution {
fun copyRandomList(head: ListNode?): ListNode? {
if (head == null) {
return null
}
var node: ListNode? = head
while (node != null) {
val nextNode = node.next
val copyNode = ListNode(node.`val`)
node.next = copyNode
copyNode.next = nextNode
node = nextNode
}
node = head
while (node != null) {
val copyNode = node.next
if (node.random != null) {
copyNode!!.random = node.random!!.next
}
node = copyNode!!.next
}
val newHead = head.next
node = head
while (node != null) {
val copyNode = node.next
val nextNode = copyNode!!.next
if (nextNode != null) {
copyNode.next = nextNode.next
}
node.next = nextNode
node = nextNode
}
return newHead
}
}
The Kotlin details worth knowing:
- The declared type
ListNode?. It states thatnodecan benull, which is how every loop here ends. Kotlin 2.1 would infer the same type fromhead, but spelling it out removes any doubt about the smart cast that follows the guard. - Mutable properties aren't smart-cast.
node.randomis avarproperty, so afternode.random != nullthe compiler still treats it as nullable, andnode.random!!.nextneeds the assertion. A localvalsuch asnextNodeis smart-cast after its own check. copyNode!!.node.nextis typed as nullable, but after the first pass every original has a copy after it, so the assertion holds. AftercopyNode!!.nextsucceeds once, Kotlin smart-casts the localval copyNodeto non-null for the rest of that iteration.- Several
val copyNodedeclarations. Each is local to its own loop body, so the same name can be reused in all three loops. - Backticks for
val. The field is writtennode.`val`.
Complexity
Time: O(n). Each of the three passes visits each node a constant number of times.
Space: O(1) extra, apart from the n nodes of the copy, which the output requires. There is no map, no recursion and no per-node storage beyond a few pointers.
Edge cases and common mistakes
- Empty list. The guard returns
nullimmediately. - Single node pointing at itself. For
[[1,0]], the node's random is the node itself, so the copy's random isnode.next, which is the copy. The result is a one-node list whose random points at itself. - A null random. The check on
node.randomleaves the copy's pointer asnull, instead of calling.nextonnull. - Not restoring the original. If the third pass doesn't reset
node.next, the caller's list stays interleaved with the copies. - Reading
head.nextafter the third pass. It has been restored to the original second node, so takenewHeadbefore. - Doing the random pass and the split in one pass. Splitting early would remove copies that later random pointers still need to find.
Testing
[[7,null],[13,0],[11,4],[10,2],[1,0]]gives the same pairs, made of new nodes, with the original list unchanged.[[1,1],[2,1]]gives[[1,1],[2,1]].[[3,null],[3,0],[3,null]]gives the same pairs.- An empty list gives
null.
