Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Deep Clone, Constraint by Constraint
The incremental-coding round again, on a problem where stage three genuinely breaks the obvious design: copy an object, then nested structures, then one containing a cycle, then one where two keys must still share the same clone. Tests whether your first version can absorb the next requirement.
The problem
Implement deep_clone(value) / deepClone(value) through four stated stages. (1) Copy a flat object of primitives. (2) Recurse into nested objects and arrays, so no nested container is shared with the source. (3) Handle a structure containing a cycle without overflowing the stack. (4) Preserve shared identity: if two properties of the source referenced the same object, the corresponding properties of the clone must reference the same clone. Primitives, null, and non-container values are returned as-is.
Your turn — write it
Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.
Implement deep_clone / deepClone satisfying all four stages at once.
Examples and constraints 5 examples
s = {"a": 1, "b": "x"}; c = deep_clone(s)c == s → True; c is s → Falses = {"xs": [1, [2, 3]]}; c = deep_clone(s)c["xs"][1] == [2, 3] → True; c["xs"][1] is s["xs"][1] → Falseo = {"a": 1}; o["self"] = o; c = deep_clone(o)c["self"] is c → True; c["self"] is o → Falsesh = {"s": 1}; c = deep_clone({"p": sh, "q": sh})c["p"] is c["q"] → True; c["p"] is sh → Falsec = deep_clone({"p": {"s": 1}, "q": {"s": 1}})c["p"] is c["q"] → False; c["p"] == c["q"] → True- Never return the same container object the caller passed in.
- A cycle must not overflow the stack, and the cycle must close onto the clone rather than the original.
- Two source properties pointing at one object must produce two clone properties pointing at one clone.
- Primitives and
nullare returned unchanged; arrays must clone as arrays.
Hints 4 ideas
- Keep a map from each original container to its clone, and check it before you recurse.
- A visited *set* stops the crash and loses shared identity — you need the original-to-clone *map*.
- Create the empty clone and record it in the map before filling in its properties, or the cycle never terminates.
- Arrays are containers too: check for them explicitly so they clone as arrays, not as objects with numeric keys.
Approach, complexity & discussion — open after you solve
Start from brute force
The first version most people write is plain recursion: a dict becomes {k: deep_clone(v) for k, v in value.items()}, a list becomes [deep_clone(x) for x in value], and anything else is returned as-is. It is correct for stages 1 and 2, because it builds a new container for every container it reaches, and on a tree it costs O(n) time and O(depth) stack. Stage 3 breaks it loudly: on o["self"] = o it recurses until Python raises RecursionError. Stage 4 breaks it quietly: a shared object is cloned once per reference, so c["p"] is c["q"] is False, and 20 levels that each hold two references to the next produce 2,097,151 dict copies instead of 21.
The approach
One map, consulted before recursing, fixes stages 3 and 4 together. The insight is that a cycle and a shared reference are the same event: reaching an object you have already started cloning. If a map from original to clone answers “what did I turn this into?”, returning that answer ends the cycle and keeps both shared references on one clone. The invariant: every container entered so far has exactly one clone, recorded in seen before any of its children are visited.
The reference passes seen, a dict keyed by id(value), through every call. Non-containers return immediately. For a dict or list it computes key = id(value) and returns seen[key] if present. Otherwise it creates the empty out, stores seen[key] = out first, and only then fills out by recursing into each item or value. Recording before filling is what lets a cycle find a clone that is still under construction, and keying by identity rather than by value keeps two equal but distinct objects as two clones.
Walk through an example
The object the reference builds in its own demo covers stages 2 to 4 at once: o = {"a": 1, "n": {"x": 2}} with o["self"] = o and o["p"] and o["q"] both set to one shared = {"s": 1}.
| step | call on | in seen? | what happens | len(seen) |
|---|---|---|---|---|
| 1 | o | no | dict: out = {}, seen[id(o)] = out, then loop over a, n, self, p, q | 1 |
| 2 | o["a"] = 1 | — | primitive, returned as-is | 1 |
| 3 | o["n"] | no | new dict clone recorded, then "x": 2 copied into it | 2 |
| 4 | o["self"], which is o | yes | return the clone of o from step 1, still being filled | 2 |
| 5 | o["p"], which is shared | no | new dict clone recorded, then "s": 1 copied into it | 3 |
| 6 | o["q"], which is shared | yes | return the clone made in step 5 | 3 |
| 7 | return | — | c is not o, c["self"] is c, c["p"] is c["q"], c["n"] is not o["n"]: True True True True | 3 |
Complexity
Reference: O(V + E) time, where V is the number of distinct containers and E the number of references inside them, because each container is cloned once and each reference followed once; O(V) space for seen plus O(depth) stack. Plain recursion: time proportional to the structure unfolded into a tree, which is exponential when objects are shared and never ends on a cycle.
Edge cases to test
- A self-cycle,
o = {"a": 1}; o["self"] = o:c["self"] is cmust be True. A visited set that returns the original instead givesc["self"] is o, a clone that points back into the source. - A cycle through a list,
xs = [1]; xs.append(xs): lists go through the sameseenmap, soc[1] is candc is not xs. - Equal but distinct objects,
{"p": {"s": 1}, "q": {"s": 1}}:c["p"] is c["q"]must stay False. The map is keyed by identity,id(value)in Python and the object itself in a JavaScriptMap; a key built from the contents would merge them. - Sharing that compounds: 20 levels where each holds two references to the next. The map clones 21 dicts; plain recursion makes 2,097,151.
- Deep nesting with no cycle at all, 2,000 dicts each inside the next: the recursive reference still raises
RecursionError, because Python’s default limit is 1,000 frames. Only an explicit stack removes the depth limit. - Primitives and
None:deep_clone(7),deep_clone(None)anddeep_clone("s")return the value itself. In TypeScript,typeof null === "object", so the null check has to come before the object branch.
Common mistakes
- Using a plain set of visited objects to stop the recursion, which breaks the cycle but returns the wrong shape — you need the map from original to clone, not a membership test.
- Cloning shared sub-objects twice, so
out.a === out.bis false whensrc.a === src.bwas true. - Recording the clone in the map only after its children are filled: the cycle reaches the original again before the entry exists, and the recursion never ends.
- Forgetting that arrays are objects too, and cloning them into plain objects with numeric keys.
- Treating
nullas an object and recursing into it — the classictypeof null === "object"trap in JavaScript.
Follow-ups an interviewer asks next
- The structure is a million levels deep. Remove the recursion.
- Use an explicit stack of (original, clone) pairs. Create each clone, record it in
seen, and push the pair; when you pop a pair, fill the clone’s slots, creating, recording and pushing any child that is not yet inseen. The filling order changes but the result does not: every container is still cloned once, and cycles and sharing resolve through the same map. - Support sets, dates and class instances too.
- Add a branch per type. Copy a date by value, rebuild a set from cloned members, and for a class instance create an empty instance without running its constructor, record it in
seen, then clone its fields. Python’scopy.deepcopyworks this way: it keeps a memo keyed byid(), and a class can override the behavior with a__deepcopy__method. - Why not just call the standard library?
- In production, do. Python’s
copy.deepcopyuses the same identity map, and JavaScript’sstructuredClonepreserves cycles and shared references. Neither clones everything:structuredClonethrows aDataCloneErroron a function and returns a class instance as a plain object with its prototype gone. Building it yourself is how you learn where those limits are. - What is wrong with JSON.parse(JSON.stringify(x))?
- It throws a
TypeErroron a cycle and creates a separate copy of a shared object for every reference, so stages 3 and 4 both fail. It is also lossy: dates become strings,undefinedvalues and functions are dropped from objects, andNaNandInfinitybecomenull. It is a serialization round trip, not a clone.
Where this shows up
Every serializer, state-management library and undo stack meets this. The reason it is a good incremental round is that each stage is a reasonable next sentence from an interviewer, and the third one genuinely breaks the obvious design — so it tests whether you refactor calmly or patch around the symptom. Narrating "a visited set fixes the crash but loses shared identity, so I want a map instead" is the whole signal.
Reference solution
Reference solution — Python
def deep_clone(value, seen=None):
if seen is None:
seen = {}
# primitives and None pass straight through
if not isinstance(value, (dict, list)):
return value
key = id(value)
if key in seen: # stage 3 + 4: already cloned this exact object
return seen[key]
if isinstance(value, list):
out = []
seen[key] = out # record BEFORE recursing, or a cycle never ends
for item in value:
out.append(deep_clone(item, seen))
return out
out = {}
seen[key] = out
for k, v in value.items():
out[k] = deep_clone(v, seen)
return out
o = {"a": 1, "n": {"x": 2}}
o["self"] = o
shared = {"s": 1}
o["p"], o["q"] = shared, shared
c = deep_clone(o)
print(c is not o, c["self"] is c, c["p"] is c["q"], c["n"] is not o["n"])
# -> True True True TrueReference solution — TypeScript
function deepClone(value: any, seen = new Map<any, any>()): any {
// primitives and null pass straight through (typeof null === "object")
if (value === null || typeof value !== "object") return value;
if (seen.has(value)) return seen.get(value); // stage 3 + 4
if (Array.isArray(value)) {
const out: any[] = [];
seen.set(value, out); // record BEFORE recursing
for (const item of value) out.push(deepClone(item, seen));
return out;
}
const out: any = {};
seen.set(value, out);
for (const k of Object.keys(value)) out[k] = deepClone(value[k], seen);
return out;
}
const o: any = { a: 1, n: { x: 2 } };
o.self = o;
const shared = { s: 1 };
o.p = shared; o.q = shared;
const c = deepClone(o);
console.log(c !== o, c.self === c, c.p === c.q, c.n !== o.n);
// -> true true true trueQuestions about Deep Clone, Constraint by Constraint
How do you deep clone an object with circular references?
Keep a map from every original object to its clone, and consult it before cloning anything. When you meet an object for the first time, create its empty clone, record it in the map, and only then clone its children. When you meet an object that is already in the map, return its clone instead of recursing. The cycle then closes onto the clone rather than the original: in this challenge, cloning an o whose self key points at o gives a clone whose self key points at the clone itself.
What is the difference between a shallow copy and a deep copy?
A shallow copy creates a new top-level container but reuses every object inside it, so changing a nested list through the copy changes the original too. A deep copy creates new containers all the way down, so the two structures share nothing mutable. In Python, dict.copy and copy.copy are shallow and copy.deepcopy is deep; in JavaScript, spread syntax and Object.assign are shallow and structuredClone is deep. Stage 1 of this challenge only needs a shallow copy, and stage 2 makes it deep.
Why does a visited set not work for deep cloning a graph?
A visited set can tell you that you have seen an object, but not what you turned it into. Stopping the recursion there leaves nothing correct to return: returning the original makes the clone point back into the source, and building a fresh copy duplicates a shared object. A map from original to clone answers both questions at once, because a repeat visit returns the existing clone, which ends the cycle and keeps shared references pointing at a single object.
Does JSON.parse(JSON.stringify(obj)) deep clone an object?
Only for plain data with no cycles and no sharing that matters. It throws a TypeError on a circular structure, it creates a separate copy of a shared object for every reference to it, and it is lossy: dates become strings, undefined values and functions are dropped from objects, and NaN and Infinity become null. structuredClone, available in modern browsers and Node.js, keeps cycles, shared references and dates intact, but it still cannot clone functions.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- SoftmaxThe function at the end of every classifier and language model: turn raw scores (logits) into a probability distribution. Implement the numerically stable version so big logits do not overflow. Solve it in Python or TypeScript.Read →
- Cosine SimilarityThe measure behind every embedding search and RAG system: how aligned are two vectors, ignoring their length? Dot product over the product of magnitudes — 1 identical, 0 orthogonal, -1 opposite. Solve it in Python or TypeScript.Read →
- Top-K RetrievalThe core of the "R" in RAG: given a query embedding and a set of document embeddings, return the indices of the k most similar docs by cosine similarity, with a stable tie-break. Solve it in Python or TypeScript.Read →