Facebook Pixel

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 becomes 2 (from obj2)
  • The key "c" only exists in obj1, so it remains 3
  • The key "b" only exists in obj2, so it's added with value 2

The function should handle nested structures recursively, applying the same rules at each level of nesting.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. 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.

  2. Type Mismatch: If one value is an array and the other is a plain object, they're incompatible for merging, so we return obj2.

  3. 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 using for...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() returns true for any object (including arrays), but false for null and primitives
  • isArr() specifically identifies arrays

Main Algorithm Flow:

  1. Type Validation:

    if (!isObj(obj1) || !isObj(obj2)) {
        return obj2;
    }

    If either value is not an object (primitive, null, or undefined), return obj2 directly.

  2. 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.

  3. 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], modifying obj1 in place
    • Return the modified obj1

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 in obj1, obj1[key] is undefined, causing the recursive call to return obj2[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 Evaluator

Example 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:

  1. Initial call: deepMerge(obj1, obj2)

    • Both are objects (not arrays) ✓
    • Both have same type (object) ✓
    • Start iterating through obj2's keys: "a", "b", "c"
  2. Processing key "a":

    • Call deepMerge(obj1["a"], obj2["a"])deepMerge(1, 2)
    • 1 is not an object, so return 2
    • Set obj1["a"] = 2
    • Current obj1: {a: 2, b: {x: 10}}
  3. 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 return 20

    • 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}}

  4. Processing key "c":

    • Call deepMerge(obj1["c"], obj2["c"])deepMerge(undefined, 3)
    • undefined is not an object, so return 3
    • Set obj1["c"] = 3
    • Current obj1: {a: 2, b: {x: 10, y: 20}, c: 3}
  5. Final result: Return modified obj1 = {a: 2, b: {x: 10, y: 20}, c: 3}

Notice how:

  • Existing primitive values get replaced (a: 1a: 2)
  • Nested objects get merged recursively (b: {x: 10} merges with b: {y: 20})
  • New keys get added (c: 3 added from obj2)
  • 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:

  1. 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.

  2. 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 in obj1.

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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More