Start learning free

LESSON 481 OF 540 · GRAPHS

Clone Graph in Go: BFS with a Map from Originals to Copies

Practice O(V + E) Read the full explanation

You are given node 1 of a connected, undirected graph. Return a deep copy (a clone) of the graph.

Each node holds a value and the list of its neighbors; the node class is shown in the starter code. The graph has n nodes, numbered from 1 to n, and node i has the value i. The graph is connected, so every node can be reached from node 1 by going from neighbor to neighbor.

A deep copy is made of exactly n brand-new nodes, one for each node of the graph, with the same value. The neighbor list of each new node holds the new nodes of the original node's neighbors, in the same order. No node of the copy may be a node of the original graph. Return the new node 1. If the graph is empty (n = 0), you get nil and return nil.

The examples show a graph by its adjacency lists: adjList[i - 1] holds the values of node i's neighbors. After your function returns, the tests walk your copy from the node you returned, print the neighbors of each node, node by node, and report any node that belongs to the original graph.

 

Example 1:

Input: adjList = [[2, 4], [1, 3], [2, 4], [1, 3]]
Output: [[2, 4], [1, 3], [2, 4], [1, 3]]
Explanation: The graph has 4 nodes, joined in a square:
    1 - 2
    |   |
    4 - 3
Node 1's neighbors are nodes 2 and 4, node 2's are nodes 1 and 3, node 3's are nodes 2 and 4,
and node 4's are nodes 1 and 3. The copy has the same shape, made of 4 new nodes.

Example 2:

Input: adjList = [[]]
Output: [[]]
Explanation: The graph is a single node, node 1, with no neighbors. The copy is one new node with the value 1.

Example 3:

Input: adjList = []
Output: []
Explanation: The graph is empty: the function gets nil and returns nil.

 

Constraints:

  • 0 <= n <= 5 * 104
  • Node i has the value i, for 1 <= i <= n.
  • There are at most 105 edges. No edge joins a node to itself, and no edge appears twice.
  • The graph is connected.

Note:

Your algorithm should run in O(V + E) time, where V is the number of nodes and E the number of edges, and use O(V) extra space.

The tutorial walks the graph with a BFS, as in Course Schedule, and keeps a hash map from each original node to its copy. A node's copy is made the first time the search meets the node, and that is also when the node joins the queue. When a node leaves the queue, the copies of its neighbors are appended to its copy's list. The map is what stops the search from going around a cycle forever: a node that already has a copy is never made or queued again. A recursive DFS with the same map works too.

