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:
- Compare corresponding properties: For each key that exists in both objects, compare their values
- Return differences as arrays: When values differ, represent the change as
[obj1_value, obj2_value]
- Ignore added/removed keys: Keys that exist in only one object should not be included in the result
- Handle nested structures: The comparison should work recursively for nested objects and arrays
- 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
andobj2.a = 2
, the result includes"a": [1, 2]
- If
obj1.x = []
andobj2.x = []
, nothing is included (empty arrays are equal) - If
obj1.z.a = null
andobj2.z.a = 2
, the result includes"z": {"a": [null, 2]}
- For arrays, if
arr1[2] = 4
andarr2[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.
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:
- 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)
- Both values are primitives (numbers, strings, booleans, null)
- 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:
-
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. -
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]
- If they're equal (
-
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)
- Find common keys:
Helper Functions:
-
type(obj)
: UsesObject.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 thantypeof
for distinguishing arrays from objects. -
isObject(obj)
: Checks if something is an object or array that we can recurse into. Returnstrue
for objects and arrays, butfalse
fornull
(which technically hastypeof 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}}
:
- Compare types - both are objects, continue
- Find common keys:
["a", "b"]
- Process "a":
objDiff(1, 1)
returns{}
(no difference) - 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]}
- Both are objects, find common keys:
- 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 EvaluatorExample 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:
-
Initial call:
objDiff(obj1, obj2)
- Both are objects (same type) → continue to recursive comparison
- Common keys:
["x", "y", "z"]
-
Process key "x":
objDiff(5, 5)
- Both are primitives and equal
- Return
{}
(no difference) - Don't add to result
-
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
-
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 takesO(k)
wherek
is the number of keys inobj1
- 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 areO(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 beO(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)
).
How many times is a tree node visited in a depth first search?
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!