Start learning free

LESSON 481 OF 540 · GRAPHS

Clone Graph in Kotlin: BFS with a HashMap in O(V + E)

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

    KOTLIN · GRAPHS

    Clone Graph in Kotlin, explained

    Problem overview

    You get node, node 1 of a connected, undirected graph, and cloneGraph must return a deep copy of the whole graph: the copy of node 1. Node i has the value i, and every node reaches every other through its neighbors. A deep copy is made of exactly n brand-new nodes with the same values, and each new node's neighbor list 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. An empty graph arrives as null and the function returns null.

    The lesson provides the node class in a comment in the starter code, so you don't write it:

    class Node(var `val`: Int) {
        var neighbors: ArrayList<Node> = ArrayList()
    }
    

    The examples describe a graph by adjacency lists. In the first one, [[2, 4], [1, 3], [2, 4], [1, 3]], four nodes form a square: node 1 touches 2 and 4, node 2 touches 1 and 3, and so on. The copy has the same shape, built from four new nodes. A single node with no neighbors, [[]], copies to one new node, and the empty graph copies to null.

    Approach

    Copying a node is easy; the difficulty is the cycles. In the square, node 1 leads to 2, which leads to 3, then 4, then back to 1. A copy that blindly follows neighbors never stops, and a copy that makes a fresh node every time it meets one produces duplicates.

    The fix is a HashMap 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. A node that already has a copy is never copied or queued again, which stops the search from circling forever and guarantees one copy per node.

    The search is a breadth-first search. When a node leaves the queue, its copy still has an empty neighbor list. The search walks over the original node's neighbors, makes a copy for any neighbor that has none, and appends each neighbor's copy to the copy's list. Because the neighbors are visited in the original order, each copied list keeps that order.

    A recursive DFS with the same map also works. This lesson uses a queue so that a long chain of nodes doesn't depend on the call stack.

    Step by step

    1. Handle the empty graph. If node == null, return null. After this check Kotlin knows node is not null, so it can be used as a Node in the rest of the function.
    2. Create the map. HashMap<Node, Node>() maps each original node to its copy. Node doesn't define equals or hashCode, so the map compares nodes by identity: two different nodes are two different keys even when their values are equal.
    3. Copy the first node. copies[node] = Node(node.`val`). The field is named val, a Kotlin keyword, so it is written in backticks.
    4. Create the queue. ArrayDeque<Node>() holds the nodes whose neighbors still need copying. add puts a node at the back and removeFirst takes the one at the front.
    5. Queue the start. queue.add(node). Every node in the queue already has its copy; what the copy lacks is its neighbor list.
    6. Loop while the queue has nodes. while (queue.isNotEmpty()).
    7. Take the next node. val current = queue.removeFirst().
    8. Visit its neighbors. for (neighbor in current.neighbors). Over the whole run each edge is looked at twice, once from each end.
    9. Test for a new node. if (!copies.containsKey(neighbor)) means the search meets this node for the first time.
    10. Copy it. copies[neighbor] = Node(neighbor.`val`). This is the only place a node's copy is made.
    11. Queue it. queue.add(neighbor), so its own neighbors are handled later. A node joins the queue only when its copy is made, so each node is queued once.
    12. Link the copies. After the if, add the neighbor's copy to the list of the current node's copy. The copy of current points to the copy of neighbor, never to neighbor itself. getValue reads both copies.
    13. Return the start's copy. copies[node]. The graph is connected, so every node was reached.

    The solution in Kotlin

    class Solution {
        fun cloneGraph(node: Node?): Node? {
            if (node == null) {
                return null
            }
            val copies = HashMap<Node, Node>()
            copies[node] = Node(node.`val`)
            val queue = ArrayDeque<Node>()
            queue.add(node)
            while (queue.isNotEmpty()) {
                val current = queue.removeFirst()
                for (neighbor in current.neighbors) {
                    if (!copies.containsKey(neighbor)) {
                        copies[neighbor] = Node(neighbor.`val`)
                        queue.add(neighbor)
                    }
                    copies.getValue(current).neighbors.add(copies.getValue(neighbor))
                }
            }
            return copies[node]
        }
    }
    

    Kotlin details worth knowing:

    • Nullable types. The parameter is Node? and so is the return type, since both can be null. After if (node == null) return null, the compiler smart-casts node to Node.
    • copies[key] versus getValue(key). Reading a map with [] gives a Node?, because the key may be missing. getValue(key) gives a plain Node and throws if the key is absent, which can't happen here because every neighbor in the loop has a copy by the time the line runs. The final copies[node] returns a Node?, which matches the return type.
    • Identity keys. A class that doesn't override equals and hashCode uses object identity. Two Node(1) objects are two separate keys in a HashMap; a test with two such nodes gives a map of size 2. That is what the algorithm needs.
    • Backticks. val can't be used as a plain name, so `val` is the way to declare and read it.
    • ArrayDeque here is Kotlin's own ArrayDeque, so no import is needed.

    Complexity

    Each node is copied once and queued once, and each edge is examined twice, once from each end. That makes the time O(V + E), where V is the number of nodes and E the number of edges. The map and the queue hold at most V entries, so the extra space is O(V), not counting the copy itself.

    Edge cases and common mistakes

    • Empty graph. A null input must give null. Reading node.`val` first would not compile, because node is nullable.
    • Single node. [[]] gives one new node and an empty neighbor list. The loop body never runs.
    • Returning the original node. Putting an original node into a copied list breaks the deep copy. A check on the square graph confirms that cloneGraph returns a different object, with none of the original nodes shared.
    • No map. Without it, a cycle makes the search loop forever, or the same node gets copied twice and neighbors point at different copies of the same node.
    • Copying at dequeue time instead of at discovery time. A node can be found as a neighbor several times before it leaves the queue, so it could be copied more than once. The solution copies exactly when the node is first met.
    • Forgetting order. Neighbors must be appended in the original order; iterating current.neighbors in order does that.
    • Deep recursion. A graph can be a chain of 50,000 nodes. The queue-based version handles it; a recursive DFS goes that deep in the call stack.

    Testing

    Results from running the solution on graphs built from adjacency lists, printing the neighbor lists of the copy from node 1 up:

    • [[2, 4], [1, 3], [2, 4], [1, 3]]: [[2, 4], [1, 3], [2, 4], [1, 3]], with 4 copied nodes and none shared with the original
    • [[]]: [[]]
    • []: null
    • [[2], [1, 3], [2]]: [[2], [1, 3], [2]]
    • [[3, 2], [1], [1]]: [[3, 2], [1], [1]], so the neighbor order is kept
    • A chain of 50,000 nodes: 50,000 copied nodes
    Keep going with Kotlin500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free