STEP 1 OF 12
    Ctrl + Enter to check

    GO · GRAPHS

    Clone Graph in Go, explained

    Problem overview

    The function cloneGraph gets node, node 1 of a connected, undirected graph, and returns a deep copy of the graph: the copy of node 1. The lesson provides the node type, shown in the starter code as a comment: a Node struct with an int field Val and a field Neighbors, a slice of *Node.

    The graph has n nodes, numbered 1 to n, and node i has the value i. A deep copy is made of exactly n brand-new nodes with the same values, and the neighbor list of each new node holds the new nodes of the original's neighbors, in the same order. No node of the copy may be a node of the original. For an empty graph the function gets nil and returns nil.

    In the first example the adjacency lists are [[2, 4], [1, 3], [2, 4], [1, 3]]: a square of four nodes, 1-2, 2-3, 3-4 and 4-1. The copy has four new nodes joined in the same shape. In the second example the graph is a single node with no neighbors, and the copy is one new node with the value 1.

    The lesson asks for O(V + E) time and O(V) extra space, where V is the number of nodes and E the number of edges.

    Approach: a map from each original to its copy

    Copying one node is easy: make a Node with the same Val. The hard part is the neighbors. A copy's neighbor list must point to copies, never to originals, and a node can be reached from several others. The graph also has cycles, as in the square, so walking blindly from neighbor to neighbor never ends.

    One map solves both problems. It maps each original node to its copy, and it does two jobs:

    • It finds the copy of any node met so far, so a node is never copied twice.
    • It tells which nodes have been met already, which stops the search from going around a cycle forever.

    The search is a breadth-first search. A node's copy is made the first time the search meets the node, and that is also when the node joins the queue. When a node leaves the queue, the copies of its neighbors are appended to its copy's list. A recursive DFS with the same map works too; this lesson uses a BFS with its own queue.

    Step by step

    1. Handle the empty graph. If node == nil, return nil. The next lines read the node's value, which needs a node.
    2. Create the map copies with make(map[*Node]*Node). The keys are pointers, so two different nodes are two different keys, even if their values were equal.
    3. Add the first entry: copies[node] = &Node{Val: node.Val}. The expression makes a new node and gives its address. Its Neighbors starts as an empty (nil) slice.
    4. Create the queue as a slice of *Node holding just node. Every node in the queue already has its copy. What its copy lacks is the neighbor list, which is filled when the node leaves the queue.
    5. Loop while len(queue) > 0. The search ends when every reachable node has left the queue.
    6. Take the first node: current := queue[0], then queue = queue[1:] drops it.
    7. Loop over current.Neighbors with range. Over the whole run each edge is looked at twice, once from each end, which is why the work is O(V + E).
    8. Check copies[neighbor] == nil. Reading a key that isn't in a map gives the zero value, which is nil for a pointer. So no copy yet means the search is meeting this node for the first time.
    9. Make the copy: copies[neighbor] = &Node{Val: neighbor.Val}. A node that has a copy never gets a second one.
    10. Queue it: queue = append(queue, neighbor). A node joins the queue only when its copy is made, so each node is queued exactly once.
    11. Link the copies after the if, still inside the loop: append copies[neighbor] to copies[current].Neighbors and store the result back. The copy of current points to the copy of neighbor, never to neighbor itself, and the neighbors come in current's order, so every copied list keeps the original order.
    12. Return copies[node], the copy of node 1.

    The solution in Go

    func cloneGraph(node *Node) *Node {
        if node == nil {
            return nil
        }
        copies := make(map[*Node]*Node)
        copies[node] = &Node{Val: node.Val}
        queue := []*Node{node}
        for len(queue) > 0 {
            current := queue[0]
            queue = queue[1:]
            for _, neighbor := range current.Neighbors {
                if copies[neighbor] == nil {
                    copies[neighbor] = &Node{Val: neighbor.Val}
                    queue = append(queue, neighbor)
                }
                copies[current].Neighbors = append(copies[current].Neighbors, copies[neighbor])
            }
        }
        return copies[node]
    }
    

    The Go details worth a closer look:

    • Composite literals and pointers. &Node{Val: node.Val} builds a struct and takes its address in one expression. Fields not named, such as Neighbors, start with their zero value, here a nil slice.
    • Pointer keys. A map[*Node]*Node compares keys by address. Two nodes with the same Val but different addresses are different keys, which is what this problem needs.
    • Missing keys give the zero value. copies[neighbor] == nil works as a "not seen yet" test because every stored value is a non-nil pointer. For other value types you would use v, ok := m[k].
    • append returns the slice. It may allocate a new array, so the result is assigned back to copies[current].Neighbors. Because copies[current] is a pointer, the assignment changes the copy itself.
    • queue = queue[1:] is the slice without its first element. It is a simple queue for this lesson.

    Complexity

    Time: O(V + E). Every node is queued once, and every neighbor list is read once. The map operations take constant time on average.

    Space: O(V) extra. The map holds one entry per node, and the queue never holds more than V nodes. The new neighbor lists are part of the copy itself, not extra space.

    Edge cases and common mistakes

    • An empty graph. nil in, nil out. Without the first check, node.Val would stop the program with a nil pointer dereference.
    • A single node. The queue holds it once, its neighbor loop does nothing, and the copy has an empty neighbor list.
    • Linking to originals. Appending neighbor instead of copies[neighbor] makes a copy that still points into the original graph. The shallow version &Node{Val: node.Val, Neighbors: node.Neighbors} has the same problem: its first neighbor is the very same node as the original's.
    • Copying a node twice. If you make a copy without first checking the map, the same original gets several copies, and the graph's shape breaks.
    • No map, no stop. Without a record of the met nodes, a cycle such as the square sends the search around forever.

    Testing

    These are the adjacency lists of the copy the function returns, read node by node from node 1, for each graph:

    • [[2, 4], [1, 3], [2, 4], [1, 3]] gives [[2 4] [1 3] [2 4] [1 3]], with 4 nodes and none shared with the original.
    • [[]] gives [[]]: one node, no neighbors.
    • [] gives nil.
    • [[2, 3], [1, 3], [1, 2]], a triangle, gives [[2 3] [1 3] [1 2]].
    • [[2], [1, 3], [2]], a path of three nodes, gives [[2] [1 3] [2]].

    On the square, the search queues the nodes in the order 1, 2, 4, 3: node 1's neighbors are 2 and 4, and node 3 is first met from node 2.

    Keep going with Go500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free