2755. Deep Merge of Two Objects 🔒
Problem Description
This problem asks you to implement a deep merge function that combines two values obj1
and obj2
according to specific rules.
The deep merge operation works as follows:
For Objects: When both values are objects (non-array), create a result that contains all keys from both objects. If a key exists in both objects, recursively deep merge the values associated with that key. If a key exists in only one object, include it directly in the result.
For Arrays: When both values are arrays, create a result array with length equal to the longer of the two arrays. Treat array indices like object keys - if an index exists in both arrays, deep merge the values at that index. If an index exists in only one array, use that value.
For Mismatched Types: If the values are not both objects, or if one is an array and the other is a plain object, simply return obj2
.
For Primitives: When either value is a primitive (not an object), return obj2
.
Example:
- Merging
{"a": 1, "c": 3}
with{"a": 2, "b": 2}
produces{"a": 2, "c": 3, "b": 2}
- The key
"a"
exists in both, so its value becomes2
(fromobj2
) - The key
"c"
only exists inobj1
, so it remains3
- The key
"b"
only exists inobj2
, so it's added with value2
The function should handle nested structures recursively, applying the same rules at each level of nesting.
Intuition
The key insight for solving this problem is recognizing that deep merge is inherently a recursive operation. When we encounter nested structures, we need to apply the same merging logic at each level of depth.
We start by identifying what type of values we're dealing with. The fundamental question at each step is: "Are both values objects that we can merge, or should we just return obj2
?"
First, we need helper functions to distinguish between different types:
isObj()
checks if a value is an object (including arrays)isArr()
specifically checks if a value is an array
The core logic follows this pattern:
-
Base Case: If either value isn't an object, we can't merge them, so we return
obj2
. This handles primitives like numbers, strings,null
, etc. -
Type Mismatch: If one value is an array and the other is a plain object, they're incompatible for merging, so we return
obj2
. -
Recursive Case: When both values are compatible (both arrays or both objects), we iterate through all keys/indices in
obj2
and recursively merge each property. The beauty of this approach is that JavaScript treats array indices the same as object keys when usingfor...in
loop.
The solution modifies obj1
in place by iterating through obj2
's properties and either:
- Directly assigning the value if the key doesn't exist in
obj1
- Recursively merging if the key exists in both
This approach naturally handles the array length requirement - when iterating through a longer array's indices, we automatically extend the shorter array by setting values at higher indices.
The recursion naturally terminates when we reach primitive values or incompatible types, at which point we simply use the value from obj2
.
Solution Approach
The implementation uses a recursive approach with type checking to handle different data structures appropriately.
Helper Functions:
const isObj = (obj: any) => obj && typeof obj === 'object';
const isArr = (obj: any) => Array.isArray(obj);
isObj()
returnstrue
for any object (including arrays), butfalse
fornull
and primitivesisArr()
specifically identifies arrays
Main Algorithm Flow:
-
Type Validation:
if (!isObj(obj1) || !isObj(obj2)) { return obj2; }
If either value is not an object (primitive,
null
, orundefined
), returnobj2
directly. -
Type Compatibility Check:
if (isArr(obj1) !== isArr(obj2)) { return obj2; }
If one is an array and the other is a plain object, they can't be merged, so return
obj2
. -
Recursive Merging:
for (const key in obj2) { obj1[key] = deepMerge(obj1[key], obj2[key]); } return obj1;
- Iterate through all properties/indices in
obj2
- For each key, recursively call
deepMerge
with the corresponding values from both objects - The result is assigned back to
obj1[key]
, modifyingobj1
in place - Return the modified
obj1
- Iterate through all properties/indices in
Key Implementation Details:
- The
for...in
loop works seamlessly for both objects and arrays since array indices are treated as string keys - When
obj2
has a key that doesn't exist inobj1
,obj1[key]
isundefined
, causing the recursive call to returnobj2[key]
directly - For arrays with different lengths, the loop automatically handles extending the shorter array by setting values at higher indices
- The in-place modification of
obj1
is efficient as it avoids creating unnecessary intermediate objects
Time Complexity: O(n)
where n
is the total number of properties across all nested levels in both objects.
Space Complexity: O(d)
where d
is the maximum depth of nesting, due to the recursive call stack.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through merging two simple nested objects to see how the recursive solution works:
Input:
obj1 = {a: 1, b: {x: 10}}
obj2 = {a: 2, b: {y: 20}, c: 3}
Step-by-step execution:
-
Initial call:
deepMerge(obj1, obj2)
- Both are objects (not arrays) ✓
- Both have same type (object) ✓
- Start iterating through
obj2
's keys:"a"
,"b"
,"c"
-
Processing key
"a"
:- Call
deepMerge(obj1["a"], obj2["a"])
→deepMerge(1, 2)
1
is not an object, so return2
- Set
obj1["a"] = 2
- Current
obj1
:{a: 2, b: {x: 10}}
- Call
-
Processing key
"b"
:- Call
deepMerge(obj1["b"], obj2["b"])
→deepMerge({x: 10}, {y: 20})
- Both are objects (not arrays) ✓
- Start iterating through
{y: 20}
's keys:"y"
3.1. Processing nested key
"y"
:-
Call
deepMerge(obj1["b"]["y"], obj2["b"]["y"])
→deepMerge(undefined, 20)
-
undefined
is not an object, so return20
-
Set
obj1["b"]["y"] = 20
-
obj1["b"]
becomes{x: 10, y: 20}
-
Return
{x: 10, y: 20}
from nested call -
Set
obj1["b"] = {x: 10, y: 20}
-
Current
obj1
:{a: 2, b: {x: 10, y: 20}}
- Call
-
Processing key
"c"
:- Call
deepMerge(obj1["c"], obj2["c"])
→deepMerge(undefined, 3)
undefined
is not an object, so return3
- Set
obj1["c"] = 3
- Current
obj1
:{a: 2, b: {x: 10, y: 20}, c: 3}
- Call
-
Final result: Return modified
obj1
={a: 2, b: {x: 10, y: 20}, c: 3}
Notice how:
- Existing primitive values get replaced (
a: 1
→a: 2
) - Nested objects get merged recursively (
b: {x: 10}
merges withb: {y: 20}
) - New keys get added (
c: 3
added fromobj2
) - The original nested value
x: 10
is preserved in the merged object
Solution Implementation
1def deepMerge(obj1, obj2):
2 """
3 Deep merge two objects or arrays recursively
4
5 Args:
6 obj1: The target object/array to merge into
7 obj2: The source object/array to merge from
8
9 Returns:
10 The merged result (modifies obj1 in place)
11 """
12
13 def is_object(value):
14 """Helper function to check if a value is an object (including lists/dicts)"""
15 return value is not None and isinstance(value, (dict, list))
16
17 def is_array(value):
18 """Helper function to check if a value is an array/list"""
19 return isinstance(value, list)
20
21 # If either value is not an object, return the second value
22 if not is_object(obj1) or not is_object(obj2):
23 return obj2
24
25 # If one is an array and the other is not, return the second value
26 if is_array(obj1) != is_array(obj2):
27 return obj2
28
29 # Handle list merging
30 if is_array(obj1) and is_array(obj2):
31 # For lists, we need to handle index-based merging
32 for i in range(len(obj2)):
33 if i < len(obj1):
34 # Recursively merge existing indices
35 obj1[i] = deepMerge(obj1[i], obj2[i])
36 else:
37 # Append new indices from obj2
38 obj1.append(obj2[i])
39 else:
40 # Handle dictionary merging
41 # Iterate through all properties in obj2 and merge them into obj1
42 for key in obj2:
43 # Recursively merge each property
44 obj1[key] = deepMerge(obj1.get(key), obj2[key])
45
46 # Return the modified obj1
47 return obj1
48
49
50# Example usage:
51# obj1 = {"a": 1, "c": 3}
52# obj2 = {"a": 2, "b": 2}
53# deepMerge(obj1, obj2) # {"a": 2, "c": 3, "b": 2}
54
1import java.util.*;
2
3public class DeepMergeUtil {
4
5 /**
6 * Deep merge two objects or arrays recursively
7 * @param obj1 - The target object/array to merge into
8 * @param obj2 - The source object/array to merge from
9 * @return The merged result (modifies obj1 in place)
10 */
11 public static Object deepMerge(Object obj1, Object obj2) {
12 // If either value is not an object (including null), return the second value
13 if (!isObject(obj1) || !isObject(obj2)) {
14 return obj2;
15 }
16
17 // If one is a List and the other is not, return the second value
18 if (isArray(obj1) != isArray(obj2)) {
19 return obj2;
20 }
21
22 // Handle List merging
23 if (isArray(obj1) && isArray(obj2)) {
24 List<Object> list1 = (List<Object>) obj1;
25 List<Object> list2 = (List<Object>) obj2;
26
27 // Merge elements from list2 into list1
28 for (int i = 0; i < list2.size(); i++) {
29 if (i < list1.size()) {
30 // Recursively merge existing elements
31 list1.set(i, deepMerge(list1.get(i), list2.get(i)));
32 } else {
33 // Add new elements from list2
34 list1.add(list2.get(i));
35 }
36 }
37 return list1;
38 }
39
40 // Handle Map merging
41 if (obj1 instanceof Map && obj2 instanceof Map) {
42 Map<String, Object> map1 = (Map<String, Object>) obj1;
43 Map<String, Object> map2 = (Map<String, Object>) obj2;
44
45 // Iterate through all entries in map2 and merge them into map1
46 for (Map.Entry<String, Object> entry : map2.entrySet()) {
47 String key = entry.getKey();
48 Object value2 = entry.getValue();
49
50 // Recursively merge each property
51 if (map1.containsKey(key)) {
52 map1.put(key, deepMerge(map1.get(key), value2));
53 } else {
54 map1.put(key, value2);
55 }
56 }
57 return map1;
58 }
59
60 // For other object types, return the second value
61 return obj2;
62 }
63
64 /**
65 * Helper method to check if a value is an object (not null and not primitive)
66 * @param value - The value to check
67 * @return true if the value is an object, false otherwise
68 */
69 private static boolean isObject(Object value) {
70 return value != null && (value instanceof Map || value instanceof List ||
71 value instanceof Object[]);
72 }
73
74 /**
75 * Helper method to check if a value is an array/list
76 * @param value - The value to check
77 * @return true if the value is a List or array, false otherwise
78 */
79 private static boolean isArray(Object value) {
80 return value instanceof List || value instanceof Object[];
81 }
82
83 /**
84 * Example usage:
85 * Map<String, Object> obj1 = new HashMap<>();
86 * obj1.put("a", 1);
87 * obj1.put("c", 3);
88 *
89 * Map<String, Object> obj2 = new HashMap<>();
90 * obj2.put("a", 2);
91 * obj2.put("b", 2);
92 *
93 * deepMerge(obj1, obj2); // {"a": 2, "c": 3, "b": 2}
94 */
95}
96
1#include <iostream>
2#include <unordered_map>
3#include <vector>
4#include <variant>
5#include <memory>
6#include <typeinfo>
7
8// Forward declaration
9class JsonValue;
10
11// Type alias for convenience
12using JsonObject = std::unordered_map<std::string, std::shared_ptr<JsonValue>>;
13using JsonArray = std::vector<std::shared_ptr<JsonValue>>;
14using JsonVariant = std::variant<std::nullptr_t, bool, int, double, std::string, JsonObject, JsonArray>;
15
16/**
17 * JsonValue class to represent any JSON-like value
18 */
19class JsonValue {
20public:
21 JsonVariant value;
22
23 // Constructors for different types
24 JsonValue() : value(nullptr) {}
25 JsonValue(std::nullptr_t) : value(nullptr) {}
26 JsonValue(bool val) : value(val) {}
27 JsonValue(int val) : value(val) {}
28 JsonValue(double val) : value(val) {}
29 JsonValue(const std::string& val) : value(val) {}
30 JsonValue(const JsonObject& val) : value(val) {}
31 JsonValue(const JsonArray& val) : value(val) {}
32
33 /**
34 * Check if the value is an object (including arrays)
35 * @return true if the value is an object or array, false otherwise
36 */
37 bool isObject() const {
38 return std::holds_alternative<JsonObject>(value) ||
39 std::holds_alternative<JsonArray>(value);
40 }
41
42 /**
43 * Check if the value is an array
44 * @return true if the value is an array, false otherwise
45 */
46 bool isArray() const {
47 return std::holds_alternative<JsonArray>(value);
48 }
49
50 /**
51 * Check if the value is a JSON object (not array)
52 * @return true if the value is a JSON object, false otherwise
53 */
54 bool isJsonObject() const {
55 return std::holds_alternative<JsonObject>(value);
56 }
57};
58
59/**
60 * Deep merge two objects or arrays recursively
61 * @param obj1 - The target object/array to merge into (modified in place)
62 * @param obj2 - The source object/array to merge from
63 * @return The merged result (modifies obj1 in place and returns it)
64 */
65std::shared_ptr<JsonValue> deepMerge(std::shared_ptr<JsonValue> obj1,
66 std::shared_ptr<JsonValue> obj2) {
67 // If obj1 is null, create a new JsonValue
68 if (!obj1) {
69 obj1 = std::make_shared<JsonValue>();
70 }
71
72 // If obj2 is null, return obj1 as is
73 if (!obj2) {
74 return obj1;
75 }
76
77 // If either value is not an object, return the second value
78 if (!obj1->isObject() || !obj2->isObject()) {
79 *obj1 = *obj2;
80 return obj1;
81 }
82
83 // If one is an array and the other is not, return the second value
84 if (obj1->isArray() != obj2->isArray()) {
85 *obj1 = *obj2;
86 return obj1;
87 }
88
89 // Handle array merging
90 if (obj1->isArray() && obj2->isArray()) {
91 auto& arr1 = std::get<JsonArray>(obj1->value);
92 const auto& arr2 = std::get<JsonArray>(obj2->value);
93
94 // Merge arrays by index
95 size_t maxSize = std::max(arr1.size(), arr2.size());
96 arr1.resize(maxSize);
97
98 for (size_t i = 0; i < arr2.size(); ++i) {
99 if (!arr1[i]) {
100 arr1[i] = std::make_shared<JsonValue>();
101 }
102 arr1[i] = deepMerge(arr1[i], arr2[i]);
103 }
104 }
105 // Handle object merging
106 else if (obj1->isJsonObject() && obj2->isJsonObject()) {
107 auto& map1 = std::get<JsonObject>(obj1->value);
108 const auto& map2 = std::get<JsonObject>(obj2->value);
109
110 // Iterate through all properties in obj2 and merge them into obj1
111 for (const auto& [key, value] : map2) {
112 // If key doesn't exist in obj1, create a new entry
113 if (map1.find(key) == map1.end()) {
114 map1[key] = std::make_shared<JsonValue>();
115 }
116 // Recursively merge each property
117 map1[key] = deepMerge(map1[key], value);
118 }
119 }
120
121 // Return the modified obj1
122 return obj1;
123}
124
125/**
126 * Example usage:
127 * JsonObject obj1Map = {{"a", std::make_shared<JsonValue>(1)},
128 * {"c", std::make_shared<JsonValue>(3)}};
129 * JsonObject obj2Map = {{"a", std::make_shared<JsonValue>(2)},
130 * {"b", std::make_shared<JsonValue>(2)}};
131 * auto obj1 = std::make_shared<JsonValue>(obj1Map);
132 * auto obj2 = std::make_shared<JsonValue>(obj2Map);
133 * deepMerge(obj1, obj2); // {"a": 2, "c": 3, "b": 2}
134 */
135
1/**
2 * Deep merge two objects or arrays recursively
3 * @param obj1 - The target object/array to merge into
4 * @param obj2 - The source object/array to merge from
5 * @returns The merged result (modifies obj1 in place)
6 */
7function deepMerge(obj1: any, obj2: any): any {
8 // Helper function to check if a value is an object (including arrays)
9 const isObject = (value: any): boolean => {
10 return value !== null && typeof value === 'object';
11 };
12
13 // Helper function to check if a value is an array
14 const isArray = (value: any): boolean => {
15 return Array.isArray(value);
16 };
17
18 // If either value is not an object, return the second value
19 if (!isObject(obj1) || !isObject(obj2)) {
20 return obj2;
21 }
22
23 // If one is an array and the other is not, return the second value
24 if (isArray(obj1) !== isArray(obj2)) {
25 return obj2;
26 }
27
28 // Iterate through all properties in obj2 and merge them into obj1
29 for (const key in obj2) {
30 // Recursively merge each property
31 obj1[key] = deepMerge(obj1[key], obj2[key]);
32 }
33
34 // Return the modified obj1
35 return obj1;
36}
37
38/**
39 * Example usage:
40 * let obj1 = {"a": 1, "c": 3}, obj2 = {"a": 2, "b": 2};
41 * deepMerge(obj1, obj2); // {"a": 2, "c": 3, "b": 2}
42 */
43
Time and Space Complexity
Time Complexity: O(n)
where n
is the total number of properties across all nested levels in both objects.
The algorithm performs a depth-first traversal through all properties in obj2
. For each property in obj2
, it either:
- Returns immediately if one or both values are non-objects (constant time)
- Returns immediately if there's a type mismatch between array and object (constant time)
- Recursively processes nested objects/arrays
Each property is visited exactly once, and the operations performed at each property (type checking, assignment) are O(1)
. Therefore, the overall time complexity is linear with respect to the total number of properties.
Space Complexity: O(d)
where d
is the maximum depth of nesting in the objects.
The space complexity comes from two sources:
-
Recursion Call Stack: The recursive calls create a call stack with maximum depth equal to the deepest nesting level in the objects. Each recursive call adds a frame to the stack, resulting in
O(d)
space for the call stack. -
In-place Modification: The algorithm modifies
obj1
in place rather than creating a new object, so no additional space is needed for storing the merged result beyond what already exists inobj1
.
The dominant factor is the recursion depth, making the overall space complexity O(d)
.
Common Pitfalls
1. Mutating the Original Object
The Problem:
The current implementation modifies obj1
in place, which can lead to unexpected side effects when the original object is used elsewhere in the code.
original = {"a": 1, "b": {"c": 2}} source = {"b": {"d": 3}} result = deepMerge(original, source) # original is now modified: {"a": 1, "b": {"c": 2, "d": 3}} # This might break other code that expects original to remain unchanged
Solution:
Create a deep copy of obj1
before merging to preserve the original:
import copy
def deepMerge(obj1, obj2):
def is_object(value):
return value is not None and isinstance(value, (dict, list))
def is_array(value):
return isinstance(value, list)
if not is_object(obj1) or not is_object(obj2):
return obj2
if is_array(obj1) != is_array(obj2):
return obj2
# Create a deep copy to avoid mutating the original
result = copy.deepcopy(obj1)
if is_array(result) and is_array(obj2):
for i in range(len(obj2)):
if i < len(result):
result[i] = deepMerge(result[i], obj2[i])
else:
result.append(obj2[i])
else:
for key in obj2:
result[key] = deepMerge(result.get(key), obj2[key])
return result
2. Circular References Causing Stack Overflow
The Problem: If either object contains circular references, the recursive function will enter an infinite loop and cause a stack overflow:
obj1 = {"a": 1} obj1["self"] = obj1 # Circular reference obj2 = {"b": 2} # deepMerge(obj1, obj2) would cause RecursionError
Solution: Track visited objects to detect and handle circular references:
def deepMerge(obj1, obj2, visited=None):
if visited is None:
visited = set()
def is_object(value):
return value is not None and isinstance(value, (dict, list))
def is_array(value):
return isinstance(value, list)
if not is_object(obj1) or not is_object(obj2):
return obj2
if is_array(obj1) != is_array(obj2):
return obj2
# Check for circular references
obj1_id = id(obj1)
obj2_id = id(obj2)
if obj1_id in visited or obj2_id in visited:
return obj2 # Or handle as needed
visited.add(obj1_id)
visited.add(obj2_id)
result = copy.deepcopy(obj1)
if is_array(result) and is_array(obj2):
for i in range(len(obj2)):
if i < len(result):
result[i] = deepMerge(result[i], obj2[i], visited.copy())
else:
result.append(obj2[i])
else:
for key in obj2:
result[key] = deepMerge(result.get(key), obj2[key], visited.copy())
return result
3. Incorrect Handling of Special Object Types
The Problem:
The current is_object
check treats all non-primitive types as mergeable objects, including dates, sets, or custom classes, which may not behave correctly:
from datetime import datetime obj1 = {"date": datetime.now()} obj2 = {"date": {"year": 2024}} # This would attempt to merge a datetime with a dict, causing errors
Solution: Be more specific about what types should be merged:
def is_mergeable_object(value):
"""Check if value is a plain dict or list that should be merged"""
return isinstance(value, (dict, list)) and \
not isinstance(value, (datetime, set, frozenset, bytes, bytearray))
def deepMerge(obj1, obj2):
if not is_mergeable_object(obj1) or not is_mergeable_object(obj2):
return obj2
# ... rest of the implementation
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!