← Back to DevBytes

Solving Copy List with Random Pointer in Go: Step-by-Step Guide

Introduction to the Copy List with Random Pointer Problem

The "Copy List with Random Pointer" problem is a classic algorithmic challenge frequently encountered in coding interviews and computer science coursework. The task involves creating a deep copy of a linked list where each node contains not only a Next pointer (as in a standard singly linked list) but also a Random pointer that can point to any node in the list — or to nil. The challenge lies in correctly reconstructing these random connections in the new list without accidentally sharing references with the original.

While the problem sounds straightforward, the random pointers introduce a subtle complication: when you copy a node, its random target may not yet exist in the new list. This forces developers to think carefully about ordering, memory, and reference management. In Go, where pointers are explicit and memory safety is enforced by the runtime, this problem offers an excellent opportunity to practice pointer manipulation and map-based lookups.

Why This Problem Matters

Beyond its interview popularity, the Copy List with Random Pointer problem models real-world scenarios where data structures contain non-linear references. Examples include:

Mastering this problem teaches two fundamental techniques: using hash maps to track node correspondences, and in-place pointer weaving to achieve constant extra space. Both techniques appear repeatedly in more advanced problems involving graphs, trees, and object cloning.

Understanding the Node Structure

Before diving into solutions, let's define the node structure in Go. Each node holds an integer value, a pointer to the next node, and a pointer to an arbitrary node within the list.

package main

// Node represents a node in the linked list with a random pointer.
type Node struct {
    Val    int
    Next   *Node
    Random *Node
}

The Val field stores the node's data, Next points to the subsequent node in sequence, and Random can point to any node in the list (including itself) or be nil. A typical input list might look like: 7 -> 13 -> 11 -> 10 -> 1, where node 13's random points to node 7, node 11's random points to node 1, and so on.

Approach 1: Hash Map Based Solution

The most intuitive approach uses a hash map to record the mapping between original nodes and their copies. The algorithm proceeds in two passes:

This approach runs in O(n) time and uses O(n) extra space for the map.

package main

// copyRandomList creates a deep copy of the list using a hash map.
func copyRandomList(head *Node) *Node {
    if head == nil {
        return nil
    }

    // Map each original node to its copy.
    nodeMap := make(map[*Node]*Node)

    // First pass: create copies of all nodes.
    current := head
    for current != nil {
        nodeMap[current] = &Node{Val: current.Val}
        current = current.Next
    }

    // Second pass: assign Next and Random pointers.
    current = head
    for current != nil {
        copyNode := nodeMap[current]
        copyNode.Next = nodeMap[current.Next]
        copyNode.Random = nodeMap[current.Random]
        current = current.Next
    }

    return nodeMap[head]
}

Notice how the map lookup handles nil gracefully: when current.Next or current.Random is nil, the map returns the zero value for a pointer, which is nil — exactly what we want.

Walking Through the Hash Map Approach

Consider a list with three nodes: A (val 1, random C), B (val 2, random A), C (val 3, random nil). After the first pass, the map contains three entries mapping A, B, and C to their fresh copies. During the second pass, when processing node A, we look up nodeMap[C] to set the copy's random pointer, ensuring the new node references the new copy of C — not the original. This guarantees a true deep copy.

Approach 2: In-Place Interweaving (O(1) Space)

For situations where memory is constrained, we can eliminate the hash map by weaving copied nodes directly into the original list. The strategy has three phases:

package main

// copyRandomListConstantSpace creates a deep copy using O(1) extra space.
func copyRandomListConstantSpace(head *Node) *Node {
    if head == nil {
        return nil
    }

    // Phase 1: Interleave copied nodes with original nodes.
    current := head
    for current != nil {
        copyNode := &Node{Val: current.Val}
        copyNode.Next = current.Next
        current.Next = copyNode
        current = copyNode.Next
    }

    // Phase 2: Assign random pointers to the copied nodes.
    current = head
    for current != nil {
        if current.Random != nil {
            current.Next.Random = current.Random.Next
        }
        current = current.Next.Next
    }

    // Phase 3: Separate the two lists.
    current = head
    copyHead := head.Next
    for current != nil {
        copyNode := current.Next
        current.Next = copyNode.Next
        if copyNode.Next != nil {
            copyNode.Next = copyNode.Next.Next
        }
        current = current.Next
    }

    return copyHead
}

