2631. Group By
Problem Description
This problem asks you to extend JavaScript's built-in Array prototype with a custom groupBy
method. The method should transform any array into a grouped object based on a provided function.
The groupBy
method takes a callback function fn
as its parameter. This callback function:
- Accepts each item from the array as input
- Returns a string that serves as the grouping key
The method should return an object where:
- Each key is a unique string returned by calling
fn
on the array items - Each value is an array containing all items from the original array that produced that particular key when passed through
fn
Key requirements:
- Items in each grouped array must maintain their original order from the source array
- The order of keys in the resulting object doesn't matter
- You cannot use external libraries like lodash's
_.groupBy
For example, if you have [1, 2, 3]
and use String
as the grouping function:
fn(1)
returns"1"
, so1
goes into the group with key"1"
fn(2)
returns"2"
, so2
goes into the group with key"2"
fn(3)
returns"3"
, so3
goes into the group with key"3"
- Result:
{"1": [1], "2": [2], "3": [3]}
The solution uses the reduce
method to iterate through the array once, building up the grouped object by either creating new arrays for new keys or appending items to existing arrays for keys that have already been encountered.
Intuition
When we need to group array elements by some criteria, we're essentially building a mapping from keys to collections of values. Think about organizing items into buckets - each bucket has a label (the key), and we place items into the appropriate bucket based on what the function tells us.
The natural data structure for this is an object (or Record<string, T[]>
in TypeScript), where each property represents a bucket. As we go through the array, we need to:
- Determine which bucket each item belongs to by calling
fn(item)
- Place the item in that bucket
This is a classic accumulation pattern - we start with an empty object and gradually build it up by processing each element. JavaScript's reduce
method is perfect for this because it's designed to transform an array into a single value (in this case, an object) by iterating through elements and accumulating results.
For each item, we face two scenarios:
- The bucket (key) already exists: We simply add the item to the existing array
- The bucket doesn't exist yet: We create a new bucket with this item as its first element
The beauty of reduce
is that it handles the iteration and accumulation in one clean operation. We pass an empty object {}
as the initial accumulator, and for each item, we:
- Calculate the key using
fn(item)
- Check if
acc[key]
exists - Either push to the existing array or create a new array with the current item
- Return the updated accumulator for the next iteration
This approach ensures we only traverse the array once (O(n) time complexity) and maintain the original order of elements within each group since we process items sequentially and use push
to append them.
Solution Approach
The implementation extends the Array prototype by adding a new groupBy
method. Let's walk through the key components:
TypeScript Declaration:
declare global {
interface Array<T> {
groupBy(fn: (item: T) => string): Record<string, T[]>;
}
}
This declaration tells TypeScript that all arrays now have a groupBy
method that accepts a function returning a string and produces a Record<string, T[]>
(an object with string keys and array values).
Core Implementation using reduce
:
Array.prototype.groupBy = function (fn) {
return this.reduce((acc, item) => {
const key = fn(item);
if (acc[key]) {
acc[key].push(item);
} else {
acc[key] = [item];
}
return acc;
}, {});
};
The algorithm works as follows:
-
Initialize with an empty object: The
reduce
method starts with{}
as the initial accumulator value. -
Process each array element: For each
item
in the array:- Call
fn(item)
to get the grouping key - Store this key for use in the next steps
- Call
-
Conditional bucket management:
- If
acc[key]
exists: This means we've seen this key before, so we have an existing array. We simply push the current item to that array usingacc[key].push(item)
. - If
acc[key]
doesn't exist: This is a new key, so we create a new array containing just the current item:acc[key] = [item]
.
- If
-
Return the accumulator: After handling the current item, we return
acc
to pass it to the next iteration. -
Final result: After all items are processed,
reduce
returns the fully populated object with all groups.
Example execution trace for [1, 2, 1].groupBy(String)
:
- Start:
acc = {}
- Item 1:
key = "1"
,acc["1"]
doesn't exist →acc = {"1": [1]}
- Item 2:
key = "2"
,acc["2"]
doesn't exist →acc = {"1": [1], "2": [2]}
- Item 1:
key = "1"
,acc["1"]
exists → push to array →acc = {"1": [1, 1], "2": [2]}
- Return:
{"1": [1, 1], "2": [2]}
This pattern ensures O(n) time complexity with a single pass through the array, while maintaining the insertion order of elements within each group.
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 grouping an array of people by their age:
const people = [
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 },
{ name: "Charlie", age: 25 },
{ name: "David", age: 30 }
];
const result = people.groupBy(person => String(person.age));
Step-by-step execution:
-
Initialize: Start with empty accumulator
acc = {}
-
Process Alice (age: 25):
- Call
fn({name: "Alice", age: 25})
→ returns"25"
- Check if
acc["25"]
exists → No - Create new array:
acc["25"] = [{name: "Alice", age: 25}]
- Accumulator becomes:
{"25": [{name: "Alice", age: 25}]}
- Call
-
Process Bob (age: 30):
- Call
fn({name: "Bob", age: 30})
→ returns"30"
- Check if
acc["30"]
exists → No - Create new array:
acc["30"] = [{name: "Bob", age: 30}]
- Accumulator becomes:
{"25": [{name: "Alice", age: 25}], "30": [{name: "Bob", age: 30}]}
- Call
-
Process Charlie (age: 25):
- Call
fn({name: "Charlie", age: 25})
→ returns"25"
- Check if
acc["25"]
exists → Yes! - Push to existing array:
acc["25"].push({name: "Charlie", age: 25})
- Accumulator becomes:
{"25": [{name: "Alice", age: 25}, {name: "Charlie", age: 25}], "30": [{name: "Bob", age: 30}]}
- Call
-
Process David (age: 30):
- Call
fn({name: "David", age: 30})
→ returns"30"
- Check if
acc["30"]
exists → Yes! - Push to existing array:
acc["30"].push({name: "David", age: 30})
- Final result:
{"25": [{name: "Alice", age: 25}, {name: "Charlie", age: 25}], "30": [{name: "Bob", age: 30}, {name: "David", age: 30}]}
- Call
The key insight is that we build up our grouped object incrementally. When we encounter a new key, we create a new array bucket. When we see an existing key, we append to that bucket. The original order is preserved within each group because we process elements sequentially and use push
to maintain insertion order.
Solution Implementation
1from typing import TypeVar, Callable, Dict, List
2
3# Generic type variable for array elements
4T = TypeVar('T')
5
6def group_by(array: List[T], fn: Callable[[T], str]) -> Dict[str, List[T]]:
7 """
8 Groups array elements into a dictionary based on a key function.
9
10 This function takes an array and a function that generates keys,
11 then returns a dictionary where each key maps to a list of elements
12 that produced that key.
13
14 Args:
15 array: The list of elements to group
16 fn: Function that returns a string key for each element
17
18 Returns:
19 A dictionary where keys are strings and values are lists of elements
20
21 Examples:
22 >>> group_by([1, 2, 3], str)
23 {'1': [1], '2': [2], '3': [3]}
24
25 >>> users = [
26 ... {'name': 'Alice', 'age': 25},
27 ... {'name': 'Bob', 'age': 30},
28 ... {'name': 'Charlie', 'age': 25}
29 ... ]
30 >>> group_by(users, lambda user: str(user['age']))
31 {'25': [{'name': 'Alice', 'age': 25}, {'name': 'Charlie', 'age': 25}],
32 '30': [{'name': 'Bob', 'age': 30}]}
33 """
34 # Initialize an empty dictionary to store grouped results
35 accumulator = {}
36
37 # Iterate through each item in the array
38 for current_item in array:
39 # Get the key for the current item by calling the function
40 key = fn(current_item)
41
42 # If the key already exists in the dictionary, append the item to the existing list
43 if key in accumulator:
44 accumulator[key].append(current_item)
45 else:
46 # Otherwise, create a new list with the current item as the first element
47 accumulator[key] = [current_item]
48
49 # Return the grouped dictionary
50 return accumulator
51
52
53# Alternative implementation using defaultdict for cleaner code
54from collections import defaultdict
55
56def group_by_alternative(array: List[T], fn: Callable[[T], str]) -> Dict[str, List[T]]:
57 """
58 Alternative implementation using defaultdict for automatic list initialization.
59
60 This approach eliminates the need to check if a key exists before appending.
61
62 Args:
63 array: The list of elements to group
64 fn: Function that returns a string key for each element
65
66 Returns:
67 A dictionary where keys are strings and values are lists of elements
68 """
69 # Use defaultdict to automatically create empty lists for new keys
70 accumulator = defaultdict(list)
71
72 # Iterate through each item and group by the key
73 for current_item in array:
74 # Get the key and append the item to the corresponding list
75 key = fn(current_item)
76 accumulator[key].append(current_item)
77
78 # Convert defaultdict back to regular dict before returning
79 return dict(accumulator)
80
1import java.util.*;
2import java.util.function.Function;
3import java.util.stream.Collectors;
4
5/**
6 * Utility class that provides groupBy functionality for collections
7 * Since Java doesn't allow extending built-in classes like arrays or List,
8 * we create a utility class with static methods
9 */
10public class ArrayGroupByUtility {
11
12 /**
13 * Groups array elements into a Map based on a key function
14 *
15 * @param <T> The type of elements in the array
16 * @param array The input array to be grouped
17 * @param keyExtractor Function that returns a string key for each element
18 * @return A Map where keys are strings and values are Lists of elements
19 *
20 * @example
21 * Integer[] numbers = {1, 2, 3};
22 * Map<String, List<Integer>> result = groupBy(numbers, String::valueOf);
23 * // {"1": [1], "2": [2], "3": [3]}
24 *
25 * @example
26 * User[] users = {
27 * new User("Alice", 25),
28 * new User("Bob", 30),
29 * new User("Charlie", 25)
30 * };
31 * Map<String, List<User>> result = groupBy(users, user -> String.valueOf(user.getAge()));
32 * // {"25": [User(Alice, 25), User(Charlie, 25)], "30": [User(Bob, 30)]}
33 */
34 public static <T> Map<String, List<T>> groupBy(T[] array, Function<T, String> keyExtractor) {
35 // Initialize result map to store grouped elements
36 Map<String, List<T>> groupedMap = new HashMap<>();
37
38 // Iterate through each element in the array
39 for (T currentItem : array) {
40 // Get the key for the current item using the provided function
41 String key = keyExtractor.apply(currentItem);
42
43 // If the key already exists, add the item to the existing list
44 if (groupedMap.containsKey(key)) {
45 groupedMap.get(key).add(currentItem);
46 } else {
47 // Otherwise, create a new list with the current item
48 List<T> newList = new ArrayList<>();
49 newList.add(currentItem);
50 groupedMap.put(key, newList);
51 }
52 }
53
54 return groupedMap;
55 }
56
57 /**
58 * Alternative implementation using Java 8 Streams
59 * Groups List elements into a Map based on a key function
60 *
61 * @param <T> The type of elements in the list
62 * @param list The input list to be grouped
63 * @param keyExtractor Function that returns a string key for each element
64 * @return A Map where keys are strings and values are Lists of elements
65 */
66 public static <T> Map<String, List<T>> groupBy(List<T> list, Function<T, String> keyExtractor) {
67 // Use Java 8 Streams to group elements by the key returned from the provided function
68 return list.stream()
69 .collect(Collectors.groupingBy(
70 keyExtractor,
71 HashMap::new,
72 Collectors.toList()
73 ));
74 }
75
76 /**
77 * Overloaded method that accepts varargs for convenience
78 *
79 * @param <T> The type of elements
80 * @param keyExtractor Function that returns a string key for each element
81 * @param items Variable number of items to be grouped
82 * @return A Map where keys are strings and values are Lists of elements
83 */
84 @SafeVarargs
85 public static <T> Map<String, List<T>> groupBy(Function<T, String> keyExtractor, T... items) {
86 return groupBy(items, keyExtractor);
87 }
88}
89
1#include <vector>
2#include <unordered_map>
3#include <string>
4#include <functional>
5
6/**
7 * Template class that provides groupBy functionality for vectors
8 * Groups vector elements into a map based on a key function
9 */
10template<typename T>
11class GroupByVector {
12public:
13 /**
14 * Groups vector elements by the key returned from the provided function
15 *
16 * @param vec - The vector to group
17 * @param fn - Function that returns a string key for each element
18 * @return An unordered_map where keys are strings and values are vectors of elements
19 *
20 * @example
21 * vector<int> nums = {1, 2, 3};
22 * auto result = GroupByVector<int>::groupBy(nums, [](int x) { return to_string(x); });
23 * // {"1": [1], "2": [2], "3": [3]}
24 *
25 * @example
26 * struct User { string name; int age; };
27 * vector<User> users = {
28 * {"Alice", 25},
29 * {"Bob", 30},
30 * {"Charlie", 25}
31 * };
32 * auto result = GroupByVector<User>::groupBy(users, [](const User& u) { return to_string(u.age); });
33 * // {"25": [{"Alice", 25}, {"Charlie", 25}], "30": [{"Bob", 30}]}
34 */
35 static std::unordered_map<std::string, std::vector<T>> groupBy(
36 const std::vector<T>& vec,
37 std::function<std::string(const T&)> fn) {
38
39 // Initialize empty map to store grouped results
40 std::unordered_map<std::string, std::vector<T>> accumulator;
41
42 // Iterate through each item in the vector
43 for (const auto& currentItem : vec) {
44 // Get the key for the current item
45 std::string key = fn(currentItem);
46
47 // Check if key already exists in the map
48 auto it = accumulator.find(key);
49
50 if (it != accumulator.end()) {
51 // If the key exists, add the item to the existing vector
52 it->second.push_back(currentItem);
53 } else {
54 // Otherwise, create a new vector with the current item
55 accumulator[key] = std::vector<T>{currentItem};
56 }
57 }
58
59 return accumulator;
60 }
61
62 /**
63 * Alternative implementation using operator[] for cleaner syntax
64 * This version is more concise but may be slightly less efficient
65 * as operator[] creates an empty vector if key doesn't exist
66 */
67 static std::unordered_map<std::string, std::vector<T>> groupByAlternative(
68 const std::vector<T>& vec,
69 std::function<std::string(const T&)> fn) {
70
71 // Initialize empty map to store grouped results
72 std::unordered_map<std::string, std::vector<T>> accumulator;
73
74 // Iterate through each item in the vector
75 for (const auto& currentItem : vec) {
76 // Get the key for the current item
77 std::string key = fn(currentItem);
78
79 // Add item to the vector associated with the key
80 // operator[] creates empty vector if key doesn't exist
81 accumulator[key].push_back(currentItem);
82 }
83
84 return accumulator;
85 }
86};
87
88/**
89 * Helper function to provide a more convenient interface
90 * Allows calling groupBy without specifying the template parameter explicitly
91 */
92template<typename T>
93std::unordered_map<std::string, std::vector<T>> groupBy(
94 const std::vector<T>& vec,
95 std::function<std::string(const T&)> fn) {
96 return GroupByVector<T>::groupBy(vec, fn);
97}
98
1/**
2 * Extends the global Array interface to include the groupBy method
3 * This allows all arrays to have access to the groupBy functionality
4 */
5declare global {
6 interface Array<T> {
7 /**
8 * Groups array elements into an object based on a key function
9 * @param fn - Function that returns a string key for each element
10 * @returns An object where keys are strings and values are arrays of elements
11 */
12 groupBy(fn: (item: T) => string): Record<string, T[]>;
13 }
14}
15
16/**
17 * Implementation of the groupBy method for Array prototype
18 * Groups array elements by the key returned from the provided function
19 *
20 * @example
21 * [1, 2, 3].groupBy(String) // {"1": [1], "2": [2], "3": [3]}
22 *
23 * @example
24 * const users = [
25 * { name: 'Alice', age: 25 },
26 * { name: 'Bob', age: 30 },
27 * { name: 'Charlie', age: 25 }
28 * ];
29 * users.groupBy(user => String(user.age))
30 * // {"25": [{name: 'Alice', age: 25}, {name: 'Charlie', age: 25}], "30": [{name: 'Bob', age: 30}]}
31 */
32Array.prototype.groupBy = function<T>(this: T[], fn: (item: T) => string): Record<string, T[]> {
33 // Use reduce to build the grouped object
34 return this.reduce<Record<string, T[]>>((accumulator, currentItem) => {
35 // Get the key for the current item
36 const key: string = fn(currentItem);
37
38 // If the key already exists, add the item to the existing array
39 if (accumulator[key]) {
40 accumulator[key].push(currentItem);
41 } else {
42 // Otherwise, create a new array with the current item
43 accumulator[key] = [currentItem];
44 }
45
46 return accumulator;
47 }, {}); // Initialize with empty object
48};
49
50// Export empty object to make this a module
51export {};
52
Time and Space Complexity
Time Complexity: O(n)
The reduce
method iterates through each element in the array exactly once. For each element:
- Calling the provided function
fn(item)
takesO(1)
assuming the function itself isO(1)
- Checking if
acc[key]
exists isO(1)
for object property access - Adding an element to an array with
push()
is amortizedO(1)
- Creating a new array with a single element is
O(1)
Since we perform constant-time operations for each of the n
elements, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- The accumulator object
acc
which will store alln
elements distributed across different keys:O(n)
- Each element is stored exactly once in the resulting object structure
- The number of keys in the result object is at most
n
(when each element maps to a unique key):O(n)
in the worst case - Temporary variables like
key
useO(1)
space
The total space complexity is O(n)
where n
is the number of elements in the input array.
Common Pitfalls
1. Non-String Keys from Callback Function
The most critical pitfall is when the callback function returns non-string values. The specification explicitly states that fn
should return a string, but JavaScript's dynamic typing allows any value to be used as an object key, which gets automatically coerced to a string.
Problem Example:
[1, 2, 3].groupBy(x => x); // Returns number, not string
// This "works" but violates the contract
// Result: {"1": [1], "2": [2], "3": [3]}
[{id: 1}, {id: 2}].groupBy(item => item); // Returns object
// Objects get stringified to "[object Object]"
// Result: {"[object Object]": [{id: 1}, {id: 2}]} - All items grouped under same key!
Solution: Always ensure the callback explicitly returns a string:
// Good practices:
[1, 2, 3].groupBy(x => String(x));
[{id: 1}, {id: 2}].groupBy(item => String(item.id));
[{id: 1}, {id: 2}].groupBy(item => JSON.stringify(item));
2. Mutating the Original Array
While the current implementation doesn't mutate the original array, a common mistake when modifying prototypes is accidentally changing the original data.
Problem Example:
// Bad implementation that sorts original array
Array.prototype.groupBy = function(fn) {
this.sort(); // Mutates original array!
// ... rest of implementation
};
Solution:
Never modify this
directly. If you need a sorted version, create a copy:
Array.prototype.groupBy = function(fn) {
const sorted = [...this].sort(); // Work with a copy
// ... rest of implementation
};
3. Callback Function Throws Errors
If the callback function throws an error for certain items, the entire groupBy operation fails.
Problem Example:
const data = [1, null, 3];
data.groupBy(x => x.toString()); // Error: Cannot read property 'toString' of null
Solution: Add error handling or validate inputs:
Array.prototype.groupBy = function(fn) {
return this.reduce((acc, item) => {
try {
const key = fn(item);
if (typeof key !== 'string') {
throw new TypeError(`Callback must return a string, got ${typeof key}`);
}
acc[key] = acc[key] || [];
acc[key].push(item);
} catch (error) {
// Either skip the item or handle the error appropriately
console.warn(`Error processing item ${item}:`, error);
}
return acc;
}, {});
};
4. Name Collision with Future JavaScript Features
groupBy
is actually being considered for future JavaScript specifications. Your implementation might conflict with native implementations.
Problem Example:
// Your implementation
Array.prototype.groupBy = function(fn) { /* ... */ };
// Future native implementation might have different behavior
// This could break your code when browsers implement it
Solution: Use a namespaced method name or check for existing implementation:
if (!Array.prototype.groupBy) {
Array.prototype.groupBy = function(fn) { /* ... */ };
}
// Or use a unique name
Array.prototype.customGroupBy = function(fn) { /* ... */ };
5. Python-Specific: Unhashable Keys
In the Python implementation, there's an additional pitfall where the function might attempt to return unhashable types.
Problem Example:
# This will fail if fn returns a list or dict group_by([1, 2, 3], lambda x: [x]) # Lists can't be dictionary keys
Solution: Ensure the function returns only hashable types (strings, numbers, tuples):
def group_by(array: List[T], fn: Callable[[T], str]) -> Dict[str, List[T]]:
accumulator = {}
for current_item in array:
key = fn(current_item)
if not isinstance(key, str):
raise TypeError(f"Callback must return string, got {type(key).__name__}")
# ... rest of implementation
Depth first search is equivalent to which of the tree traversal order?
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!