Start learning free

LESSON 283 OF 347 · LINKED LISTS

Copy List with Random Pointer in C#: Two Passes with a Dictionary

Practice O(n) Read the full explanation

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.

 

Example 1:

Input: head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
Output: [[7,null],[13,0],[11,4],[10,2],[1,0]]

Example 2:

Input: head = [[1,1],[2,1]]
Output: [[1,1],[2,1]]

Example 3:

Input: head = [[3,null],[3,0],[3,null]]
Output: [[3,null],[3,0],[3,null]]

Example 4:

Input: head = []
Output: []
Explanation: The given linked list is empty (null pointer), so return null.

 

Constraints:

  • 0 <= n <= 1000
  • -10000 <= Node.val <= 10000
  • Node.random is null or is pointing to some node in the linked list.

Note:

Your algorithm should run in O(n) time and use at most O(n) extra space.


STEP 1 OF 11
    Ctrl + Enter to check

    C# · LINKED LISTS

    Copy List Random Pointer in C#, explained

    Problem overview

    Each ListNode has a val, a next pointer and a random pointer. The random pointer can lead to any node in the list, including the node itself, or to null. CopyRandomList(head) must build a deep copy: exactly n brand new nodes with the same values, where the copies' next and random pointers point to other copies, never to original nodes. It returns the head of the copy, or null for an empty list.

    In the first example, the input [[7,null],[13,0],[11,4],[10,2],[1,0]] lists each node as [val, random_index]. Node 13 has its random pointer on node 0 (the 7), node 11 points to node 4 (the 1), and so on. The copy must have the same shape: its second node's random pointer leads to the copy of the first node, not to the original 7. The note asks for O(n) time and at most O(n) extra space.

    Approach: map every original node to its copy

    The difficulty is that a random pointer can lead forward, backward, or to the node itself. When you copy a node, the node it points to may not have a copy yet. Building the list in one pass runs into this constantly.

    The fix is to split the work in two:

    1. Pass one creates a copy of every node, with just the value, and records it in a dictionary keyed by the original node.
    2. Pass two wires up the pointers. By now every original has a copy in the dictionary, so for any node the copies of its next and random targets are one lookup away.

    The dictionary costs O(n) extra space, which the problem allows. The lesson on the same problem that follows this one gets rid of the dictionary by weaving the copies into the original list. This lesson takes the clearer route first.

    Step by step

    1. Create the map. copies is a Dictionary<ListNode, ListNode>: original node in, copy out. ListNode doesn't override Equals or GetHashCode, so the dictionary compares keys by reference. Two nodes with the same value are still two different keys.
    2. Start a cursor. node begins at head, so head itself stays put. The second pass and the return both need it.
    3. Loop while node != null to visit every original node.
    4. Store a value-only copy. copies[node] = new ListNode(node.val). The copy's pointers stay unset for now, because its targets might not exist yet. Assigning through the indexer adds the key if it is new.
    5. Move to the next original node. Without this the loop never ends.
    6. Reset the cursor to head for the second pass.
    7. Loop again over the originals.
    8. Wire next. The copy's next is the copy of node.next, or null if there is no next node. The conditional guards the lookup because a Dictionary throws an ArgumentNullException when you pass it a null key.
    9. Wire random. Same pattern with node.random.
    10. Advance along the original list's next pointers. The copies' links are what this loop is building, so they're not used for walking.
    11. Return the copy of head, guarded for an empty list, where looking up head would be a lookup with a null key.

    The solution in C#

    class Solution {
        public ListNode CopyRandomList(ListNode head) {
            Dictionary<ListNode, ListNode> copies = new Dictionary<ListNode, ListNode>();
            ListNode node = head;
            while (node != null) {
                copies[node] = new ListNode(node.val);
                node = node.next;
            }
            node = head;
            while (node != null) {
                copies[node].next = node.next != null ? copies[node.next] : null;
                copies[node].random = node.random != null ? copies[node.random] : null;
                node = node.next;
            }
            return head != null ? copies[head] : null;
        }
    }
    

    Some C# details are worth a closer look:

    • Reference keys. Class instances use reference equality by default, which is what you want here. A class that overrides Equals to compare values would break this approach for duplicate values like the 3, 3, 3 in the third example.
    • The indexer reads and writes. copies[node] = … inserts or overwrites. copies[node] on the right-hand side reads, and throws a KeyNotFoundException if the key is missing. Here every original node is a key by the second pass, so reads always succeed.
    • The conditional operator. a != null ? copies[a] : null evaluates only the branch it picks, so copies[null] is never executed. The line reads copies[node].next = (node.next != null) ? … : null, because = has lower precedence than ?:.
    • Namespaces. Dictionary lives in System.Collections.Generic, so a standalone file needs using System.Collections.Generic; at the top.
    • Assigning null to a reference. Because ListNode is a class, null is a valid value for its variables and for the next and random fields.

    Complexity

    Time: O(n). Each pass visits every node once, and a dictionary insert or lookup takes constant time on average.

    Space: O(n). The dictionary holds one entry per node, on top of the n new nodes that make up the output.

    Edge cases and common mistakes

    • Empty list. With head == null both loops are skipped, and the final conditional returns null without touching the dictionary.
    • Random pointer to itself. A node whose random points at the node itself must get a copy that points at itself, not at the original. The dictionary does this, because it looks up the node's own copy.
    • Random pointer forward or backward. That is why the pointers wait for the second pass. In a single pass, a node's random may point to a node that has no copy yet.
    • Equal values. The third example has three nodes with value 3. Keying a dictionary by value would merge them. Keying by node does not.
    • Looking up null. copies[node.random] with a null random throws an ArgumentNullException, so keep the null check.
    • Pointing at originals. Writing copy.random = node.random would make the copy share nodes with the original list, which the problem forbids.
    • Losing the cursor. If the first loop moves head instead of node, the second pass has nowhere to start.

    Testing

    • [[7,null],[13,0],[11,4],[10,2],[1,0]] copies to the same pairs, with every pointer inside the new list.
    • [[1,1],[2,1]] copies to [[1,1],[2,1]]: both random pointers go to the second node.
    • [[3,null],[3,0],[3,null]] copies to the same pairs.
    • [] returns null.

    To confirm the copy is deep, check that no node of the result is a node of the input: ReferenceEquals(copyNode, originalNode) must be false for every pair.

    Keep going with C#About 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free