This approach achieves O(n) time with O(1) extra space (excluding the output list). The trade-off is that it temporarily mutates the original list, which may be undesirable if the original must remain untouched during the operation. Although the original list is restored by the end, concurrent readers could observe inconsistent state mid-operation.

Testing the Implementation

A robust solution deserves thorough testing. Below is a complete test harness that constructs a sample list, copies it, and verifies that the copy is structurally identical but physically distinct from the original.

package main

import (
    "fmt"
    "reflect"
)

// buildList constructs a list from values and random indices.
// randomIndices[i] is the 0-based index of the random target, or -1 for nil.
func buildList(values []int, randomIndices []int) *Node {
    if len(values) == 0 {
        return nil
    }

    nodes := make([]*Node, len(values))
    for i, v := range values {
        nodes[i] = &Node{Val: v}
    }
    for i := 0; i < len(nodes)-1; i++ {
        nodes[i].Next = nodes[i+1]
    }
    for i, idx := range randomIndices {
        if idx >= 0 {
            nodes[i].Random = nodes[idx]
        }
    }
    return nodes[0]
}

// listToSlice converts a list back to values and random indices for comparison.
func listToSlice(head *Node) ([]int, []int) {
    indexMap := make(map[*Node]int)
    idx := 0
    for cur := head; cur != nil; cur = cur.Next {
        indexMap[cur] = idx
        idx++
    }

    values := []int{}
    randoms := []int{}
    for cur := head; cur != nil; cur = cur.Next {
        values = append(values, cur.Val)
        if cur.Random != nil {
            randoms = append(randoms, indexMap[cur.Random])
        } else {
            randoms = append(randoms, -1)
        }
    }
    return values, randoms
}

func main() {
    // Build: 7 -> 13 -> 11 -> 10 -> 1
    // Randoms: nil, 0, 4, 2, 0
    original := buildList(
        []int{7, 13, 11, 10, 1},
        []int{-1, 0, 4, 2, 0},
    )

    copy1 := copyRandomList(original)
    copy2 := copyRandomListConstantSpace(original)

    origVals, origRand := listToSlice(original)
    c1Vals, c1Rand := listToSlice(copy1)
    c2Vals, c2Rand := listToSlice(copy2)

    fmt.Println("Original:", origVals, origRand)
    fmt.Println("Copy 1:  ", c1Vals, c1Rand)
    fmt.Println("Copy 2:  ", c2Vals, c2Rand)

    if reflect.DeepEqual(origVals, c1Vals) && reflect.DeepEqual(origRand, c1Rand) {
        fmt.Println("Hash map copy: PASS")
    } else {
        fmt.Println("Hash map copy: FAIL")
    }

    if reflect.DeepEqual(origVals, c2Vals) && reflect.DeepEqual(origRand, c2Rand) {
        fmt.Println("Constant space copy: PASS")
    } else {
        fmt.Println("Constant space copy: FAIL")
    }
}

Running this program should print matching values and random indices for all three lists, confirming that both approaches produce correct deep copies.

Best Practices and Common Pitfalls

When implementing this solution in Go, keep the following best practices in mind:

Performance Comparison

Both approaches run in linear time, but their constant factors differ. The hash map approach performs two passes with map insertions and lookups, which involve hashing pointer values. The in-place approach performs three passes with only pointer assignments. In practice, the hash map approach is often faster for small to medium lists due to simpler memory access patterns, while the in-place approach shines for very large lists where the map's memory overhead becomes significant.

Conclusion

The Copy List with Random Pointer problem is a deceptively simple exercise that rewards careful thinking about references, memory, and algorithm design. In Go, the explicit pointer semantics make the solution particularly instructive, as every reference assignment is visible in the code. Whether you choose the clarity of the hash map approach or the elegance of the in-place interweaving technique, mastering both will deepen your understanding of linked structures and prepare you for more complex graph cloning challenges. By following the step-by-step implementations and best practices outlined in this guide, you now have a reliable toolkit for solving this problem confidently in any Go codebase.

— Ad —

Google AdSense will appear here after approval

← Back to all articles