Lesson 25 — Cloning with a map

~25 minutes · linked-list cloning · Python · Day 27

Tonight you solve Copy List with Random Pointer. Interviewers like it because ordinary list copying is easy to describe, but the random pointer can jump to any node in the list. The trap is subtle: if a copied node points back into the original list, you did not make an independent copy.

The invariant

A hashmap from old node to new node lets you wire up arbitrary pointers in a second pass after all clones exist.

That is the whole move. First make every clone node and store the relationship: original node → copied node. Only after that do you connect next and random. The second pass is safe because every pointer target already has a clone waiting in the map.

Key: old_to_new[old target] gives clone target original clone old A old B old C old A.random is old C old_to_new new A new B new C new A.random becomes new C

The map keeps clone pointers independent: an old random target becomes a lookup for the matching cloned target.

What Python gives you

Python's dict stores key/value pairs. Here the key is an old node object; the value is its new clone. Dictionary lookup and insertion are average O(1) hash-table operations (Python wiki: time complexity), which is why the map version can stay linear.

Do not key the map by node.val. Values can repeat; node objects are the things being copied.

Copy List: the O(n²) slow way first

The brute-force idea is to copy the next chain first. Then, for each original node, find its random target by scanning from the old head, and walk the same number of steps in the copied list to find the matching clone.

old = head
new = copy_head
while old:
    scan_old, scan_new = head, copy_head
    while scan_old is not old.random:   # rescan to find target
        scan_old = scan_old.next
        scan_new = scan_new.next
    new.random = scan_new
    old, new = old.next, new.next

This works as an idea, but it is O(n²): each node may trigger another full scan. The better approach keeps the same clone output but spends O(n) extra space so every target clone is one dictionary lookup away.

Copy List: the O(n) map way

Use two passes. Pass one creates clone nodes only. Pass two connects arrows. The small trick {None: None} means a missing next or random pointer maps cleanly to None.

Pass 1: create clones Pass 2: wire arrows old A old B old C new A new B new C map filled; clone arrows empty new A new B new C next/random = map[old pointer] wire by lookup, not by search

Pass one guarantees every target clone exists; pass two turns each old pointer into a map lookup.

def copyRandomList(head):
    old_to_new = {None: None}              # handles empty next/random
    cur = head
    while cur:                             # pass 1: create clones
        old_to_new[cur] = Node(cur.val)
        cur = cur.next
    cur = head
    while cur:                             # pass 2: wire pointers
        old_to_new[cur].next = old_to_new[cur.next]
        old_to_new[cur].random = old_to_new[cur.random]
        cur = cur.next
    return old_to_new[head]

The important order is not a syntax detail. A random pointer can point forward, backward, to itself, or to nothing. Creating every clone first removes all of those cases from the wiring step.

Transfer: same map, new shape

Clone Graph: each node has a list of neighbors instead of one next pointer and one random pointer. Before opening the reveal, predict the map. What should the key be, what should the value be, and when is it safe to append cloned neighbors?

Reveal after your prediction

The map is still original object → cloned object. For a graph, the second pass may happen inside DFS or BFS, but the invariant does not change: get or create the clone for each old node, then wire each cloned neighbor through the same map. The pointer field changed shape; the old-to-new relationship stayed the same.

What the trade buys you

ApproachTimeSpace
Brute force (rescan for random target)O(n²)O(1) extra
Hashmap clone (two passes)O(n)O(n) map

The output copy itself is O(n) either way. The trade is about extra working memory: a map buys you direct access to every clone, so no pointer target needs a search.

Practice — answer before you scroll past

1. What does the clone map store?

2. Why create all clones before wiring random pointers?

3. Spot the bug in this one-pass attempt:

old_to_new = {}
cur = head
while cur:
    old_to_new[cur] = Node(cur.val)
    old_to_new[cur].random = old_to_new[cur.random]
    cur = cur.next

4. Which task has the same old-to-new map shape?

Your move (tonight)

Solve this on LeetCode, logging it in your Notion tracker with the cycle — predict → attempt → compare → encode:

  1. Copy List with Random Pointer — predict the O(n²) rescan version first, then attempt the two-pass hashmap version from the invariant.

For the encode step, write one sentence explaining why the random pointers must be wired after all clones exist.

One primary source

Watch NeetCode — Copy List with Random Pointer after your attempt, as the compare step. Listen for the moment where the old-to-new map turns arbitrary pointers into ordinary lookups.