Facebook Pixel

2700. Differences Between Two Objects 🔒

Problem Description

This problem asks you to compare two deeply nested objects or arrays and return a new object that represents only the differences between them.

The key requirements are:

  1. Compare corresponding properties: For each key that exists in both objects, compare their values
  2. Return differences as arrays: When values differ, represent the change as [obj1_value, obj2_value]
  3. Ignore added/removed keys: Keys that exist in only one object should not be included in the result
  4. Handle nested structures: The comparison should work recursively for nested objects and arrays
  5. Treat arrays like objects: Array indices are considered as keys (e.g., index 0 becomes key "0")

The function should handle several cases:

  • Primitive values: If two primitive values at the same key are different, return them as [old_value, new_value]
  • Identical values: If values are the same, don't include them in the result
  • Type differences: If the same key has different types (e.g., object vs array), return both as [obj1_value, obj2_value]
  • Nested differences: For nested objects/arrays, recursively find differences and only include keys with changes

For example:

  • If obj1.a = 1 and obj2.a = 2, the result includes "a": [1, 2]
  • If obj1.x = [] and obj2.x = [], nothing is included (empty arrays are equal)
  • If obj1.z.a = null and obj2.z.a = 2, the result includes "z": {"a": [null, 2]}
  • For arrays, if arr1[2] = 4 and arr2[2] = 3, the result includes "2": [4, 3]

The output should be a nested object where each leaf value is either a difference array [old, new] or another object containing differences. Empty objects indicate no differences were found.

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

Intuition

The core insight is that we need to perform a recursive comparison that handles different data types appropriately. Let's think through how to approach this step by step.

First, we need to recognize that when comparing two values, there are fundamentally three scenarios:

  1. The values have different types (one is an object, the other is a primitive, or one is an array and the other is an object)
  2. Both values are primitives (numbers, strings, booleans, null)
  3. Both values are objects/arrays that need deeper comparison

For different types, the comparison is straightforward - they're definitely different, so we return [obj1, obj2] immediately.

For primitive values, we simply check if they're equal. If they are, we return an empty object {} (no difference), otherwise we return [obj1, obj2].

The interesting case is when both values are objects or arrays. Here, we need to:

  • Find the keys that exist in both objects (intersection of keys)
  • For each common key, recursively compare the values
  • Only include a key in our result if its recursive comparison found differences

The key insight for handling the "ignore added/removed keys" requirement is to only iterate through keys that exist in both objects. We use Object.keys(obj1).filter(key => key in obj2) to find this intersection.

For the recursive step, we call objDiff on each pair of values for common keys. If the recursive call returns an empty object (no differences found), we don't include that key in our result. If it returns something (either a difference array or an object with differences), we include it.

The beauty of this approach is that arrays naturally work with the same logic since in JavaScript, arrays are objects where indices are keys. So [1, 2, 3] is treated like {"0": 1, "1": 2, "2": 3}.

The helper functions type() and isObject() help us cleanly determine whether we're dealing with objects/arrays that need recursive processing or primitives that can be compared directly. Using Object.prototype.toString.call() gives us a reliable way to distinguish between different types, even handling edge cases like null (which has typeof null === 'object' but isn't actually an object we can recurse into).

Solution Approach

The solution implements a recursive comparison algorithm with three helper functions working together to handle different data types and edge cases.

Main Function Structure:

The objDiff function follows this logic flow:

  1. First, check if the two inputs have different types using type(obj1) !== type(obj2). If they do, immediately return [obj1, obj2] since different types are always considered a difference.

  2. Next, check if we're dealing with non-objects (primitives) using !isObject(obj1). For primitives:

    • If they're equal (obj1 === obj2), return {} (no difference)
    • If they're different, return [obj1, obj2]
  3. For objects/arrays, create an empty diff object to collect differences, then:

    • Find common keys: Object.keys(obj1).filter(key => key in obj2)
    • For each common key, recursively call objDiff(obj1[key], obj2[key])
    • Only add to diff if the recursive call found differences (non-empty result)

