2823. Deep Object Filter 🔒
Problem Description
This problem asks you to implement a deepFilter
function that recursively filters an object or array based on a given filter function fn
.
The function takes two parameters:
obj
: An object or array to be filteredfn
: A filter function that returnstrue
orfalse
for primitive values
The filtering process works as follows:
-
For primitive values (numbers, strings, booleans, etc.): Apply the filter function
fn
. Iffn
returnsfalse
, the value should be removed. -
For arrays: Recursively filter each element. After filtering, if the array becomes empty, it should be removed entirely (return
undefined
). -
For objects: Recursively filter each property value. If a property's filtered value becomes
undefined
, remove that property. If the object becomes empty after filtering, it should be removed entirely (returnundefined
). -
Return value: The function returns the filtered object/array, or
undefined
if the result is completely empty.
The key insight is that this is a deep filter - it doesn't just filter top-level properties, but recursively processes nested structures. Empty containers (objects with no properties or empty arrays) that result from the filtering process are considered invalid and should be removed from the parent structure.
For example, if you have an object like {a: 1, b: {c: 2}}
and the filter function only accepts values greater than 5, the entire structure would return undefined
because:
1
fails the filter2
fails the filter, making{c: 2}
become an empty object{}
- The empty object
{}
is removed - The top-level object becomes empty and returns
undefined
Intuition
The key insight is that we need to process the data structure from the bottom up - we can't know whether a container (object or array) should be kept until we've processed all its contents. This naturally suggests a recursive depth-first search (DFS) approach.
Think about it this way: if we have a nested structure like {a: {b: {c: 1}}}
, we need to:
- First check if
1
passes the filter - If it doesn't,
{c: 1}
becomes empty - An empty
{}
means{b: {}}
also becomes empty - Finally,
{a: {}}
becomes empty too
This bottom-up evaluation means we need to recursively process children before deciding what to do with their parent container.
The solution uses a helper function dfs
that handles three distinct cases:
-
Base case (primitives): When we hit a primitive value, we simply apply the filter function. This is our recursion termination point - return the value if
fn(data)
is true, otherwise returnundefined
. -
Arrays: Map each element through recursive
dfs
calls, then filter out anyundefined
results. If the resulting array is empty, we returnundefined
to signal that this entire array should be removed from its parent. -
Objects: Iterate through each key-value pair, recursively process the value, and only keep the key if the processed value isn't
undefined
. After processing all keys, check if we have any keys left - if not, returnundefined
.
The pattern of returning undefined
to indicate "remove this from parent" is elegant because it works uniformly across all levels of nesting. Each level only needs to check if its child returned undefined
to know whether to include it or not.
Solution Approach
The implementation uses a recursive depth-first search (DFS) pattern with a helper function to traverse and filter the data structure.
Main Function Structure:
function deepFilter(obj: Record<string, any>, fn: Function): Record<string, any> | undefined {
const dfs = (data: any): any => {
// Helper function logic
};
return dfs(obj);
}
The DFS Helper Function Implementation:
The dfs
function handles three different data types:
- Array Processing:
if (Array.isArray(data)) {
const res = data.map(dfs).filter((item: any) => item !== undefined);
return res.length > 0 ? res : undefined;
}
- Use
map(dfs)
to recursively process each element - Filter out
undefined
values (elements that didn't pass the filter) - Return the filtered array if it has elements, otherwise return
undefined
- Object Processing:
if (typeof data === 'object' && data !== null) {
const res: Record<string, any> = {};
for (const key in data) {
if (data.hasOwnProperty(key)) {
const filteredValue = dfs(data[key]);
if (filteredValue !== undefined) {
res[key] = filteredValue;
}
}
}
return Object.keys(res).length > 0 ? res : undefined;
}
- Create a new empty object
res
to store filtered results - Iterate through each property using
for...in
loop - Use
hasOwnProperty
to ensure we only process direct properties - Recursively filter each property value with
dfs(data[key])
- Only add properties whose filtered values are not
undefined
- Return the object if it has properties, otherwise return
undefined
- Primitive Value Processing:
return fn(data) ? data : undefined;
- Apply the filter function directly to primitive values
- Return the value if
fn(data)
is truthy, otherwise returnundefined
Key Design Decisions:
- Using
undefined
as a removal signal: When any level returnsundefined
, the parent level knows to exclude that value/property - Bottom-up processing: The recursive nature ensures we process leaf nodes first, then work our way up
- Creating new objects/arrays: Instead of modifying in place, we create new filtered structures, maintaining immutability
hasOwnProperty
check: Ensures we don't accidentally process inherited properties from the prototype chain
The algorithm has a time complexity of O(n)
where n
is the total number of nodes (including all nested properties and array elements) since we visit each node exactly once.
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 understand how the deepFilter
function works:
Input:
obj = {
a: 1,
b: {
c: 2,
d: -1,
e: {
f: 3
}
},
g: [1, 2, -3, {h: -4}]
}
fn = (val) => val > 0 // Keep only positive numbers
Step-by-step execution:
-
Start with root object - Call
dfs(obj)
- Since it's an object, iterate through each property
-
Process
a: 1
- Call
dfs(1)
→ primitive value - Apply filter:
fn(1)
returnstrue
(1 > 0) - Return
1
✓
- Call
-
Process
b: {c: 2, d: -1, e: {f: 3}}
- Call
dfs({c: 2, d: -1, e: {f: 3}})
→ object, so recurse on each property
3.1. Process
c: 2
dfs(2)
→fn(2)
returnstrue
→ keep2
✓
3.2. Process
d: -1
dfs(-1)
→fn(-1)
returnsfalse
→ returnundefined
✗
3.3. Process
e: {f: 3}
-
dfs({f: 3})
→ object, recurse -
Process
f: 3
→dfs(3)
→fn(3)
returnstrue
→ keep3
✓ -
Object
{f: 3}
has properties, so return{f: 3}
✓ -
After processing all of
b
's properties:{c: 2, e: {f: 3}}
(d is removed)
- Call
-
Process
g: [1, 2, -3, {h: -4}]
- Call
dfs([1, 2, -3, {h: -4}])
→ array, so map each element
4.1.
dfs(1)
→fn(1)
returnstrue
→1
✓4.2.
dfs(2)
→fn(2)
returnstrue
→2
✓4.3.
dfs(-3)
→fn(-3)
returnsfalse
→undefined
✗4.4.
dfs({h: -4})
→ object-
Process
h: -4
→dfs(-4)
→fn(-4)
returnsfalse
→undefined
✗ -
Object becomes empty
{}
→ returnundefined
✗ -
After mapping:
[1, 2, undefined, undefined]
-
After filtering out
undefined
:[1, 2]
✓
- Call
-
Final result:
{ a: 1, b: { c: 2, e: { f: 3 } }, g: [1, 2] }
Key observations from this walkthrough:
- Negative values (
-1
,-3
,-4
) are filtered out - The object
{h: -4}
becomes empty after filtering and is removed entirely from the array - The property
d: -1
is removed from objectb
- The recursive nature ensures all nested levels are properly filtered
- Empty containers are automatically removed (like the
{h: -4}
object)
Solution Implementation
1def deepFilter(obj, fn):
2 """
3 Deeply filters an object or array based on a predicate function.
4 Recursively traverses nested structures and removes values that don't satisfy the predicate.
5 Empty objects and arrays resulting from filtering are also removed.
6
7 Args:
8 obj: The input object to be filtered (dict, list, or any type)
9 fn: The predicate function to test primitive values
10
11 Returns:
12 The filtered object/array, or None if all values are filtered out
13 """
14
15 def dfs(data):
16 """
17 Recursive helper function to perform depth-first traversal and filtering
18
19 Args:
20 data: Current data being processed (can be any type)
21
22 Returns:
23 Filtered data or None if filtered out
24 """
25
26 # Handle list case: recursively filter each element
27 if isinstance(data, list):
28 # Map each element through dfs and filter out None values
29 filtered_list = []
30 for item in data:
31 filtered_item = dfs(item)
32 if filtered_item is not None:
33 filtered_list.append(filtered_item)
34
35 # Return the filtered list if non-empty, otherwise None
36 return filtered_list if len(filtered_list) > 0 else None
37
38 # Handle dict case: recursively filter each property
39 if isinstance(data, dict):
40 filtered_dict = {}
41
42 # Iterate through all key-value pairs of the dictionary
43 for key, value in data.items():
44 # Recursively filter the value
45 filtered_value = dfs(value)
46
47 # Only include the key-value pair if its filtered value is not None
48 if filtered_value is not None:
49 filtered_dict[key] = filtered_value
50
51 # Return the filtered dict if it has properties, otherwise None
52 return filtered_dict if len(filtered_dict) > 0 else None
53
54 # Handle primitive values: apply the predicate function
55 return data if fn(data) else None
56
57 # Start the recursive filtering from the root object
58 return dfs(obj)
59
1import java.util.*;
2import java.util.function.Predicate;
3
4/**
5 * Utility class for deep filtering of complex data structures
6 */
7public class DeepFilterUtil {
8
9 /**
10 * Deeply filters an object or array based on a predicate function.
11 * Recursively traverses nested structures and removes values that don't satisfy the predicate.
12 * Empty objects and arrays resulting from filtering are also removed.
13 *
14 * @param obj The input object to be filtered (can be Map, List, or primitive wrapper)
15 * @param fn The predicate function to test primitive values
16 * @return The filtered object/array, or null if all values are filtered out
17 */
18 public static Object deepFilter(Object obj, Predicate<Object> fn) {
19 return dfs(obj, fn);
20 }
21
22 /**
23 * Recursive helper function to perform depth-first traversal and filtering
24 *
25 * @param data Current data being processed (can be any type)
26 * @param fn The predicate function to test primitive values
27 * @return Filtered data or null if filtered out
28 */
29 private static Object dfs(Object data, Predicate<Object> fn) {
30 // Handle List case: recursively filter each element
31 if (data instanceof List) {
32 List<Object> originalList = (List<Object>) data;
33 List<Object> filteredList = new ArrayList<>();
34
35 // Map each element through dfs and filter out null values
36 for (Object item : originalList) {
37 Object filteredItem = dfs(item, fn);
38 if (filteredItem != null) {
39 filteredList.add(filteredItem);
40 }
41 }
42
43 // Return the filtered list if non-empty, otherwise null
44 return filteredList.isEmpty() ? null : filteredList;
45 }
46
47 // Handle Map case: recursively filter each property
48 if (data instanceof Map) {
49 Map<String, Object> originalMap = (Map<String, Object>) data;
50 Map<String, Object> filteredMap = new HashMap<>();
51
52 // Iterate through all entries of the map
53 for (Map.Entry<String, Object> entry : originalMap.entrySet()) {
54 // Recursively filter the entry value
55 Object filteredValue = dfs(entry.getValue(), fn);
56
57 // Only include the entry if its filtered value is not null
58 if (filteredValue != null) {
59 filteredMap.put(entry.getKey(), filteredValue);
60 }
61 }
62
63 // Return the filtered map if it has entries, otherwise null
64 return filteredMap.isEmpty() ? null : filteredMap;
65 }
66
67 // Handle primitive values and other objects: apply the predicate function
68 // This includes String, Number types, Boolean, and other objects
69 return fn.test(data) ? data : null;
70 }
71}
72
1#include <unordered_map>
2#include <vector>
3#include <functional>
4#include <variant>
5#include <memory>
6#include <any>
7
8/**
9 * Deeply filters an object or array based on a predicate function.
10 * Recursively traverses nested structures and removes values that don't satisfy the predicate.
11 * Empty objects and arrays resulting from filtering are also removed.
12 *
13 * Note: This C++ implementation uses std::any to handle dynamic typing similar to JavaScript.
14 * In production code, consider using a more type-safe approach with templates or variant types.
15 */
16class DeepFilterUtil {
17public:
18 using Object = std::unordered_map<std::string, std::any>;
19 using Array = std::vector<std::any>;
20 using Predicate = std::function<bool(const std::any&)>;
21
22 /**
23 * Main function to deeply filter an object based on a predicate function
24 *
25 * @param obj - The input object to be filtered
26 * @param fn - The predicate function to test primitive values
27 * @return The filtered object/array wrapped in std::any, or empty std::any if all values are filtered out
28 */
29 static std::any deepFilter(const Object& obj, const Predicate& fn) {
30 return dfs(std::any(obj), fn);
31 }
32
33private:
34 /**
35 * Recursive helper function to perform depth-first traversal and filtering
36 *
37 * @param data - Current data being processed (can be any type)
38 * @param fn - The predicate function to test primitive values
39 * @return Filtered data or empty std::any if filtered out
40 */
41 static std::any dfs(const std::any& data, const Predicate& fn) {
42 // Handle array case: recursively filter each element
43 if (data.type() == typeid(Array)) {
44 const Array& arr = std::any_cast<const Array&>(data);
45 Array filteredArray;
46
47 // Map each element through dfs and filter out empty values
48 for (const auto& item : arr) {
49 std::any filteredItem = dfs(item, fn);
50 if (filteredItem.has_value()) {
51 filteredArray.push_back(filteredItem);
52 }
53 }
54
55 // Return the filtered array if non-empty, otherwise empty std::any
56 if (!filteredArray.empty()) {
57 return std::any(filteredArray);
58 }
59 return std::any();
60 }
61
62 // Handle object case: recursively filter each property
63 if (data.type() == typeid(Object)) {
64 const Object& obj = std::any_cast<const Object&>(data);
65 Object filteredObject;
66
67 // Iterate through all properties of the object
68 for (const auto& [key, value] : obj) {
69 // Recursively filter the property value
70 std::any filteredValue = dfs(value, fn);
71
72 // Only include the property if its filtered value is not empty
73 if (filteredValue.has_value()) {
74 filteredObject[key] = filteredValue;
75 }
76 }
77
78 // Return the filtered object if it has properties, otherwise empty std::any
79 if (!filteredObject.empty()) {
80 return std::any(filteredObject);
81 }
82 return std::any();
83 }
84
85 // Handle primitive values: apply the predicate function
86 // If predicate returns true, return the data; otherwise return empty std::any
87 if (fn(data)) {
88 return data;
89 }
90 return std::any();
91 }
92};
93
1/**
2 * Deeply filters an object or array based on a predicate function.
3 * Recursively traverses nested structures and removes values that don't satisfy the predicate.
4 * Empty objects and arrays resulting from filtering are also removed.
5 *
6 * @param obj - The input object to be filtered
7 * @param fn - The predicate function to test primitive values
8 * @returns The filtered object/array, or undefined if all values are filtered out
9 */
10function deepFilter(obj: Record<string, any>, fn: Function): Record<string, any> | undefined {
11 /**
12 * Recursive helper function to perform depth-first traversal and filtering
13 *
14 * @param data - Current data being processed (can be any type)
15 * @returns Filtered data or undefined if filtered out
16 */
17 const dfs = (data: any): any => {
18 // Handle array case: recursively filter each element
19 if (Array.isArray(data)) {
20 // Map each element through dfs and filter out undefined values
21 const filteredArray = data
22 .map(dfs)
23 .filter((item: any) => item !== undefined);
24
25 // Return the filtered array if non-empty, otherwise undefined
26 return filteredArray.length > 0 ? filteredArray : undefined;
27 }
28
29 // Handle object case: recursively filter each property
30 if (typeof data === 'object' && data !== null) {
31 const filteredObject: Record<string, any> = {};
32
33 // Iterate through all properties of the object
34 for (const key in data) {
35 if (data.hasOwnProperty(key)) {
36 // Recursively filter the property value
37 const filteredValue = dfs(data[key]);
38
39 // Only include the property if its filtered value is not undefined
40 if (filteredValue !== undefined) {
41 filteredObject[key] = filteredValue;
42 }
43 }
44 }
45
46 // Return the filtered object if it has properties, otherwise undefined
47 return Object.keys(filteredObject).length > 0 ? filteredObject : undefined;
48 }
49
50 // Handle primitive values: apply the predicate function
51 return fn(data) ? data : undefined;
52 };
53
54 // Start the recursive filtering from the root object
55 return dfs(obj);
56}
57
Time and Space Complexity
Time Complexity: O(n)
where n
is the total number of nodes (including all nested values) in the input object/array structure.
The algorithm performs a depth-first traversal of the entire data structure, visiting each node exactly once. For each node:
- If it's an array, we iterate through all elements once using
map
andfilter
- If it's an object, we iterate through all key-value pairs once
- If it's a primitive value, we apply the filter function
fn
once
Since each node is processed with constant-time operations (aside from the filter function call which we assume is O(1)
), and we visit all n
nodes exactly once, the overall time complexity is O(n)
.
Space Complexity: O(d + m)
where d
is the maximum depth of the nested structure and m
is the total number of nodes that pass the filter condition.
The space complexity consists of two components:
- Recursion call stack:
O(d)
- The recursivedfs
function can go as deep as the maximum nesting level of the input structure - Output data structure:
O(m)
- We create new objects and arrays to store the filtered results, wherem
represents the number of nodes (including container objects/arrays) that remain after filtering
In the worst case where all elements pass the filter and the structure is deeply nested, this becomes O(d + n)
. In the best case where no elements pass the filter, only the call stack space O(d)
is used since the function returns undefined
without creating new data structures.
Common Pitfalls
1. Not Handling None
/null
Values Correctly
A common mistake is treating None
(Python) or null
(JavaScript) as objects, which can cause runtime errors when trying to iterate over them.
Problematic Code:
def dfs(data):
if isinstance(data, list):
# handle list
if isinstance(data, dict): # This will fail for None values!
# handle dict
return data if fn(data) else None
Why it fails: In Python, None
is not caught by isinstance(data, dict)
, but in some implementations, developers might forget that primitive checks should handle None
explicitly, especially when the filter function might receive None
as a valid input.
Solution:
def dfs(data):
# Check for None explicitly before other type checks
if data is None:
return data if fn(data) else None
if isinstance(data, list):
# handle list
elif isinstance(data, dict):
# handle dict
else:
# handle other primitives
return data if fn(data) else None
2. Modifying the Original Object
Another pitfall is accidentally modifying the original object instead of creating a new filtered copy.
Problematic Code:
def dfs(data):
if isinstance(data, dict):
for key, value in list(data.items()): # Still dangerous!
filtered_value = dfs(value)
if filtered_value is None:
del data[key] # Modifying original!
else:
data[key] = filtered_value # Modifying original!
return data if len(data) > 0 else None
Why it fails: This modifies the input object in place, which can cause unexpected side effects if the original object is used elsewhere in the code.
Solution: Always create new objects/arrays (as shown in the correct implementation):
if isinstance(data, dict):
filtered_dict = {} # Create new dict
for key, value in data.items():
filtered_value = dfs(value)
if filtered_value is not None:
filtered_dict[key] = filtered_value
return filtered_dict if len(filtered_dict) > 0 else None
3. Incorrect Handling of Falsy Values
A subtle but critical pitfall is confusing "falsy" values with values that should be filtered out.
Problematic Code:
def dfs(data):
# ... other cases ...
# Wrong: using falsy check instead of None check
filtered_value = dfs(value)
if filtered_value: # This filters out 0, "", False, [] etc.!
filtered_dict[key] = filtered_value
Why it fails: Values like 0
, ""
(empty string), False
, or even empty arrays []
are falsy in Python, but they might be valid values that passed the filter function. Using a falsy check would incorrectly remove these values.
Solution: Always explicitly check for None
:
filtered_value = dfs(value) if filtered_value is not None: # Explicit None check filtered_dict[key] = filtered_value
4. Not Preserving Special Object Types
The code might fail to handle special object types like dates, sets, or custom classes properly.
Problematic Code:
def dfs(data):
if isinstance(data, list):
# handle list
elif isinstance(data, dict):
# handle dict
else:
# Treats everything else as primitive
return data if fn(data) else None
Why it fails: Objects like datetime
, set
, or custom class instances would be passed directly to the filter function, which might not be the intended behavior.
Solution: Add explicit handling for special types or document the expected behavior:
def dfs(data):
# Handle special types that should be treated as primitives
if isinstance(data, (datetime, date, set, frozenset)):
return data if fn(data) else None
if isinstance(data, list):
# handle list
elif isinstance(data, dict):
# handle dict
elif hasattr(data, '__dict__'): # Custom objects
# Optionally handle custom objects by filtering their attributes
filtered_obj = dfs(data.__dict__)
# Additional logic to reconstruct the object
else:
return data if fn(data) else None
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
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!