235 lines
11 KiB
YAML
235 lines
11 KiB
YAML
title: Copy List with Random Pointer
|
|
slug: copy-list-with-random-pointer
|
|
difficulty: medium
|
|
leetcode_id: 138
|
|
leetcode_url: https://leetcode.com/problems/copy-list-with-random-pointer/
|
|
categories:
|
|
- linked-lists
|
|
- hash-tables
|
|
patterns:
|
|
- linkedlist-reversal
|
|
|
|
function_signature: "def copy_random_list(head: Node | None) -> Node | None:"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { head: [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]] }
|
|
expected: [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]]
|
|
- input: { head: [[1, 1], [2, 1]] }
|
|
expected: [[1, 1], [2, 1]]
|
|
- input: { head: [[3, null], [3, 0], [3, null]] }
|
|
expected: [[3, null], [3, 0], [3, null]]
|
|
hidden:
|
|
- input: { head: [] }
|
|
expected: []
|
|
- input: { head: [[1, null]] }
|
|
expected: [[1, null]]
|
|
- input: { head: [[5, 0]] }
|
|
expected: [[5, 0]]
|
|
- input: { head: [[1, 1], [2, 0]] }
|
|
expected: [[1, 1], [2, 0]]
|
|
- input: { head: [[1, null], [2, null], [3, null], [4, null], [5, null]] }
|
|
expected: [[1, null], [2, null], [3, null], [4, null], [5, null]]
|
|
- input: { head: [[-1, 0], [0, 1], [1, 0]] }
|
|
expected: [[-1, 0], [0, 1], [1, 0]]
|
|
|
|
description: |
|
|
A linked list of length `n` is given such that each node contains an additional random pointer, which could point to any node in the list, or `null`.
|
|
|
|
Construct a **deep copy** of the list. The deep copy should consist of exactly `n` **brand new** nodes, where each new node has its value set to the value of its corresponding original node. Both the `next` and `random` pointer of the new nodes should point to new nodes in the copied list such that the pointers in the original list and copied list represent the same list state. **None of the pointers in the new list should point to nodes in the original list**.
|
|
|
|
For example, if there are two nodes `X` and `Y` in the original list, where `X.random --> Y`, then for the corresponding two nodes `x` and `y` in the copied list, `x.random --> y`.
|
|
|
|
Return *the head of the copied linked list*.
|
|
|
|
The linked list is represented in the input/output as a list of `n` nodes. Each node is represented as a pair of `[val, random_index]` where:
|
|
|
|
- `val`: an integer representing `Node.val`
|
|
- `random_index`: the index of the node (range from `0` to `n-1`) that the `random` pointer points to, or `null` if it does not point to any node.
|
|
|
|
Your code will **only** be given the `head` of the original linked list.
|
|
|
|
constraints: |
|
|
- `0 <= n <= 1000`
|
|
- `-10^4 <= Node.val <= 10^4`
|
|
- `Node.random` is `null` or is pointing to some node in the linked list.
|
|
|
|
examples:
|
|
- input: "head = [[7,null],[13,0],[11,4],[10,2],[1,0]]"
|
|
output: "[[7,null],[13,0],[11,4],[10,2],[1,0]]"
|
|
explanation: "A list with 5 nodes. Node 0 has value 7 and random points to null. Node 1 has value 13 and random points to node 0. Node 2 has value 11 and random points to node 4. And so on. The output is a deep copy with the same structure."
|
|
- input: "head = [[1,1],[2,1]]"
|
|
output: "[[1,1],[2,1]]"
|
|
explanation: "Two nodes where both random pointers point to node 1 (the second node with value 2)."
|
|
- input: "head = [[3,null],[3,0],[3,null]]"
|
|
output: "[[3,null],[3,0],[3,null]]"
|
|
explanation: "Three nodes all with value 3. The middle node's random pointer points to the first node."
|
|
|
|
explanation:
|
|
intuition: |
|
|
Imagine you're tasked with duplicating a spider web where each thread (the `next` pointer) connects to the next junction, but there are also random silk threads (`random` pointers) that can connect any junction to any other junction — or nowhere at all.
|
|
|
|
The challenge is that when you're building your copy, you need to connect the `random` pointer of a copied node to another *copied* node. But how do you find the copied version of the node that the original `random` points to?
|
|
|
|
Think of it like this: if you're copying a contact list where each person has a "best friend" field pointing to another person in the same list, you can't fill in the "best friend" until you've created entries for *everyone*. The core insight is that you need a way to **map original nodes to their copies**.
|
|
|
|
There are two elegant approaches:
|
|
1. **Hash Map**: Use a dictionary to store the mapping from each original node to its copy. This makes lookups instant but uses O(n) extra space.
|
|
2. **Interweaving**: Cleverly weave copied nodes directly into the original list (A → A' → B → B' → ...), so each copy sits right next to its original. The original's `random.next` gives you the copy's random target. Then unweave to separate the lists.
|
|
|
|
approach: |
|
|
We'll describe the **Hash Map Approach** as the primary solution due to its clarity:
|
|
|
|
**Step 1: Handle the edge case**
|
|
|
|
- If `head` is `null`, return `null` immediately
|
|
|
|
|
|
|
|
**Step 2: First pass — create all copied nodes**
|
|
|
|
- Traverse the original list
|
|
- For each original node, create a new node with the same value
|
|
- Store the mapping `original_node → copied_node` in a hash map
|
|
- This ensures we have all copies created before wiring up pointers
|
|
|
|
|
|
|
|
**Step 3: Second pass — wire up `next` and `random` pointers**
|
|
|
|
- Traverse the original list again
|
|
- For each original node:
|
|
- Set `copy.next = hash_map[original.next]` (or `null` if `original.next` is `null`)
|
|
- Set `copy.random = hash_map[original.random]` (or `null` if `original.random` is `null`)
|
|
- The hash map allows O(1) lookup of any copied node
|
|
|
|
|
|
|
|
**Step 4: Return the copied head**
|
|
|
|
- Return `hash_map[head]` — the copy of the original head node
|
|
|
|
common_pitfalls:
|
|
- title: Copying Random Pointers to Original Nodes
|
|
description: |
|
|
A common mistake is to set `copy.random = original.random`. This makes the copy's random pointer point to a node in the *original* list, not the copied list.
|
|
|
|
The problem explicitly states: "None of the pointers in the new list should point to nodes in the original list."
|
|
|
|
You must translate every pointer to point to the corresponding *copy*.
|
|
wrong_approach: "copy.random = original.random"
|
|
correct_approach: "copy.random = hash_map[original.random]"
|
|
|
|
- title: Single Pass Without Pre-Creating Nodes
|
|
description: |
|
|
You might try to copy nodes and wire pointers in a single pass. But consider: when processing node A, its `random` might point to node Z which you haven't created yet.
|
|
|
|
Either use two passes (create all nodes first, then wire pointers), or create nodes on-demand and check the hash map before creating duplicates.
|
|
wrong_approach: "Single pass assuming nodes exist"
|
|
correct_approach: "Two-pass or create-on-demand with hash map"
|
|
|
|
- title: Not Handling Null Pointers
|
|
description: |
|
|
Both `next` and `random` can be `null`. When looking up in the hash map, ensure you handle the case where the key is `null`.
|
|
|
|
In Python, `hash_map.get(None)` returns `None`, which is correct. In other languages, you may need explicit null checks.
|
|
wrong_approach: "Unconditionally accessing hash_map[node]"
|
|
correct_approach: "Check if node is null before hash map lookup"
|
|
|
|
key_takeaways:
|
|
- "**Hash map for node mapping**: When cloning graph-like structures, a hash map from original to copy nodes enables O(1) pointer translation"
|
|
- "**Two-pass pattern**: Create all nodes first, then wire connections — this avoids forward reference problems"
|
|
- "**Space-time tradeoff**: The interweaving approach achieves O(1) space but is trickier to implement; hash map is clearer at O(n) space"
|
|
- "**Deep copy fundamentals**: This problem teaches the core concept of deep copying interconnected structures — applicable to graphs, trees with parent pointers, and more"
|
|
|
|
time_complexity: "O(n). We traverse the list twice — once to create copies, once to wire pointers. Each node is visited a constant number of times."
|
|
space_complexity: "O(n). The hash map stores a mapping for each of the `n` nodes. The output list itself also uses O(n) space, but that's required by the problem."
|
|
|
|
solutions:
|
|
- approach_name: Hash Map
|
|
is_optimal: true
|
|
code: |
|
|
class Node:
|
|
def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
|
|
self.val = x
|
|
self.next = next
|
|
self.random = random
|
|
|
|
def copyRandomList(head: 'Node') -> 'Node':
|
|
if not head:
|
|
return None
|
|
|
|
# Map from original node to its copy
|
|
old_to_new = {}
|
|
|
|
# First pass: create all copied nodes
|
|
current = head
|
|
while current:
|
|
old_to_new[current] = Node(current.val)
|
|
current = current.next
|
|
|
|
# Second pass: wire up next and random pointers
|
|
current = head
|
|
while current:
|
|
copy = old_to_new[current]
|
|
# Wire next pointer (use .get() to handle None gracefully)
|
|
copy.next = old_to_new.get(current.next)
|
|
# Wire random pointer
|
|
copy.random = old_to_new.get(current.random)
|
|
current = current.next
|
|
|
|
# Return the copy of the head
|
|
return old_to_new[head]
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Two linear passes through the list.
|
|
|
|
**Space Complexity:** O(n) — Hash map stores n mappings.
|
|
|
|
The hash map approach is intuitive: first create all copies, then use the map to translate every pointer from the original domain to the copy domain. The `.get()` method handles `None` keys gracefully by returning `None`.
|
|
|
|
- approach_name: Interweaving Nodes
|
|
is_optimal: false
|
|
code: |
|
|
def copyRandomList(head: 'Node') -> 'Node':
|
|
if not head:
|
|
return None
|
|
|
|
# Step 1: Interweave copied nodes into the original list
|
|
# Original: A -> B -> C
|
|
# After: A -> A' -> B -> B' -> C -> C'
|
|
current = head
|
|
while current:
|
|
copy = Node(current.val)
|
|
copy.next = current.next
|
|
current.next = copy
|
|
current = copy.next
|
|
|
|
# Step 2: Wire up random pointers for copied nodes
|
|
current = head
|
|
while current:
|
|
copy = current.next
|
|
if current.random:
|
|
# The copy of current.random is current.random.next
|
|
copy.random = current.random.next
|
|
current = copy.next
|
|
|
|
# Step 3: Unweave the lists to separate original and copy
|
|
current = head
|
|
copy_head = head.next
|
|
while current:
|
|
copy = current.next
|
|
# Restore original list
|
|
current.next = copy.next
|
|
# Wire copy list
|
|
if copy.next:
|
|
copy.next = copy.next.next
|
|
current = current.next
|
|
|
|
return copy_head
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Three linear passes through the list.
|
|
|
|
**Space Complexity:** O(1) — No hash map; copies are woven into the original list temporarily.
|
|
|
|
This clever approach avoids extra space by placing each copy immediately after its original. The trick is that `original.random.next` gives us the copy of the random target. After wiring random pointers, we unweave to restore the original list and extract the copy. While space-efficient, it's more complex and modifies the original list temporarily.
|