Facebook Pixel

138. Copy List with Random Pointer

MediumHash TableLinked List
Leetcode Link

Problem Description

You are given a linked list where each node has two pointers:

  • A next pointer that points to the next node in the list (or null if it's the last node)
  • A random pointer that can point to any node in the list (or null)

Your task is to create a deep copy of this linked list. A deep copy means:

  • Create n brand new nodes (where n is the length of the original list)
  • Each new node should have the same value as its corresponding original node
  • The next and random pointers in the copied list should maintain the same structure as the original list, but must point to the new copied nodes, not the original ones

For example, if in the original list node X has X.random pointing to node Y, then in the copied list, the corresponding node x should have x.random pointing to the corresponding copied node y.

The solution uses a hash table approach:

  1. First pass: Traverse the original list, create a copy of each node, and store the mapping between original nodes and their copies in a hash table d. During this pass, also connect the next pointers of the copied nodes.
  2. Second pass: Traverse the original list again and use the hash table to set up the random pointers for the copied nodes. For each original node, find its copy in the hash table, then set the copy's random pointer to point to the copy of the original node's random target.

The function returns the head of the newly created deep copy of the linked list.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The main challenge in this problem is handling the random pointers. When we're creating a copy of a node, its random pointer might reference a node that we haven't created yet, or it might reference a node we've already passed.

Consider what happens if we try to copy the list in a single pass: When we copy node A, and A's random pointer points to node C (which comes later in the list), we can't set up this connection yet because node C's copy doesn't exist. Similarly, if A's random pointer points backwards to a previously copied node, we need a way to find that copied node.

This naturally leads us to think about maintaining a mapping between original nodes and their copies. A hash table is perfect for this - it gives us O(1) access to find any copied node given its original.

The solution becomes clear: we need two passes through the list:

  1. First pass: Create all the new nodes and establish the mapping. While we're at it, we can connect the next pointers since they follow a sequential pattern.
  2. Second pass: Now that all nodes are created and we have our mapping, we can properly set up the random pointers. For each original node, we look up its copy, then set the copy's random pointer to point to the copy of wherever the original's random points.

The key insight is that we must separate node creation from random pointer assignment. The hash table serves as the bridge between the original and copied lists, allowing us to translate any reference from the original list to its corresponding reference in the copied list.

Solution Approach

The implementation uses a hash table to maintain the mapping between original nodes and their copies. Here's the step-by-step walkthrough:

Initialization:

  • Create a dummy head node dummy to simplify list construction
  • Use a tail pointer initialized to dummy to track where to append new nodes
  • Initialize an empty dictionary d to store the mapping between original and copied nodes
  • Set cur pointer to the original list's head

First Pass - Node Creation and Next Pointer Setup:

while cur:
    node = Node(cur.val)      # Create a new node with same value
    tail.next = node           # Connect to the previous copied node
    tail = tail.next           # Move tail to the newly created node
    d[cur] = node             # Store mapping: original -> copy
    cur = cur.next            # Move to next original node

During this pass:

  • We traverse the original list once
  • For each original node, we create a corresponding new node with the same value
  • We build the next pointer chain for the copied list
  • We populate the hash table d where keys are original nodes and values are their copies

Second Pass - Random Pointer Setup:

cur = head                    # Reset to start of original list
while cur:
    d[cur].random = d[cur.random] if cur.random else None
    cur = cur.next

During this pass:

  • We traverse the original list again
  • For each original node, we access its copy via d[cur]
  • We set the copy's random pointer by looking up where the original's random points
  • If cur.random exists, we use d[cur.random] to get the corresponding copied node
  • If cur.random is None, we set the copy's random to None as well

Return the Result:

  • Return dummy.next, which is the head of the deep copied list

The time complexity is O(n) where n is the number of nodes (two passes through the list), and the space complexity is O(n) for the hash table storing the node mappings.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with 3 nodes to illustrate the solution approach.

Original List:

Node 1 (val=7) -> Node 2 (val=13) -> Node 3 (val=11) -> null
Random pointers:
- Node 1.random -> null
- Node 2.random -> Node 1
- Node 3.random -> Node 2

Step-by-step execution:

Initialization:

  • Create dummy node (helper node for building the copy)
  • tail = dummy (points to where we'll append next)
  • d = {} (empty hash table)
  • cur = head (points to Node 1)

First Pass - Creating nodes and setting next pointers:

Iteration 1: cur points to Node 1 (val=7)

  • Create new Node 1' with val=7
  • tail.next = Node 1' (dummy -> Node 1')
  • tail = Node 1'
  • d[Node 1] = Node 1' (store mapping)
  • cur = cur.next (move to Node 2)

Iteration 2: cur points to Node 2 (val=13)

  • Create new Node 2' with val=13
  • tail.next = Node 2' (Node 1' -> Node 2')
  • tail = Node 2'
  • d[Node 2] = Node 2' (store mapping)
  • cur = cur.next (move to Node 3)

Iteration 3: cur points to Node 3 (val=11)

  • Create new Node 3' with val=11
  • tail.next = Node 3' (Node 2' -> Node 3')
  • tail = Node 3'
  • d[Node 3] = Node 3' (store mapping)
  • cur = cur.next (becomes null, exit loop)

After First Pass:

  • Copied list structure: Node 1' -> Node 2' -> Node 3' -> null
  • Hash table d:
    {
      Node 1 -> Node 1',
      Node 2 -> Node 2',
      Node 3 -> Node 3'
    }

Second Pass - Setting random pointers:

Iteration 1: cur points to Node 1

  • cur.random is null
  • Set d[Node 1].random = None (Node 1'.random = null)
  • cur = cur.next (move to Node 2)

Iteration 2: cur points to Node 2

  • cur.random points to Node 1
  • Set d[Node 2].random = d[Node 1] (Node 2'.random = Node 1')
  • cur = cur.next (move to Node 3)

Iteration 3: cur points to Node 3

  • cur.random points to Node 2
  • Set d[Node 3].random = d[Node 2] (Node 3'.random = Node 2')
  • cur = cur.next (becomes null, exit loop)

Final Result: The deep copied list has the structure:

Node 1' (val=7) -> Node 2' (val=13) -> Node 3' (val=11) -> null
Random pointers:
- Node 1'.random -> null
- Node 2'.random -> Node 1'
- Node 3'.random -> Node 2'

The function returns dummy.next, which is Node 1', the head of our deep copied list. Notice how all the pointers in the copied list point to copied nodes, not the original ones, achieving a true deep copy.

Solution Implementation

1"""
2# Definition for a Node.
3class Node:
4    def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
5        self.val = int(x)
6        self.next = next
7        self.random = random
8"""
9
10from typing import Optional
11
12
13class Solution:
14    def copyRandomList(self, head: Optional['Node']) -> Optional['Node']:
15        # Handle edge case: empty list
16        if not head:
17            return None
18      
19        # Dictionary to map original nodes to their corresponding copied nodes
20        original_to_copy = {}
21      
22        # Create dummy node to simplify list construction
23        dummy = Node(0)
24        tail = dummy
25      
26        # First pass: create all nodes and build the next pointers
27        current = head
28        while current:
29            # Create a new node with the same value
30            new_node = Node(current.val)
31          
32            # Link the new node to the copied list
33            tail.next = new_node
34            tail = tail.next
35          
36            # Store mapping from original node to copied node
37            original_to_copy[current] = new_node
38          
39            # Move to next node in original list
40            current = current.next
41      
42        # Second pass: set up the random pointers using the mapping
43        current = head
44        while current:
45            # Set random pointer of copied node to the copied version of the random node
46            if current.random:
47                original_to_copy[current].random = original_to_copy[current.random]
48            else:
49                original_to_copy[current].random = None
50          
51            # Move to next node in original list
52            current = current.next
53      
54        # Return the head of the copied list (skip dummy node)
55        return dummy.next
56
1/*
2// Definition for a Node.
3class Node {
4    int val;
5    Node next;
6    Node random;
7
8    public Node(int val) {
9        this.val = val;
10        this.next = null;
11        this.random = null;
12    }
13}
14*/
15
16class Solution {
17    public Node copyRandomList(Node head) {
18        // HashMap to store mapping from original nodes to copied nodes
19        Map<Node, Node> oldToNewMap = new HashMap<>();
20      
21        // Create a dummy node to simplify list construction
22        Node dummyHead = new Node(0);
23        Node currentTail = dummyHead;
24      
25        // First pass: Create all nodes and build the next pointers
26        // Also populate the mapping from original to copied nodes
27        for (Node currentOriginal = head; currentOriginal != null; currentOriginal = currentOriginal.next) {
28            // Create a new node with the same value as the original
29            Node newNode = new Node(currentOriginal.val);
30          
31            // Connect the new node to the copied list
32            currentTail.next = newNode;
33            currentTail = newNode;
34          
35            // Store the mapping from original node to copied node
36            oldToNewMap.put(currentOriginal, newNode);
37        }
38      
39        // Second pass: Set the random pointers for all copied nodes
40        for (Node currentOriginal = head; currentOriginal != null; currentOriginal = currentOriginal.next) {
41            // Get the corresponding copied node
42            Node copiedNode = oldToNewMap.get(currentOriginal);
43          
44            // Set the random pointer of the copied node
45            // If original's random is null, set to null; otherwise, get the corresponding copied node
46            copiedNode.random = (currentOriginal.random == null) ? null : oldToNewMap.get(currentOriginal.random);
47        }
48      
49        // Return the head of the copied list (skip dummy node)
50        return dummyHead.next;
51    }
52}
53
1/*
2// Definition for a Node.
3class Node {
4public:
5    int val;
6    Node* next;
7    Node* random;
8
9    Node(int _val) {
10        val = _val;
11        next = NULL;
12        random = NULL;
13    }
14};
15*/
16
17class Solution {
18public:
19    Node* copyRandomList(Node* head) {
20        // Create a dummy head for the new linked list
21        Node* dummyHead = new Node(0);
22        Node* currentNewNode = dummyHead;
23      
24        // Hash map to store mapping from original nodes to their copies
25        unordered_map<Node*, Node*> originalToCopy;
26      
27        // First pass: Create all nodes and establish next pointers
28        for (Node* currentOriginal = head; currentOriginal != nullptr; currentOriginal = currentOriginal->next) {
29            // Create a copy of the current node
30            Node* copiedNode = new Node(currentOriginal->val);
31          
32            // Link the new node to the new list
33            currentNewNode->next = copiedNode;
34            currentNewNode = copiedNode;
35          
36            // Store the mapping from original to copy
37            originalToCopy[currentOriginal] = copiedNode;
38        }
39      
40        // Second pass: Set up random pointers using the mapping
41        for (Node* currentOriginal = head; currentOriginal != nullptr; currentOriginal = currentOriginal->next) {
42            // If original node has a random pointer, set the corresponding random pointer in the copy
43            if (currentOriginal->random != nullptr) {
44                originalToCopy[currentOriginal]->random = originalToCopy[currentOriginal->random];
45            } else {
46                originalToCopy[currentOriginal]->random = nullptr;
47            }
48        }
49      
50        // Return the head of the copied list (skip dummy node)
51        return dummyHead->next;
52    }
53};
54
1/**
2 * Definition for _Node.
3 * class _Node {
4 *     val: number
5 *     next: _Node | null
6 *     random: _Node | null
7 *
8 *     constructor(val?: number, next?: _Node, random?: _Node) {
9 *         this.val = (val===undefined ? 0 : val)
10 *         this.next = (next===undefined ? null : next)
11 *         this.random = (random===undefined ? null : random)
12 *     }
13 * }
14 */
15
16/**
17 * Creates a deep copy of a linked list with random pointers
18 * @param head - The head node of the original linked list
19 * @returns The head node of the copied linked list
20 */
21function copyRandomList(head: _Node | null): _Node | null {
22    // Early return if the list is empty
23    if (!head) {
24        return null;
25    }
26  
27    // Map to store the correspondence between original nodes and copied nodes
28    const nodeMapping: Map<_Node, _Node> = new Map();
29  
30    // Create a dummy node to simplify list construction
31    const dummyHead: _Node = new _Node();
32    let currentTail: _Node = dummyHead;
33  
34    // First pass: Create all nodes and build the next pointer chain
35    for (let currentNode: _Node | null = head; currentNode !== null; currentNode = currentNode.next) {
36        // Create a new node with the same value
37        const copiedNode: _Node = new _Node(currentNode.val);
38      
39        // Link the new node to the copied list
40        currentTail.next = copiedNode;
41        currentTail = copiedNode;
42      
43        // Store the mapping between original and copied nodes
44        nodeMapping.set(currentNode, copiedNode);
45    }
46  
47    // Second pass: Set up the random pointers using the mapping
48    for (let currentNode: _Node | null = head; currentNode !== null; currentNode = currentNode.next) {
49        // Get the corresponding copied node
50        const copiedNode: _Node = nodeMapping.get(currentNode)!;
51      
52        // Set the random pointer of the copied node
53        if (currentNode.random !== null) {
54            copiedNode.random = nodeMapping.get(currentNode.random)!;
55        } else {
56            copiedNode.random = null;
57        }
58    }
59  
60    // Return the head of the copied list (skip dummy node)
61    return dummyHead.next;
62}
63

Time and Space Complexity

Time Complexity: O(n)

The algorithm consists of two sequential while loops:

  • The first loop iterates through all n nodes in the original linked list to create new nodes and build the mapping dictionary. Each operation inside this loop (creating a node, updating pointers, adding to dictionary) takes O(1) time.
  • The second loop iterates through all n nodes again to set the random pointers using the dictionary lookup, which is O(1) per operation.

Therefore, the total time complexity is O(n) + O(n) = O(n).

Space Complexity: O(n)

The algorithm uses additional space for:

  • A dictionary d that stores mappings from original nodes to copied nodes, containing n key-value pairs: O(n)
  • The new linked list with n nodes that are being created as the deep copy: O(n)
  • A few constant auxiliary variables (dummy, tail, cur, node): O(1)

The space for the output linked list is typically considered part of the space complexity for this problem, so the total space complexity is O(n).

Common Pitfalls

1. Attempting to Set Random Pointers Before Creating All Nodes

A common mistake is trying to set up the random pointers while creating the nodes in a single pass. This fails because a node's random pointer might point to a node that hasn't been created yet.

Incorrect Approach:

def copyRandomList(self, head: Optional['Node']) -> Optional['Node']:
    if not head:
        return None
  
    original_to_copy = {}
    dummy = Node(0)
    tail = dummy
    current = head
  
    while current:
        new_node = Node(current.val)
        tail.next = new_node
        tail = tail.next
        original_to_copy[current] = new_node
      
        # WRONG: Trying to set random pointer immediately
        # This fails if current.random hasn't been copied yet
        if current.random:
            new_node.random = original_to_copy[current.random]  # KeyError!
      
        current = current.next
  
    return dummy.next

Solution: Always use two separate passes - first create all nodes and establish the mapping, then set up the random pointers.

2. Not Handling None Values in the Hash Table Lookup

Another pitfall is forgetting that the random pointer can be None, and trying to look up None in the dictionary will cause a KeyError.

Incorrect Approach:

# Second pass - WRONG
current = head
while current:
    # This fails when cur.random is None
    original_to_copy[current].random = original_to_copy[current.random]
    current = current.next

Solution: Always check if the random pointer is None before attempting the dictionary lookup:

# Correct approach
if current.random:
    original_to_copy[current].random = original_to_copy[current.random]
else:
    original_to_copy[current].random = None

3. Modifying the Original List

Some might try to optimize by temporarily modifying the original list structure (like interweaving copied nodes with original nodes). This violates the requirement of keeping the original list intact.

Incorrect Approach:

# Trying to insert copied nodes between original nodes
current = head
while current:
    copy = Node(current.val)
    copy.next = current.next  # Modifying original structure
    current.next = copy
    current = copy.next

Solution: Use a separate data structure (hash table) to maintain the mapping without modifying the original list.

4. Forgetting to Return the Correct Head

When using a dummy node, forgetting to return dummy.next instead of dummy itself is a common mistake that would include the dummy node in the result.

Solution: Always return dummy.next to skip the dummy node and return the actual head of the copied list.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]:
2    import heapq
3    heapq.heapify(arr)
4    res = []
5    for i in range(3):
6        res.append(heapq.heappop(arr))
7    return res
8
1public static int[] fun(int[] arr) {
2    int[] res = new int[3];
3    PriorityQueue<Integer> heap = new PriorityQueue<>();
4    for (int i = 0; i < arr.length; i++) {
5        heap.add(arr[i]);
6    }
7    for (int i = 0; i < 3; i++) {
8        res[i] = heap.poll();
9    }
10    return res;
11}
12
1class HeapItem {
2    constructor(item, priority = item) {
3        this.item = item;
4        this.priority = priority;
5    }
6}
7
8class MinHeap {
9    constructor() {
10        this.heap = [];
11    }
12
13    push(node) {
14        // insert the new node at the end of the heap array
15        this.heap.push(node);
16        // find the correct position for the new node
17        this.bubble_up();
18    }
19
20    bubble_up() {
21        let index = this.heap.length - 1;
22
23        while (index > 0) {
24            const element = this.heap[index];
25            const parentIndex = Math.floor((index - 1) / 2);
26            const parent = this.heap[parentIndex];
27
28            if (parent.priority <= element.priority) break;
29            // if the parent is bigger than the child then swap the parent and child
30            this.heap[index] = parent;
31            this.heap[parentIndex] = element;
32            index = parentIndex;
33        }
34    }
35
36    pop() {
37        const min = this.heap[0];
38        this.heap[0] = this.heap[this.size() - 1];
39        this.heap.pop();
40        this.bubble_down();
41        return min;
42    }
43
44    bubble_down() {
45        let index = 0;
46        let min = index;
47        const n = this.heap.length;
48
49        while (index < n) {
50            const left = 2 * index + 1;
51            const right = left + 1;
52
53            if (left < n && this.heap[left].priority < this.heap[min].priority) {
54                min = left;
55            }
56            if (right < n && this.heap[right].priority < this.heap[min].priority) {
57                min = right;
58            }
59            if (min === index) break;
60            [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61            index = min;
62        }
63    }
64
65    peek() {
66        return this.heap[0];
67    }
68
69    size() {
70        return this.heap.length;
71    }
72}
73
74function fun(arr) {
75    const heap = new MinHeap();
76    for (const x of arr) {
77        heap.push(new HeapItem(x));
78    }
79    const res = [];
80    for (let i = 0; i < 3; i++) {
81        res.push(heap.pop().item);
82    }
83    return res;
84}
85

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More