2628. JSON Deep Equal 🔒
Problem Description
This problem asks you to implement a function that checks if two values are deeply equal. Deep equality means comparing not just the surface level values, but also recursively comparing all nested structures.
The function takes two parameters o1
and o2
and returns a boolean indicating whether they are deeply equal according to these rules:
-
Primitive values (numbers, strings, booleans, null, undefined): Two primitive values are deeply equal if they pass the strict equality check
===
. For example,5 === 5
returnstrue
, but5 === "5"
returnsfalse
. -
Arrays: Two arrays are deeply equal if:
- They have the same length
- Each element at the same index is deeply equal
- The order matters -
[1, 2]
is not deeply equal to[2, 1]
-
Objects: Two objects are deeply equal if:
- They have exactly the same set of keys
- The value associated with each key in one object is deeply equal to the value of the same key in the other object
- Extra or missing keys make objects not deeply equal
The problem guarantees that both input values are valid JSON (output of JSON.parse
), so you don't need to handle special JavaScript objects like functions, undefined values as object properties, or circular references.
Examples of deep equality:
areDeeplyEqual(1, 1)
returnstrue
(primitive equality)areDeeplyEqual([1, [2, 3]], [1, [2, 3]])
returnstrue
(nested arrays with same structure)areDeeplyEqual({a: 1, b: {c: 2}}, {a: 1, b: {c: 2}})
returnstrue
(nested objects with same structure)areDeeplyEqual([1, 2], [1, 3])
returnsfalse
(different array elements)areDeeplyEqual({a: 1}, {a: 1, b: 2})
returnsfalse
(different keys)
Intuition
The key insight is that deep equality requires a recursive approach since we need to compare nested structures at all levels. We can think of this problem as a tree traversal where each node represents a value that might contain other values.
The natural approach is to handle different data types separately:
-
Start with the base case: For primitive values (numbers, strings, booleans, null), we can directly use
===
to compare them. This becomes our recursion termination condition - when we reach primitive values, we stop drilling down and return the comparison result. -
Handle type mismatches early: If two values have different types (one is an array and the other is an object, or one is primitive and the other is not), they can't be equal. Checking this early saves unnecessary computation.
-
Arrays require element-by-element comparison: Since arrays must have the same elements in the same order, we first check if their lengths match. If they do, we recursively compare each element at corresponding indices. If any pair of elements isn't deeply equal, the arrays aren't deeply equal.
-
Objects require key-value pair comparison: For objects, we need to ensure both objects have the exact same keys. We can get all keys from both objects and first check if the number of keys matches. Then, for each key, we recursively check if the values associated with that key in both objects are deeply equal.
The beauty of this recursive solution is that it naturally handles arbitrary nesting. When comparing {a: {b: {c: 1}}}
with another object, the function will:
- Compare the objects at the top level
- Find key
a
and recursively compare the values - At the next level, find key
b
and recursively compare again - Continue until reaching the primitive value
1
This depth-first approach ensures we thoroughly check every nested level while maintaining a clean, understandable code structure.
Solution Approach
The implementation uses a recursive function that handles different data types systematically. Let's walk through the solution step by step:
1. Handle Primitive Values and Null
if (o1 === null || typeof o1 !== 'object') { return o1 === o2; }
First, we check if o1
is a primitive value or null
. In JavaScript, typeof null
returns 'object'
, so we explicitly check for null
. If o1
is primitive or null
, we simply return o1 === o2
to check strict equality.
2. Type Mismatch Check
if (typeof o1 !== typeof o2) { return false; }
If the types of o1
and o2
don't match, they can't be deeply equal, so we return false
immediately.
3. Array vs Object Distinction
if (Array.isArray(o1) !== Array.isArray(o2)) {
return false;
}
Since arrays are objects in JavaScript, we need to explicitly check if one value is an array and the other is a plain object. If they differ, return false
.
4. Array Comparison
if (Array.isArray(o1)) {
if (o1.length !== o2.length) {
return false;
}
for (let i = 0; i < o1.length; i++) {
if (!areDeeplyEqual(o1[i], o2[i])) {
return false;
}
}
return true;
}
For arrays:
- First check if lengths are equal - different lengths mean not deeply equal
- Iterate through each index and recursively call
areDeeplyEqual
on corresponding elements - If any pair of elements isn't deeply equal, return
false
- If all elements match, return
true
5. Object Comparison
const keys1 = Object.keys(o1);
const keys2 = Object.keys(o2);
if (keys1.length !== keys2.length) {
return false;
}
for (const key of keys1) {
if (!areDeeplyEqual(o1[key], o2[key])) {
return false;
}
}
return true;
For objects:
- Extract keys from both objects using
Object.keys()
- Compare the number of keys - different counts mean not deeply equal
- Iterate through keys of the first object
- For each key, recursively compare the values in both objects
- If any value doesn't match or if
o2
doesn't have a key thato1
has (which would makeo2[key]
undefined), the recursive call will returnfalse
- If all key-value pairs match, return
true
Time Complexity: O(n)
where n
is the total number of primitive values in the nested structure, as we visit each value once.
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 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to see how the solution works:
Example: Check if o1 = {a: [1, 2], b: {c: 3}}
and o2 = {a: [1, 2], b: {c: 3}}
are deeply equal.
Step-by-step execution:
-
Initial call:
areDeeplyEqual(o1, o2)
- Both are objects (not null, not primitive)
- Both have same type (
object
) - Neither is an array
- Extract keys:
keys1 = ['a', 'b']
,keys2 = ['a', 'b']
- Same length (2 keys each) ✓
- Now check each key...
-
Check key 'a':
areDeeplyEqual([1, 2], [1, 2])
- Both are arrays ✓
- Same length (2) ✓
- Compare index 0:
areDeeplyEqual(1, 1)
- Both are primitives
- Return
1 === 1
=true
✓
- Compare index 1:
areDeeplyEqual(2, 2)
- Both are primitives
- Return
2 === 2
=true
✓
- All array elements match, return
true
✓
-
Check key 'b':
areDeeplyEqual({c: 3}, {c: 3})
- Both are objects (not arrays)
- Extract keys:
keys1 = ['c']
,keys2 = ['c']
- Same length (1 key each) ✓
- Check key 'c':
areDeeplyEqual(3, 3)
- Both are primitives
- Return
3 === 3
=true
✓
- All object properties match, return
true
✓
-
Final result: Since both keys 'a' and 'b' have deeply equal values, the function returns
true
.
Counter-example: If we had o2 = {a: [1, 2], b: {c: 4}}
instead, the execution would differ at step 3:
- When checking key 'c':
areDeeplyEqual(3, 4)
would return3 === 4
=false
- This would cause the check for key 'b' to return
false
- The overall function would return
false
Solution Implementation
1def areDeeplyEqual(o1, o2):
2 """
3 Deeply compares two values for equality, including nested objects and arrays
4
5 Args:
6 o1: First value to compare
7 o2: Second value to compare
8
9 Returns:
10 bool: True if values are deeply equal, False otherwise
11 """
12 # Handle primitives and None values
13 if o1 is None or not isinstance(o1, (dict, list)):
14 return o1 == o2
15
16 # Check if types are different
17 if type(o1) != type(o2):
18 return False
19
20 # Check if one is list and the other is not
21 if isinstance(o1, list) != isinstance(o2, list):
22 return False
23
24 # Handle list comparison
25 if isinstance(o1, list):
26 # Check list lengths
27 if len(o1) != len(o2):
28 return False
29
30 # Compare each list element recursively
31 for i in range(len(o1)):
32 if not areDeeplyEqual(o1[i], o2[i]):
33 return False
34
35 return True
36 else:
37 # Handle dictionary comparison
38 keys1 = list(o1.keys())
39 keys2 = list(o2.keys())
40
41 # Check if dictionaries have different number of keys
42 if len(keys1) != len(keys2):
43 return False
44
45 # Compare each property recursively
46 for key in keys1:
47 # Check if key exists in second dictionary
48 if key not in o2:
49 return False
50 if not areDeeplyEqual(o1[key], o2[key]):
51 return False
52
53 return True
54
1/**
2 * Deeply compares two values for equality, including nested objects and arrays
3 * @param o1 - First value to compare
4 * @param o2 - Second value to compare
5 * @return true if values are deeply equal, false otherwise
6 */
7public static boolean areDeeplyEqual(Object o1, Object o2) {
8 // Handle null values and reference equality
9 if (o1 == o2) {
10 return true;
11 }
12
13 // If one is null and the other isn't, they're not equal
14 if (o1 == null || o2 == null) {
15 return false;
16 }
17
18 // Check if classes are different
19 if (!o1.getClass().equals(o2.getClass())) {
20 return false;
21 }
22
23 // Handle primitive wrapper types and strings
24 if (o1 instanceof Number || o1 instanceof Boolean ||
25 o1 instanceof Character || o1 instanceof String) {
26 return o1.equals(o2);
27 }
28
29 // Handle array comparison
30 if (o1.getClass().isArray()) {
31 // Get array lengths
32 int length1 = java.lang.reflect.Array.getLength(o1);
33 int length2 = java.lang.reflect.Array.getLength(o2);
34
35 // Check if array lengths are different
36 if (length1 != length2) {
37 return false;
38 }
39
40 // Compare each array element recursively
41 for (int i = 0; i < length1; i++) {
42 Object element1 = java.lang.reflect.Array.get(o1, i);
43 Object element2 = java.lang.reflect.Array.get(o2, i);
44 if (!areDeeplyEqual(element1, element2)) {
45 return false;
46 }
47 }
48
49 return true;
50 }
51
52 // Handle Map comparison (closest equivalent to JavaScript objects)
53 if (o1 instanceof java.util.Map) {
54 java.util.Map<?, ?> map1 = (java.util.Map<?, ?>) o1;
55 java.util.Map<?, ?> map2 = (java.util.Map<?, ?>) o2;
56
57 // Check if maps have different number of keys
58 if (map1.size() != map2.size()) {
59 return false;
60 }
61
62 // Compare each key-value pair recursively
63 for (java.util.Map.Entry<?, ?> entry : map1.entrySet()) {
64 Object key = entry.getKey();
65 // Check if key exists in second map
66 if (!map2.containsKey(key)) {
67 return false;
68 }
69 // Compare values recursively
70 if (!areDeeplyEqual(entry.getValue(), map2.get(key))) {
71 return false;
72 }
73 }
74
75 return true;
76 }
77
78 // Handle List comparison
79 if (o1 instanceof java.util.List) {
80 java.util.List<?> list1 = (java.util.List<?>) o1;
81 java.util.List<?> list2 = (java.util.List<?>) o2;
82
83 // Check if lists have different sizes
84 if (list1.size() != list2.size()) {
85 return false;
86 }
87
88 // Compare each element recursively
89 for (int i = 0; i < list1.size(); i++) {
90 if (!areDeeplyEqual(list1.get(i), list2.get(i))) {
91 return false;
92 }
93 }
94
95 return true;
96 }
97
98 // For other objects, use equals method
99 return o1.equals(o2);
100}
101
1#include <vector>
2#include <unordered_map>
3#include <any>
4#include <typeinfo>
5#include <string>
6
7/**
8 * Deeply compares two values for equality, including nested objects and arrays
9 * @param o1 - First value to compare
10 * @param o2 - Second value to compare
11 * @returns true if values are deeply equal, false otherwise
12 */
13bool areDeeplyEqual(const std::any& o1, const std::any& o2) {
14 // Check if both are empty
15 if (!o1.has_value() && !o2.has_value()) {
16 return true;
17 }
18
19 // Check if only one is empty
20 if (!o1.has_value() || !o2.has_value()) {
21 return false;
22 }
23
24 // Check if types are different
25 if (o1.type() != o2.type()) {
26 return false;
27 }
28
29 // Handle primitive types
30 if (o1.type() == typeid(int)) {
31 return std::any_cast<int>(o1) == std::any_cast<int>(o2);
32 }
33 if (o1.type() == typeid(double)) {
34 return std::any_cast<double>(o1) == std::any_cast<double>(o2);
35 }
36 if (o1.type() == typeid(bool)) {
37 return std::any_cast<bool>(o1) == std::any_cast<bool>(o2);
38 }
39 if (o1.type() == typeid(std::string)) {
40 return std::any_cast<std::string>(o1) == std::any_cast<std::string>(o2);
41 }
42
43 // Handle vector (array) comparison
44 if (o1.type() == typeid(std::vector<std::any>)) {
45 const auto& vec1 = std::any_cast<const std::vector<std::any>&>(o1);
46 const auto& vec2 = std::any_cast<const std::vector<std::any>&>(o2);
47
48 // Check array lengths
49 if (vec1.size() != vec2.size()) {
50 return false;
51 }
52
53 // Compare each array element recursively
54 for (size_t i = 0; i < vec1.size(); i++) {
55 if (!areDeeplyEqual(vec1[i], vec2[i])) {
56 return false;
57 }
58 }
59
60 return true;
61 }
62
63 // Handle map (object) comparison
64 if (o1.type() == typeid(std::unordered_map<std::string, std::any>)) {
65 const auto& map1 = std::any_cast<const std::unordered_map<std::string, std::any>&>(o1);
66 const auto& map2 = std::any_cast<const std::unordered_map<std::string, std::any>&>(o2);
67
68 // Check if objects have different number of keys
69 if (map1.size() != map2.size()) {
70 return false;
71 }
72
73 // Compare each property recursively
74 for (const auto& [key, value] : map1) {
75 // Check if key exists in second map
76 auto it = map2.find(key);
77 if (it == map2.end()) {
78 return false;
79 }
80
81 // Compare values recursively
82 if (!areDeeplyEqual(value, it->second)) {
83 return false;
84 }
85 }
86
87 return true;
88 }
89
90 // For any other types, return false
91 return false;
92}
93
1/**
2 * Deeply compares two values for equality, including nested objects and arrays
3 * @param o1 - First value to compare
4 * @param o2 - Second value to compare
5 * @returns true if values are deeply equal, false otherwise
6 */
7function areDeeplyEqual(o1: any, o2: any): boolean {
8 // Handle primitives and null values
9 if (o1 === null || typeof o1 !== 'object') {
10 return o1 === o2;
11 }
12
13 // Check if types are different
14 if (typeof o1 !== typeof o2) {
15 return false;
16 }
17
18 // Check if one is array and the other is not
19 if (Array.isArray(o1) !== Array.isArray(o2)) {
20 return false;
21 }
22
23 // Handle array comparison
24 if (Array.isArray(o1)) {
25 // Check array lengths
26 if (o1.length !== o2.length) {
27 return false;
28 }
29
30 // Compare each array element recursively
31 for (let i = 0; i < o1.length; i++) {
32 if (!areDeeplyEqual(o1[i], o2[i])) {
33 return false;
34 }
35 }
36
37 return true;
38 } else {
39 // Handle object comparison
40 const keys1: string[] = Object.keys(o1);
41 const keys2: string[] = Object.keys(o2);
42
43 // Check if objects have different number of keys
44 if (keys1.length !== keys2.length) {
45 return false;
46 }
47
48 // Compare each property recursively
49 for (const key of keys1) {
50 if (!areDeeplyEqual(o1[key], o2[key])) {
51 return false;
52 }
53 }
54
55 return true;
56 }
57}
58
Time and Space Complexity
Time Complexity: O(n)
where n
is the total number of primitive values (leaves) in both objects combined.
The algorithm performs a depth-first traversal through both objects simultaneously. Each primitive value is visited exactly once during the comparison. For nested objects and arrays, the algorithm recursively explores all properties/elements, but each property or element is only checked once. The operations at each node (type checking, array checking, key comparisons) are all O(1)
operations. Therefore, the overall time complexity is linear with respect to the total number of primitive values that need to be compared.
Space Complexity: O(d)
where d
is the maximum depth of nesting in the objects.
The space complexity is determined by the recursive call stack. In the worst case, when dealing with deeply nested structures, the recursion depth equals the maximum nesting level of the objects. Each recursive call adds a new frame to the call stack with constant space for local variables (keys1
, keys2
, loop variables). The algorithm doesn't create copies of the objects being compared, only storing references and keys. For a completely flat object with no nesting, the space complexity would be O(1)
(excluding the space for storing the keys array which is O(k)
where k
is the number of keys at that level).
Common Pitfalls
1. Not Checking Key Existence in Second Object
The Pitfall:
In the Python implementation's dictionary comparison section, we iterate through keys1
and compare values, but we don't explicitly verify that all keys from o2
exist in o1
. While we check the length of keys, this alone doesn't guarantee both objects have the exact same set of keys.
Consider this edge case:
o1 = {"a": 1, "b": 2} o2 = {"a": 1, "c": 2}
Both have 2 keys, but the keys are different. The current implementation would check o1["c"]
which would raise a KeyError
or return an incorrect result if we're not careful.
The Solution: Add an explicit check to ensure the second object doesn't have extra keys:
def areDeeplyEqual(o1, o2):
# ... previous code ...
if isinstance(o1, list):
# ... list handling ...
else:
# Handle dictionary comparison
keys1 = set(o1.keys())
keys2 = set(o2.keys())
# Check if the key sets are identical
if keys1 != keys2:
return False
# Compare each property recursively
for key in keys1:
if not areDeeplyEqual(o1[key], o2[key]):
return False
return True
2. Type Checking Inconsistency Between Python and JavaScript
The Pitfall:
The Python code uses isinstance(o1, (dict, list))
to check for non-primitive types, but this doesn't perfectly mirror JavaScript's behavior. In Python, other iterable types like tuples or sets might pass through incorrectly, and the type checking logic could fail for edge cases.
The Solution: Be more explicit about the types we're handling:
def areDeeplyEqual(o1, o2):
# Define what we consider as primitive types
primitive_types = (int, float, str, bool, type(None))
# Handle primitives explicitly
if isinstance(o1, primitive_types):
return o1 == o2
# Ensure both are either list or dict (the only non-primitive JSON types)
if not isinstance(o1, (list, dict)) or not isinstance(o2, (list, dict)):
return False
# Continue with type-specific comparison...
3. Float Comparison Precision Issues
The Pitfall: When comparing floating-point numbers, precision issues can cause unexpected results:
o1 = {"value": 0.1 + 0.2} o2 = {"value": 0.3} # These might not be considered equal due to floating-point arithmetic
The Solution:
Since the problem states inputs are valid JSON output from JSON.parse
, and JSON numbers are handled consistently, this is less of an issue. However, if you need to handle floating-point comparison more robustly:
def areDeeplyEqual(o1, o2, epsilon=1e-9):
# For float comparison
if isinstance(o1, float) and isinstance(o2, float):
return abs(o1 - o2) < epsilon
# ... rest of the implementation
Note: This modification should only be used if the problem specifically requires handling floating-point precision issues, as it changes the strict equality semantics.
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
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!