Facebook Pixel

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 filtered
  • fn: A filter function that returns true or false for primitive values

The filtering process works as follows:

  1. For primitive values (numbers, strings, booleans, etc.): Apply the filter function fn. If fn returns false, the value should be removed.

  2. For arrays: Recursively filter each element. After filtering, if the array becomes empty, it should be removed entirely (return undefined).

  3. 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 (return undefined).

  4. 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 filter
  • 2 fails the filter, making {c: 2} become an empty object {}
  • The empty object {} is removed
  • The top-level object becomes empty and returns undefined
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. First check if 1 passes the filter
  2. If it doesn't, {c: 1} becomes empty
  3. An empty {} means {b: {}} also becomes empty
  4. 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:

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

  2. Arrays: Map each element through recursive dfs calls, then filter out any undefined results. If the resulting array is empty, we return undefined to signal that this entire array should be removed from its parent.

  3. 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, return undefined.

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:

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

Key Design Decisions:

  • Using undefined as a removal signal: When any level returns undefined, 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 Evaluator

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

  1. Start with root object - Call dfs(obj)

    • Since it's an object, iterate through each property
  2. Process a: 1

    • Call dfs(1) → primitive value
    • Apply filter: fn(1) returns true (1 > 0)
    • Return 1
  3. 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) returns true → keep 2

    3.2. Process d: -1

    • dfs(-1)fn(-1) returns false → return undefined

    3.3. Process e: {f: 3}

    • dfs({f: 3}) → object, recurse

    • Process f: 3dfs(3)fn(3) returns true → keep 3

    • Object {f: 3} has properties, so return {f: 3}

    • After processing all of b's properties: {c: 2, e: {f: 3}} (d is removed)

  4. 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) returns true1

    4.2. dfs(2)fn(2) returns true2

    4.3. dfs(-3)fn(-3) returns falseundefined

    4.4. dfs({h: -4}) → object

    • Process h: -4dfs(-4)fn(-4) returns falseundefined

    • Object becomes empty {} → return undefined

    • After mapping: [1, 2, undefined, undefined]

    • After filtering out undefined: [1, 2]

  5. 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 object b
  • 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 and filter
  • 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:

  1. Recursion call stack: O(d) - The recursive dfs function can go as deep as the maximum nesting level of the input structure
  2. Output data structure: O(m) - We create new objects and arrays to store the filtered results, where m 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More