2628. JSON Deep Equal

MediumJavaScript
Leetcode Link

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

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

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.

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

Which technique can we use to find the middle of a linked list?


Recommended Readings

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

Load More