See all coding puzzles and patterns here.

Problem Definition

Given a linked list where each node contains a next pointer and an arbitrary pointer to any other node, write a function to deep copy the linked list.


Node Class

Below is the Node class that will represent elements in the linked list.

class Node:
    def __init__(self, value: int, next: 'Node' = None, random: 'Node' = None):
        self.value = value
        self.next = next
        self.random = random

Examples

Some test cases to verify our solution.

five = Node(5)
four = Node(4, next=five)
three = Node(3, next=four)
two = Node(2, next=three)
one = Node(1, next=two)
zero = Node(0, next=one)

five.random = one
four.random = zero
three.random = five
two.random = four
one.random = zero

exp = [(0, 1, None), (1, 2, 0), (2, 3, 4), (3, 4, 5), (4, 5, 0), (5, None, 1)]
head = deep_copy(zero)
res = []
while head is not None:
    next = head.next.value if head.next is not None else None
    random = head.random.value if head.random is not None else None
    res.append((head.value, next, random))
    head = head.next
assert res == exp

Dictionary Solution

We will build the copy linked list and maintain 2 dictionaries, one mapping the original item to its copy and one mapping the copy of an item to the item's random pointer. We will then use these 2 maps to fill in the random pointers for the items in the copy linked list.

def deep_copy(root: Node) -> Node:
    if root is None:
        return None
    head = root
    new_head = Node(head.value)
    prev = new_head
    copy_to_rand = {hash(new_head): hash(head.random)}
    orig_to_copy = {hash(head): new_head}
    while head.next is not None:
        head = head.next
        new_node = Node(head.value)
        prev.next = new_node
        copy_to_rand[hash(new_node)] = hash(head.random)
        orig_to_copy[hash(head)] = new_node
        prev = new_node
    head = new_head
    while head is not None:
        head.random = orig_to_copy.get(copy_to_rand[hash(head)], None)
        head = head.next
    return new_head

Dictionary Solution Complexity

Let $n = $ the number of nodes in the original linked list.

Runtime complexity should be $\Theta(n)$ since we are traversing through the list and its copy once each.

Auxiliary space complexity should be $\Theta(n)$ since we are storing 2 dictionaries of size $n$.


Interleaving Pointers Solution

We can reduce auxiliary space complexity by removing the need for the dictionaries.

We will interleave the originals and copies on our first loop. In our second loop, we'll use the random pointers on the originals to set the random pointers on the copies. In our third loop, we'll separate the original and copy linked lists.

def deep_copy(root: Node) -> Node:
    if root is None:
        return None
    ans = Node(value=root.value)
    curr = root.next
    root.next = ans
    ans.next = curr
    while curr is not None:
        copy = Node(value=curr.value)
        tmp = curr.next
        curr.next = copy
        copy.next = tmp
        curr = tmp
    curr = root
    while curr is not None:
        if curr.random is not None:
            curr.next.random = curr.random.next
        curr = curr.next.next
    curr = root
    prev_copy = None
    while curr is not None:
        curr_copy = curr.next
        curr.next = curr.next.next
        if prev_copy is not None:
            prev_copy.next = curr_copy
        curr = curr.next
    return ans

Interleaving Pointers Solution Complexity

Let $n = $ the number of nodes in the linked list.

Runtime complexity should be $\Theta(n)$.

Auxiliary space complexity should be $O(1)$.