Start learning free

LESSON 481 OF 540 · GRAPHS

Clone Graph in TypeScript: 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 null and return null.

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 null and returns null.

 

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 14
    Ctrl + Enter to check

    TYPESCRIPT · GRAPHS

    Clone Graph in TypeScript, explained

    Problem overview

    The method cloneGraph receives node, node 1 of a connected, undirected graph, or null when the graph is empty. Each node holds a value val and a list neighbors. The lesson provides the Node class, shown in the starter code's comment, with exactly those two fields and a constructor that takes the value. The method must return a deep copy: the copy of node 1, in a graph of brand-new nodes with the same values, where each new node's neighbor list 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.

    The examples show a graph by its adjacency lists. For [[2, 4], [1, 3], [2, 4], [1, 3]], four nodes are joined in a square: node 1 is next to 2 and 4, node 2 to 1 and 3, and so on. The copy has the same shape, built from 4 new nodes. For [[]] the graph is a single node without neighbors, and for [] the function gets null and returns null.

    The statement 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: one copy per node, found through a Map

    Copying one node is easy: new Node(node.val). The difficulty is the neighbor lists. A node's copy must point to the copies of its neighbors, and those neighbors point back, so the graph is full of cycles. Two rules solve it:

    • Keep a Map from each original node to its copy. When the search meets a node, the map says whether it already has a copy, so a node is never copied twice.
    • A copy's neighbor list is filled by looking up the copies of the original's neighbors in the map, never by reusing the original nodes.

    The walk is a breadth-first search. A node's copy is created the first time the node is met, and that is also when it joins the queue. When a node leaves the queue, its neighbors' copies are appended to its copy's list. The map is also what stops the search from going around a cycle forever. Since the graph is connected, the search reaches every node from node 1.

    A recursive DFS with the same map works as well. On a graph with 50,000 nodes in a long chain, a BFS with a queue has no call-stack depth to worry about.

    In the run for the square graph, the copy has 4 nodes, none of them nodes of the original, and the neighbor lists read [[2, 4], [1, 3], [2, 4], [1, 3]], like the input.

    Step by step

    1. Handle the empty graph. If node === null, return null. The next lines read node.val, which needs a node.
    2. Create copies as new Map<Node, Node>(). A Map can use objects as keys and compares them by identity, so two different nodes are two different keys even when their values are equal.
    3. Put in the first entry: node mapped to new Node(node.val).
    4. Create queue as a Node[] holding just node. The nodes in the queue already have their copies; what their copies still lack is the neighbor list.
    5. Declare head with let, at 0: the index of the next node to take. Moving an index forward is cheaper than shift(), which moves every remaining element each time.
    6. Loop while head < queue.length. Some nodes are still waiting.
    7. Take queue[head] into current.
    8. Do head++. The earlier entries stay in the array but are never read again.
    9. Loop over current.neighbors with for...of. Over the whole run each edge is looked at twice, once from each end.
    10. Test !copies.has(neighbor). No copy means this is the first time the search meets this node.
    11. Make its copy: copies.set(neighbor, new Node(neighbor.val)). This is the only moment this node's copy is made.
    12. Push neighbor onto queue. Its own neighbors get copied when it leaves the queue. Because a node joins the queue only when its copy is made, each node is queued once.
    13. Link the copies after the if, still inside the for: push copies.get(neighbor) onto copies.get(current).neighbors. The copy of current points to the copy of neighbor, never to neighbor itself, and the order of current.neighbors is kept.
    14. Return copies.get(node), the copy of node 1.

    The solution in TypeScript

    class Solution {
        cloneGraph(node: Node | null): Node | null {
            if (node === null) {
                return null;
            }
            const copies = new Map<Node, Node>();
            copies.set(node, new Node(node.val));
            const queue: Node[] = [node];
            let head = 0;
            while (head < queue.length) {
                const current = queue[head];
                head++;
                for (const neighbor of current.neighbors) {
                    if (!copies.has(neighbor)) {
                        copies.set(neighbor, new Node(neighbor.val));
                        queue.push(neighbor);
                    }
                    copies.get(current).neighbors.push(copies.get(neighbor));
                }
            }
            return copies.get(node);
        }
    }
    

    Some TypeScript details:

    • Node | null is a union type: the parameter is a node or null. The check node === null uses strict equality, and after it returns, the rest of the method can use node as a Node.
    • Objects as Map keys. Map compares keys by identity. In a test with two different nodes that both have the value 1, the map keeps two entries, and each key finds its own value. A plain object can't do this: its keys are strings, so both nodes turn into the same key, "[object Object]", and the second assignment overwrites the first.
    • Map.get can return undefined. It does when the key is missing. In this method every node has a copy before it is looked up, so copies.get(current) and copies.get(neighbor) always find one.
    • new Map<Node, Node>() gives the type arguments for the keys and values. They exist only for the compiler and are erased at run time.
    • const and mutation. queue is a const, yet queue.push(...) is fine: const stops reassigning the name, not changing the array. head changes, so it uses let.

    Complexity

    Time: O(V + E). Each node is created once, queued once and taken from the queue once. Each edge is looked at twice, once from each end. The Map operations are constant time on average.

    Space: O(V) extra. The map and the queue each hold one entry per node. The copy itself, which is the output, also takes O(V + E).

    Edge cases and common mistakes

    • The empty graph. null in, null out. Without the first check, node.val would fail on null.
    • A single node. [[]] gives one new node with an empty neighbor list. The loop takes that node, finds no neighbors and ends.
    • Cycles. In the square graph, node 1 leads to 2, 2 leads to 3, 3 leads to 4 and 4 leads back to 1. The has check is what makes the search stop instead of running forever.
    • Linking to the original. Pushing neighbor instead of copies.get(neighbor) would make the copy point into the original graph, which the statement forbids.
    • Copying a node twice. Making a new Node without checking the map would create several copies of one node, and the copied graph would stop matching the original's shape.
    • Linking only the first time. The link step must run for every neighbor, not just for new ones. Otherwise the edges that lead to already-copied nodes are lost.
    • Neighbor order. The neighbors are visited in the original's order, so each copied list keeps it. For nodes whose neighbors are [3, 2], [1] and [1], the copy's lists read [[3, 2], [1], [1]].
    • Using a plain object as the map. Its keys are strings, so different nodes can collide.

    Testing

    • [[2, 4], [1, 3], [2, 4], [1, 3]] gives a copy with the same lists, built from 4 new nodes.
    • [[]] gives one new node without neighbors.
    • [] gives null.
    • [[2], [1, 3], [2]], a chain of three, gives [[2], [1, 3], [2]].
    • A chain of 50,000 nodes is copied completely: all 50,000 nodes are in the copy.
    Keep going with TypeScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free