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 === 5returnstrue, 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
aand recursively compare the values - At the next level, find key
band 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 541/** 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} 1011#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} 931/** 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} 58Solution 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
areDeeplyEqualon 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
o2doesn't have a key thato1has (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
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.
Which technique can we use to find the middle of a linked list?
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 If you prefer videos here's a video that explains recursion in a fun and easy way 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 him
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!