Facebook Pixel

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:

  1. Primitive values (numbers, strings, booleans, null, undefined): Two primitive values are deeply equal if they pass the strict equality check ===. For example, 5 === 5 returns true, but 5 === "5" returns false.

  2. 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]
  3. 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) returns true (primitive equality)
  • areDeeplyEqual([1, [2, 3]], [1, [2, 3]]) returns true (nested arrays with same structure)
  • areDeeplyEqual({a: 1, b: {c: 2}}, {a: 1, b: {c: 2}}) returns true (nested objects with same structure)
  • areDeeplyEqual([1, 2], [1, 3]) returns false (different array elements)
  • areDeeplyEqual({a: 1}, {a: 1, b: 2}) returns false (different keys)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

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

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

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

  4. 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 that o1 has (which would make o2[key] undefined), the recursive call will return false
  • 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 Evaluator

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

  1. 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...
  2. 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
  3. 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
  4. 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 return 3 === 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.

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

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

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

Load More