If I have a graph whose nodes are represented like this:
class GraphNode {
public:
int val;
std::vector<GraphNode*> adj_nei;
GraphNode() {
val = 0;
adj_nei = std::vector<GraphNode*>();
}
GraphNode(int _val) {
val = _val;
adj_nei = std::vector<GraphNode*>();
}
GraphNode(int _val, std::vector<GraphNode*> _adj_nei) {
val = _val;
adj_nei = _adj_nei;
}
};
And I am trying to write a function that performs a deep copy of it here:
#include <vector>
#include <map>
class GraphNodeDeepCopy {
private:
std::map<GraphNode*, GraphNode*> v;
public:
GraphNode* dcGraph(GraphNode* gn) {
if (v.find(gn) != v.end()) {
return v[gn];
}
GraphNode* dcGraphNode = new GraphNode(gn->val, {});
v[gn] = dcGraphNode;
for (GraphNode* nei : gn->adj_nei) {
dcGraphNode->adj_nei.push_back(dcGraph(nei));
}
return dcGraphNode;
}
};
It doesn't work, especially in the case where gn
is null. Can you help fix it? (Also please combine all the code.)
Of course! This is a classic graph problem, and your approach is very close to the standard solution. You've correctly identified that using a map (or hash map) is the key to handling cycles and ensuring that each node is only copied once.
Let's break down the issues with your current code and then provide a corrected, complete version.
What's Wrong with the Code?
There are two main issues: one critical bug you pointed out and one subtle design flaw.
As you correctly identified, your code fails when the input gn
is nullptr
.
Let's trace the execution with gn = nullptr
:
GraphNode* dcGraph(GraphNode* gn) { // gn is nullptr
if (v.find(gn) != v.end()) { // Tries to use nullptr as a key in the map.
// This might not crash immediately, but it's undefined behavior.
return v[gn];
}
// If the find doesn't crash, this line WILL:
GraphNode* dcGraphNode = new GraphNode(gn->val, {}); // CRASH!
// You are dereferencing a null pointer: gn->val
// ...
}
The Fix: The simplest way to handle this is to add a check at the very beginning of the function. If the input node is nullptr
, the deep copy should also be nullptr
.
2. Design Flaw: Stateful Class Member v
Your map v
is a member of the GraphNodeDeepCopy
class. This means it retains its state across multiple calls to dcGraph
on the same GraphNodeDeepCopy
object.
Consider this scenario:
GraphNodeDeepCopy copier;
GraphNode* new_graph1 = copier.dcGraph(old_graph1); // Works fine, 'v' is now full of nodes from graph1
GraphNode* new_graph2 = copier.dcGraph(old_graph2); // Problem! 'v' still has old mappings.
// This will cause incorrect results if old_graph2
// is not connected to old_graph1.
The map that tracks visited nodes should only exist for the duration of a single, complete copy operation.
The Fix: A much cleaner design is to make the map local to the copy operation. We can do this by creating a public "entry-point" function that initializes the map and then calls a private, recursive helper function that does the actual work, passing the map by reference. This makes your GraphNodeDeepCopy
object stateless and safely reusable.
The Corrected and Complete Code
Here is the full, corrected code. It's combined into a single block, incorporates the fixes, and includes a main
function to demonstrate that it works correctly. I've also added comments to highlight the key changes and logic.
#include <iostream>
#include <vector>
#include <map>
#include <queue> // Used for printing the graph for verification
// The GraphNode class you provided (unchanged)
class GraphNode {
public:
int val;
std::vector<GraphNode*> adj_nei;
GraphNode() : val(0) {}
GraphNode(int _val) : val(_val) {}
GraphNode(int _val, std::vector<GraphNode*> _adj_nei) : val(_val), adj_nei(_adj_nei) {}
};
// The corrected class for performing a deep copy
class GraphCloner {
public:
/**
* @brief Public entry point to clone a graph.
*
* This function initializes the necessary data structure (the visited map)
* and then calls the private recursive helper to perform the deep copy.
* This design ensures the cloner object is stateless and can be reused.
*
* @param node The starting node of the graph to clone.
* @return A pointer to the starting node of the newly created deep copy.
*/
GraphNode* cloneGraph(GraphNode* node) {
// The map is now local to this single clone operation.
// It maps an original node's address to its copy's address.
std::map<GraphNode*, GraphNode*> visited_map;
return cloneRecursive(node, visited_map);
}
private:
/**
* @brief Private recursive helper that performs the actual cloning.
*
* @param node The current original node to process.
* @param visited_map A reference to the map tracking cloned nodes.
* @return A pointer to the cloned node.
*/
GraphNode* cloneRecursive(GraphNode* node, std::map<GraphNode*, GraphNode*>& visited_map) {
// FIX 1: Handle the null graph case gracefully.
if (node == nullptr) {
return nullptr;
}
// If we have already copied this node, return the existing copy.
// This prevents infinite loops in graphs with cycles.
if (visited_map.find(node) != visited_map.end()) {
return visited_map[node];
}
// 1. Create the new node with the same value.
GraphNode* copyNode = new GraphNode(node->val);
// 2. Add the new node to the visited map BEFORE recursing.
// This is crucial for handling cycles correctly.
visited_map[node] = copyNode;
// 3. Recursively clone all neighbors and add them to the new node's adjacency list.
for (GraphNode* neighbor : node->adj_nei) {
copyNode->adj_nei.push_back(cloneRecursive(neighbor, visited_map));
}
return copyNode;
}
};
// Helper function to print a graph (using BFS) to verify the result
void printGraph(GraphNode* startNode) {
if (!startNode) {
std::cout << "Graph is empty (null)." << std::endl;
return;
}
std::map<GraphNode*, bool> visited;
std::queue<GraphNode*> q;
q.push(startNode);
visited[startNode] = true;
while (!q.empty()) {
GraphNode* u = q.front();
q.pop();
std::cout << "Node(val: " << u->val << ", addr: " << u << ") -> Neighbors: [";
for (size_t i = 0; i < u->adj_nei.size(); ++i) {
GraphNode* v = u->adj_nei[i];
std::cout << " (val: " << v->val << ", addr: " << v << ")" << (i == u->adj_nei.size() - 1 ? "" : ",");
if (visited.find(v) == visited.end()) {
visited[v] = true;
q.push(v);
}
}
std::cout << " ]" << std::endl;
}
}
int main() {
// --- Create a sample graph ---
// 1 -- 2
// | |
// 4 -- 3
GraphNode* node1 = new GraphNode(1);
GraphNode* node2 = new GraphNode(2);
GraphNode* node3 = new GraphNode(3);
GraphNode* node4 = new GraphNode(4);
node1->adj_nei.push_back(node2);
node1->adj_nei.push_back(node4);
node2->adj_nei.push_back(node1);
node2->adj_nei.push_back(node3);
node3->adj_nei.push_back(node2);
node3->adj_nei.push_back(node4);
node4->adj_nei.push_back(node1);
node4->adj_nei.push_back(node3);
std::cout << "--- Original Graph ---" << std::endl;
printGraph(node1);
// --- Perform the deep copy ---
GraphCloner cloner;
GraphNode* cloned_graph_node = cloner.cloneGraph(node1);
std::cout << "\n--- Cloned Graph ---" << std::endl;
std::cout << "Notice the values are the same, but the memory addresses are all different." << std::endl;
printGraph(cloned_graph_node);
// --- Test the null case ---
std::cout << "\n--- Cloning a Null Graph ---" << std::endl;
GraphNode* null_graph = nullptr;
GraphNode* cloned_null_graph = cloner.cloneGraph(null_graph);
printGraph(cloned_null_graph);
// Note: In a real application, you would need to implement a function
// to delete all the allocated nodes to avoid memory leaks.
// For this example, we'll omit it for simplicity.
return 0;
}
Summary of Changes
- Null Check: Added
if (node == nullptr) return nullptr;
as the first line in the recursive function.
- Stateless Cloner: The
visited_map
is now created inside the public cloneGraph
method and passed by reference to the private cloneRecursive
helper. This encapsulates the state of a single copy operation, making the GraphCloner
object itself stateless and reusable.
- Clarity: Renamed the class to
GraphCloner
and the main function to cloneGraph
for better readability, which is common practice.
- Verification: Added a
main
function and a printGraph
utility to build a graph, clone it, and print both versions to prove that the deep copy was successful (same structure, different memory addresses).