Helper Functions:

  1. type(obj): Uses Object.prototype.toString.call(obj).slice(8, -1) to get the exact type. This method returns strings like "[object Array]" or "[object Object]", and we slice to extract just the type name ("Array", "Object", etc.). This is more reliable than typeof for distinguishing arrays from objects.

  2. isObject(obj): Checks if something is an object or array that we can recurse into. Returns true for objects and arrays, but false for null (which technically has typeof null === 'object' but can't be traversed).

Key Algorithm Details:

The recursive traversal pattern ensures we:

  • Only process keys present in both objects (intersection)
  • Build the result object incrementally, adding only changed properties
  • Handle nested structures by recursively applying the same logic

For arrays, since JavaScript treats array indices as object keys, the same logic applies. An array [1, 2, 3] is processed like {"0": 1, "1": 2, "2": 3}.

Example Walkthrough:

For obj1 = {"a": 1, "b": {"c": 2}} and obj2 = {"a": 1, "b": {"c": 3}}:

  1. Compare types - both are objects, continue
  2. Find common keys: ["a", "b"]
  3. Process "a": objDiff(1, 1) returns {} (no difference)
  4. Process "b": objDiff({"c": 2}, {"c": 3}) recursively:
    • Both are objects, find common keys: ["c"]
    • Process "c": objDiff(2, 3) returns [2, 3]
    • Return {"c": [2, 3]}
  5. Final result: {"b": {"c": [2, 3]}}

The Object.keys(subDiff).length check ensures we only include keys where actual differences were found, filtering out unchanged properties automatically.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to see how the algorithm works:

Given:

obj1 = {
  "x": 5,
  "y": {
    "a": 1,
    "b": 2
  },
  "z": [1, 2, 3]
}

obj2 = {
  "x": 5,
  "y": {
    "a": 1,
    "b": 3
  },
  "z": [1, 2, 4]
}

Step-by-step execution:

  1. Initial call: objDiff(obj1, obj2)

    • Both are objects (same type) → continue to recursive comparison
    • Common keys: ["x", "y", "z"]
  2. Process key "x": objDiff(5, 5)

    • Both are primitives and equal
    • Return {} (no difference)
    • Don't add to result
  3. Process key "y": objDiff({a: 1, b: 2}, {a: 1, b: 3})

    • Both are objects → recurse deeper
    • Common keys: ["a", "b"]

    a. Process "a": objDiff(1, 1)

    • Primitives and equal → return {}

    b. Process "b": objDiff(2, 3)

    • Primitives but different → return [2, 3]

    • Build result for "y": {b: [2, 3]}

    • Add to main result

  4. Process key "z": objDiff([1, 2, 3], [1, 2, 4])

    • Both are arrays (treated as objects with indices as keys)
    • Common keys: ["0", "1", "2"]

    a. Process "0": objDiff(1, 1){}

    b. Process "1": objDiff(2, 2){}

    c. Process "2": objDiff(3, 4)[3, 4]

    • Build result for "z": {"2": [3, 4]}
    • Add to main result

Final Output:

{
  "y": {
    "b": [2, 3]
  },
  "z": {
    "2": [3, 4]
  }
}

Notice how:

  • Equal values (like x: 5) don't appear in the output
  • Nested differences are preserved in the structure
  • Array differences show the index as a string key
  • Only the actual changed values appear as [old, new] pairs

Solution Implementation

1def objDiff(obj1, obj2):
2    """
3    Compares two objects recursively and returns their differences
4  
5    Args:
6        obj1: First object to compare
7        obj2: Second object to compare
8  
9    Returns:
10        An object containing the differences between obj1 and obj2
11        - Returns [obj1, obj2] if types differ or values differ
12        - Returns empty dict {} if values are equal
13        - Returns nested diff dict for matching keys
14    """
15    # If the types of the two objects are different, return both values
16    if get_type(obj1) != get_type(obj2):
17        return [obj1, obj2]
18  
19    # If obj1 is not a dict (primitive value or list)
20    if not is_object(obj1):
21        # Return empty dict if values are equal, otherwise return both values
22        return {} if obj1 == obj2 else [obj1, obj2]
23  
24    # Initialize the result dict to store differences
25    diff = {}
26  
27    # Get keys that exist in both objects
28    same_keys = [key for key in obj1.keys() if key in obj2]
29  
30    # Iterate through common keys and recursively find differences
31    for key in same_keys:
32        sub_diff = objDiff(obj1[key], obj2[key])
33      
34        # Only add to diff if there are actual differences
35        # Check if sub_diff is non-empty (works for both dict and list)
36        if (isinstance(sub_diff, dict) and len(sub_diff) > 0) or isinstance(sub_diff, list):
37            diff[key] = sub_diff
38  
39    return diff
40
41
42def get_type(obj):
43    """
44    Gets the precise type of an object
45  
46    Args:
47        obj: The object to get the type of
48  
49    Returns:
50        A string representing the type (e.g., 'dict', 'list', 'int', 'str')
51    """
52    # Get the type name from the type object
53    return type(obj).__name__
54
55
56def is_object(obj):
57    """
58    Check if a value is a dictionary (equivalent to JavaScript object)
59  
60    Args:
61        obj: The value to check
62  
63    Returns:
64        True if obj is a dict, False otherwise
65    """
66    # In Python, we check if it's a dict since that's the equivalent of JS objects
67    return isinstance(obj, dict)
68
1import java.util.*;
2
3public class ObjectDifferenceUtil {
4  
5    /**
6     * Compares two objects recursively and returns their differences
7     * @param obj1 - First object to compare
8     * @param obj2 - Second object to compare
9     * @return An object containing the differences between obj1 and obj2
10     *         - Returns List containing [obj1, obj2] if types differ or values differ
11     *         - Returns empty Map {} if values are equal
12     *         - Returns nested diff Map for matching keys
13     */
14    public static Object objDiff(Object obj1, Object obj2) {
15        // If the types of the two objects are different, return both values
16        if (!type(obj1).equals(type(obj2))) {
17            return Arrays.asList(obj1, obj2);
18        }
19      
20        // If obj1 is not an object (primitive value or non-Map type)
21        if (!isObject(obj1)) {
22            // Return empty map if values are equal, otherwise return both values
23            return Objects.equals(obj1, obj2) ? new HashMap<>() : Arrays.asList(obj1, obj2);
24        }
25      
26        // Initialize the result map to store differences
27        Map<String, Object> diff = new HashMap<>();
28      
29        // Cast objects to maps for key-based comparison
30        Map<String, Object> map1 = (Map<String, Object>) obj1;
31        Map<String, Object> map2 = (Map<String, Object>) obj2;
32      
33        // Get keys that exist in both objects
34        Set<String> sameKeys = new HashSet<>();
35        for (String key : map1.keySet()) {
36            if (map2.containsKey(key)) {
37                sameKeys.add(key);
38            }
39        }
40      
41        // Iterate through common keys and recursively find differences
42        for (String key : sameKeys) {
43            Object subDiff = objDiff(map1.get(key), map2.get(key));
44          
45            // Only add to diff if there are actual differences
46            // Check if subDiff is a non-empty Map
47            if (subDiff instanceof Map && !((Map<?, ?>) subDiff).isEmpty()) {
48                diff.put(key, subDiff);
49            }
50            // Check if subDiff is a List (indicates difference found)
51            else if (subDiff instanceof List) {
52                diff.put(key, subDiff);
53            }
54        }
55      
56        return diff;
57    }
58  
59    /**
60     * Gets the precise type of an object using its class name
61     * @param obj - The object to get the type of
62     * @return A string representing the type (e.g., 'HashMap', 'ArrayList', 'Integer')
63     */
64    public static String type(Object obj) {
65        // Handle null case
66        if (obj == null) {
67            return "Null";
68        }
69      
70        // Get the simple class name for the object
71        return obj.getClass().getSimpleName();
72    }
73  
74    /**
75     * Type guard to check if a value is an object (specifically a Map in Java context)
76     * @param obj - The value to check
77     * @return True if obj is a Map object, false otherwise
78     */
79    public static boolean isObject(Object obj) {
80        // In Java context, we treat Map as the equivalent of JavaScript object
81        // Check if obj is not null and is an instance of Map
82        return obj != null && obj instanceof Map;
83    }
84}
85
1#include <unordered_map>
2#include <vector>
3#include <string>
4#include <any>
5#include <typeinfo>
6#include <memory>
7#include <variant>
8
9// Define a type for representing differences
10// Can be either a pair of values or a map of differences
11using DiffResult = std::variant<
12    std::unordered_map<std::string, std::any>,
13    std::vector<std::any>
14>;
15
16/**
17 * Gets the type name of an object using RTTI
18 * @param obj - The object to get the type of
19 * @returns A string representing the type name
20 */
21std::string type(const std::any& obj) {
22    // If the any object has no value, return "null"
23    if (!obj.has_value()) {
24        return "null";
25    }
26  
27    // Get the type info and return the name
28    return std::string(obj.type().name());
29}
30
31/**
32 * Checks if a std::any contains a map type
33 * @param obj - The value to check
34 * @returns True if obj contains an unordered_map, false otherwise
35 */
36bool isObject(const std::any& obj) {
37    // Check if the any object contains an unordered_map
38    try {
39        // Attempt to cast to map type
40        auto* mapPtr = std::any_cast<std::unordered_map<std::string, std::any>>(&obj);
41        return mapPtr != nullptr;
42    } catch (...) {
43        return false;
44    }
45}
46
47/**
48 * Compares two objects recursively and returns their differences
49 * @param obj1 - First object to compare
50 * @param obj2 - Second object to compare
51 * @returns A std::any containing the differences between obj1 and obj2
52 *          - Returns vector with [obj1, obj2] if types differ or values differ
53 *          - Returns empty map {} if values are equal
54 *          - Returns nested diff map for matching keys
55 */
56std::any objDiff(const std::any& obj1, const std::any& obj2) {
57    // If the types of the two objects are different, return both values
58    if (type(obj1) != type(obj2)) {
59        std::vector<std::any> result;
60        result.push_back(obj1);
61        result.push_back(obj2);
62        return result;
63    }
64  
65    // If obj1 is not an object (primitive value)
66    if (!isObject(obj1)) {
67        // Check if the values are equal
68        bool areEqual = false;
69      
70        // Compare based on actual type
71        try {
72            // Try different primitive types
73            if (obj1.type() == typeid(int)) {
74                areEqual = std::any_cast<int>(obj1) == std::any_cast<int>(obj2);
75            } else if (obj1.type() == typeid(double)) {
76                areEqual = std::any_cast<double>(obj1) == std::any_cast<double>(obj2);
77            } else if (obj1.type() == typeid(std::string)) {
78                areEqual = std::any_cast<std::string>(obj1) == std::any_cast<std::string>(obj2);
79            } else if (obj1.type() == typeid(bool)) {
80                areEqual = std::any_cast<bool>(obj1) == std::any_cast<bool>(obj2);
81            }
82        } catch (...) {
83            areEqual = false;
84        }
85      
86        // Return empty map if values are equal, otherwise return both values
87        if (areEqual) {
88            return std::unordered_map<std::string, std::any>{};
89        } else {
90            std::vector<std::any> result;
91            result.push_back(obj1);
92            result.push_back(obj2);
93            return result;
94        }
95    }
96  
97    // Initialize the result map to store differences
98    std::unordered_map<std::string, std::any> diff;
99  
100    // Cast both objects to maps
101    const auto& map1 = std::any_cast<const std::unordered_map<std::string, std::any>&>(obj1);
102    const auto& map2 = std::any_cast<const std::unordered_map<std::string, std::any>&>(obj2);
103  
104    // Get keys that exist in both objects
105    std::vector<std::string> sameKeys;
106    for (const auto& [key, value] : map1) {
107        if (map2.find(key) != map2.end()) {
108            sameKeys.push_back(key);
109        }
110    }
111  
112    // Iterate through common keys and recursively find differences
113    for (const std::string& key : sameKeys) {
114        // Recursively find differences for this key
115        std::any subDiff = objDiff(map1.at(key), map2.at(key));
116      
117        // Check if subDiff has actual differences
118        bool hasDifferences = false;
119      
120        // Check if it's a map and if it has entries
121        try {
122            const auto& diffMap = std::any_cast<const std::unordered_map<std::string, std::any>&>(subDiff);
123            hasDifferences = !diffMap.empty();
124        } catch (...) {
125            // If it's not a map, it must be a vector (meaning there are differences)
126            hasDifferences = true;
127        }
128      
129        // Only add to diff if there are actual differences
130        if (hasDifferences) {
131            diff[key] = subDiff;
132        }
133    }
134  
135    return diff;
136}
137
1/**
2 * Compares two objects recursively and returns their differences
3 * @param obj1 - First object to compare
4 * @param obj2 - Second object to compare
5 * @returns An object containing the differences between obj1 and obj2
6 *          - Returns [obj1, obj2] if types differ or values differ
7 *          - Returns empty object {} if values are equal
8 *          - Returns nested diff object for matching keys
9 */
10function objDiff(obj1: any, obj2: any): any {
11    // If the types of the two objects are different, return both values
12    if (type(obj1) !== type(obj2)) {
13        return [obj1, obj2];
14    }
15  
16    // If obj1 is not an object (primitive value)
17    if (!isObject(obj1)) {
18        // Return empty object if values are equal, otherwise return both values
19        return obj1 === obj2 ? {} : [obj1, obj2];
20    }
21  
22    // Initialize the result object to store differences
23    const diff: Record<string, unknown> = {};
24  
25    // Get keys that exist in both objects
26    const sameKeys: string[] = Object.keys(obj1).filter((key: string) => key in obj2);
27  
28    // Iterate through common keys and recursively find differences
29    sameKeys.forEach((key: string) => {
30        const subDiff: any = objDiff(obj1[key], obj2[key]);
31      
32        // Only add to diff if there are actual differences
33        if (Object.keys(subDiff).length > 0) {
34            diff[key] = subDiff;
35        }
36    });
37  
38    return diff;
39}
40
41/**
42 * Gets the precise type of an object using Object.prototype.toString
43 * @param obj - The object to get the type of
44 * @returns A string representing the type (e.g., 'Object', 'Array', 'Number')
45 */
46function type(obj: unknown): string {
47    // Extract type from "[object Type]" format
48    return Object.prototype.toString.call(obj).slice(8, -1);
49}
50
51/**
52 * Type guard to check if a value is an object (not null and typeof 'object')
53 * @param obj - The value to check
54 * @returns True if obj is an object, false otherwise
55 */
56function isObject(obj: unknown): obj is Record<string, unknown> {
57    return typeof obj === 'object' && obj !== null;
58}
59

Time and Space Complexity

Time Complexity: O(n) where n is the total number of nodes (keys and values) in both objects combined.

The algorithm performs a depth-first traversal through the object structure:

  • For each key that exists in both objects, we make a recursive call to compare the values
  • The filter operation to find common keys takes O(k) where k is the number of keys in obj1
  • The forEach loop iterates through all common keys
  • Each node (primitive value or object) is visited exactly once during the traversal
  • The type() function and equality checks are O(1) operations

Therefore, the overall time complexity is linear with respect to the total number of nodes in the input objects.

Space Complexity: O(d + m) where d is the maximum depth of the object tree and m is the size of the output difference object.

The space complexity consists of two components:

  • Call stack space: O(d) - The recursive calls can go as deep as the maximum nesting level of the objects
  • Output space: O(m) - The space needed to store the difference object, which in the worst case (when all values differ) could be O(n)

In the best case where objects are identical, the output space would be O(1) (empty object), and in the worst case where all values differ, it would approach O(n). The auxiliary space used for the sameKeys array is O(k) where k is the number of keys, which is bounded by O(n).

Common Pitfalls

1. Incorrect Handling of Arrays as Objects

The Python implementation has a critical flaw: it doesn't handle arrays (lists) properly. The problem states that arrays should be treated like objects with indices as keys, but the current is_object() function only checks for dictionaries, causing arrays to be compared as primitives.

Problem Example:

arr1 = [1, 2, 3]
arr2 = [1, 2, 4]
# Current code returns [arr1, arr2] instead of {"2": [3, 4]}

Solution:

def is_object(obj):
    """Check if a value is a dict or list (container types)"""
    return isinstance(obj, (dict, list))

def objDiff(obj1, obj2):
    # ... existing type check code ...
  
    if not is_object(obj1):
        return {} if obj1 == obj2 else [obj1, obj2]
  
    diff = {}
  
    # Handle both dicts and lists
    if isinstance(obj1, dict):
        same_keys = [key for key in obj1.keys() if key in obj2]
    else:  # Both are lists
        # Find common indices
        min_len = min(len(obj1), len(obj2))
        same_keys = list(range(min_len))
  
    for key in same_keys:
        sub_diff = objDiff(obj1[key], obj2[key])
        if (isinstance(sub_diff, dict) and len(sub_diff) > 0) or isinstance(sub_diff, list):
            diff[str(key) if isinstance(obj1, list) else key] = sub_diff
  
    return diff

2. Type Comparison Issues with None/null Values

The get_type() function might not handle None correctly when comparing with other falsy values, leading to unexpected behavior.

Problem Example:

obj1 = {"a": None}
obj2 = {"a": 0}
# Both might be treated similarly in some comparisons

Solution:

def get_type(obj):
    """Gets the precise type of an object, handling None specially"""
    if obj is None:
        return 'NoneType'
    return type(obj).__name__

3. Deep Equality Check for Complex Objects

Using == for equality checking can fail for complex nested structures or objects with circular references.

Problem Example:

import numpy as np
arr1 = np.array([1, 2, 3])
arr2 = np.array([1, 2, 3])
# arr1 == arr2 returns an array, not a boolean

Solution:

def safe_equals(obj1, obj2):
    """Safely compare two objects for equality"""
    try:
        # For numpy arrays or similar
        if hasattr(obj1, '__array__'):
            return np.array_equal(obj1, obj2)
        return obj1 == obj2
    except (ValueError, TypeError):
        # Fallback for objects that can't be compared directly
        return False

# Then in objDiff:
if not is_object(obj1):
    return {} if safe_equals(obj1, obj2) else [obj1, obj2]

4. Mixed Type Keys in Result

When handling arrays, the code should ensure that array indices are converted to strings in the result to maintain consistency with JavaScript behavior.

Problem Example:

# Current code might mix integer and string keys
result = {0: [1, 2], "a": [3, 4]}  # Inconsistent key types

Solution: Always convert array indices to strings when adding to the diff object (as shown in the first solution above with str(key)).

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How many times is a tree node visited in a depth first search?


Recommended Readings

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

Load More