Facebook Pixel

2722. Join Two Arrays by ID

Problem Description

You need to merge two arrays of objects (arr1 and arr2) based on their id field. Each object in both arrays has an id property with an integer value.

The merging rules are:

  1. Create a combined array that includes all unique id values from both arrays
  2. Sort the result in ascending order by id
  3. Handle unique ids: If an id exists in only one array, include that object as-is in the result
  4. Handle duplicate ids: When the same id appears in both arrays:
    • Merge the two objects into one
    • If a property exists in only one object, include it in the merged object
    • If a property exists in both objects, use the value from arr2 (it overrides the value from arr1)

For example:

  • If arr1 = [{id: 1, x: 1}] and arr2 = [{id: 1, y: 2}], the result would be [{id: 1, x: 1, y: 2}]
  • If arr1 = [{id: 1, x: 1}] and arr2 = [{id: 1, x: 2}], the result would be [{id: 1, x: 2}] (arr2's value overrides)
  • If arr1 = [{id: 1, x: 1}, {id: 3, z: 3}] and arr2 = [{id: 2, y: 2}], the result would be [{id: 1, x: 1}, {id: 2, y: 2}, {id: 3, z: 3}] (sorted by id)

The solution uses a hash map approach where objects from arr1 are stored by their id, then objects from arr2 either merge with existing entries or get added as new entries. Finally, the values are extracted and automatically sorted by id due to JavaScript's object key ordering for numeric keys.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to merge two arrays based on a unique identifier (id), the natural approach is to use a hash map (object) for efficient lookups and merging. Think of it like organizing items by their ID numbers - we want to quickly find if an ID already exists and either update it or add a new entry.

The key insight is that we can leverage JavaScript objects as a dictionary where the id serves as the key. This gives us O(1) access time when checking if an id already exists.

The solution follows this logical flow:

  1. First, convert arr1 into a dictionary structure where each object is indexed by its id. This creates our base collection.
  2. Then iterate through arr2 - for each object, check if its id already exists in our dictionary:
    • If it exists, merge the properties using Object.assign(), which naturally handles the override behavior we need (properties from the second object override those from the first)
    • If it doesn't exist, simply add it to the dictionary
  3. Finally, extract all values from the dictionary using Object.values()

The beauty of this approach is that JavaScript automatically maintains numeric keys in sorted order in objects, so when we extract the values, they're already sorted by id in ascending order. This eliminates the need for an explicit sorting step.

The reduce function (acc, x) => ((acc[x.id] = x), acc) is a compact way to build the initial dictionary from arr1, where the comma operator allows us to both assign the value and return the accumulator in a single expression.

Solution Approach

The implementation uses a hash map-based approach to efficiently merge the two arrays:

Step 1: Build the initial dictionary from arr1

const r = (acc: Obj, x: ArrayType): Obj => ((acc[x.id] = x), acc);
const d = arr1.reduce(r, {});
  • Define a reducer function r that takes an accumulator object acc and an array element x
  • The expression (acc[x.id] = x, acc) uses the comma operator to:
    • Assign the object x to acc[x.id] (using the object's id as the key)
    • Return the accumulator for the next iteration
  • Use reduce() to iterate through arr1 and build a dictionary d where each object is indexed by its id

Step 2: Process arr2 and merge objects

arr2.forEach(x => {
    if (d[x.id]) {
        Object.assign(d[x.id], x);
    } else {
        d[x.id] = x;
    }
});
  • Iterate through each object x in arr2
  • Check if d[x.id] exists (meaning this id was already in arr1):
    • If it exists: Use Object.assign(d[x.id], x) to merge properties. This copies all properties from x into the existing object at d[x.id], overriding any duplicate keys
    • If it doesn't exist: Simply add the object to the dictionary with d[x.id] = x

Step 3: Extract and return the result

return Object.values(d);
  • Object.values(d) extracts all the values from the dictionary into an array
  • Since JavaScript maintains numeric object keys in ascending order, the returned array is automatically sorted by id

Data Structures Used:

  • Hash Map (Object): The dictionary d provides O(1) lookup and insertion
  • Array: Input and output format for the objects

Time Complexity: O(n + m) where n is the length of arr1 and m is the length of arr2 Space Complexity: O(n + m) for storing the dictionary with all unique objects

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 small example to illustrate the solution approach:

Given:

  • arr1 = [{id: 2, x: 50}, {id: 1, x: 10}]
  • arr2 = [{id: 2, y: 20}, {id: 3, z: 30}]

Step 1: Build dictionary from arr1

Starting with an empty dictionary {}, we process each object in arr1:

  • Process {id: 2, x: 50}: Dictionary becomes {2: {id: 2, x: 50}}
  • Process {id: 1, x: 10}: Dictionary becomes {1: {id: 1, x: 10}, 2: {id: 2, x: 50}}

Our dictionary d now looks like:

{
  1: {id: 1, x: 10},
  2: {id: 2, x: 50}
}

Step 2: Process arr2 and merge

Now we iterate through arr2:

  • Process {id: 2, y: 20}:

    • Check if d[2] exists? Yes, it does!
    • Merge using Object.assign(d[2], {id: 2, y: 20})
    • The object at d[2] becomes {id: 2, x: 50, y: 20}
  • Process {id: 3, z: 30}:

    • Check if d[3] exists? No, it doesn't
    • Add new entry: d[3] = {id: 3, z: 30}

Our dictionary d now looks like:

{
  1: {id: 1, x: 10},
  2: {id: 2, x: 50, y: 20},
  3: {id: 3, z: 30}
}

Step 3: Extract values

Call Object.values(d) to extract all values from the dictionary:

[
  {id: 1, x: 10},
  {id: 2, x: 50, y: 20},
  {id: 3, z: 30}
]

The result is automatically sorted by id (1, 2, 3) because JavaScript maintains numeric keys in ascending order. Notice how:

  • ID 1 exists only in arr1, so it appears unchanged
  • ID 2 exists in both arrays, so properties are merged (x from arr1, y from arr2)
  • ID 3 exists only in arr2, so it appears unchanged

Solution Implementation

1def join(arr1, arr2):
2    """
3    Merges two arrays of objects based on their 'id' property.
4    Objects with the same id are merged, with properties from arr2 overwriting those from arr1.
5  
6    Args:
7        arr1: First array of dictionaries with 'id' property
8        arr2: Second array of dictionaries with 'id' property
9  
10    Returns:
11        Merged array of dictionaries sorted by id
12    """
13    # Convert arr1 to a dictionary with id as key for O(1) lookup
14    # Using dictionary comprehension for cleaner syntax
15    id_to_object_map = {item['id']: item for item in arr1}
16  
17    # Iterate through arr2 and merge or add objects
18    for item in arr2:
19        if item['id'] in id_to_object_map:
20            # If id exists, merge properties from arr2 item into existing object
21            # update() method merges dictionaries, with arr2 values overwriting arr1
22            id_to_object_map[item['id']].update(item)
23        else:
24            # If id doesn't exist, add new object to map
25            id_to_object_map[item['id']] = item
26  
27    # Convert dictionary back to list, sort by id, and return
28    # Sort ensures consistent ordering by id (ascending)
29    return sorted(id_to_object_map.values(), key=lambda x: x['id'])
30
1import java.util.*;
2import java.util.stream.Collectors;
3
4public class ArrayMerger {
5  
6    /**
7     * Merges two arrays of objects based on their 'id' property.
8     * Objects with the same id are merged, with properties from arr2 overwriting those from arr1.
9     * @param arr1 - First array of objects with id property
10     * @param arr2 - Second array of objects with id property
11     * @return Merged array of objects sorted by id
12     */
13    public static List<Map<String, Object>> join(List<Map<String, Object>> arr1, 
14                                                  List<Map<String, Object>> arr2) {
15        // Create a map to store objects by their id for O(1) lookup
16        // Using LinkedHashMap to maintain insertion order initially
17        Map<Integer, Map<String, Object>> idToObjectMap = new HashMap<>();
18      
19        // Add all objects from arr1 to the map
20        // Each object is keyed by its id value
21        for (Map<String, Object> item : arr1) {
22            Integer id = (Integer) item.get("id");
23            idToObjectMap.put(id, new HashMap<>(item));
24        }
25      
26        // Iterate through arr2 and merge or add objects
27        for (Map<String, Object> item : arr2) {
28            Integer id = (Integer) item.get("id");
29          
30            if (idToObjectMap.containsKey(id)) {
31                // If id exists, merge properties from arr2 item into existing object
32                // Properties from arr2 will overwrite those from arr1
33                idToObjectMap.get(id).putAll(item);
34            } else {
35                // If id doesn't exist, add new object to map
36                idToObjectMap.put(id, new HashMap<>(item));
37            }
38        }
39      
40        // Convert map back to list and sort by id
41        // Extract all values from the map and sort them by id in ascending order
42        return idToObjectMap.values().stream()
43            .sorted((a, b) -> {
44                Integer idA = (Integer) a.get("id");
45                Integer idB = (Integer) b.get("id");
46                return idA.compareTo(idB);
47            })
48            .collect(Collectors.toList());
49    }
50  
51    /**
52     * Alternative implementation using TreeMap for automatic sorting
53     */
54    public static List<Map<String, Object>> joinWithTreeMap(List<Map<String, Object>> arr1,
55                                                             List<Map<String, Object>> arr2) {
56        // Use TreeMap to automatically maintain sorted order by id
57        Map<Integer, Map<String, Object>> idToObjectMap = new TreeMap<>();
58      
59        // Process arr1: add all objects to the sorted map
60        for (Map<String, Object> item : arr1) {
61            Integer id = (Integer) item.get("id");
62            idToObjectMap.put(id, new HashMap<>(item));
63        }
64      
65        // Process arr2: merge with existing or add new objects
66        for (Map<String, Object> item : arr2) {
67            Integer id = (Integer) item.get("id");
68          
69            // Merge with existing object or create new entry
70            idToObjectMap.merge(id, new HashMap<>(item), (existingItem, newItem) -> {
71                existingItem.putAll(newItem);
72                return existingItem;
73            });
74        }
75      
76        // Return as list (already sorted due to TreeMap)
77        return new ArrayList<>(idToObjectMap.values());
78    }
79}
80
1#include <vector>
2#include <map>
3#include <algorithm>
4#include <variant>
5#include <memory>
6
7// Forward declaration for recursive type definition
8struct JSONValue;
9
10// Type definition for JSON values (primitive types, arrays, and objects)
11using JSONValueVariant = std::variant<
12    std::nullptr_t,
13    bool,
14    int,
15    double,
16    std::string,
17    std::vector<std::shared_ptr<JSONValue>>,
18    std::map<std::string, std::shared_ptr<JSONValue>>
19>;
20
21struct JSONValue {
22    JSONValueVariant value;
23};
24
25// Type definition for array elements - objects with required 'id' field and other properties
26struct ArrayType {
27    int id;
28    std::map<std::string, std::shared_ptr<JSONValue>> properties;
29  
30    // Constructor
31    ArrayType(int id) : id(id) {}
32};
33
34/**
35 * Merges two arrays of objects based on their 'id' property.
36 * Objects with the same id are merged, with properties from arr2 overwriting those from arr1.
37 * @param arr1 - First array of objects with id property
38 * @param arr2 - Second array of objects with id property
39 * @returns Merged array of objects sorted by id
40 */
41std::vector<ArrayType> join(const std::vector<ArrayType>& arr1, const std::vector<ArrayType>& arr2) {
42    // Create a map to store objects by their id for O(1) lookup
43    std::map<int, ArrayType> id_to_object_map;
44  
45    // Convert arr1 to a map object with id as key
46    for (const auto& item : arr1) {
47        id_to_object_map[item.id] = item;
48    }
49  
50    // Iterate through arr2 and merge or add objects
51    for (const auto& item : arr2) {
52        auto it = id_to_object_map.find(item.id);
53      
54        if (it != id_to_object_map.end()) {
55            // If id exists, merge properties from arr2 item into existing object
56            // Properties from arr2 overwrite those from arr1
57            for (const auto& [key, value] : item.properties) {
58                it->second.properties[key] = value;
59            }
60        } else {
61            // If id doesn't exist, add new object to map
62            id_to_object_map[item.id] = item;
63        }
64    }
65  
66    // Convert map back to vector
67    std::vector<ArrayType> result;
68    result.reserve(id_to_object_map.size());
69  
70    // std::map maintains sorted order by key, so the result will be sorted by id
71    for (const auto& [id, obj] : id_to_object_map) {
72        result.push_back(obj);
73    }
74  
75    return result;
76}
77
1/**
2 * Merges two arrays of objects based on their 'id' property.
3 * Objects with the same id are merged, with properties from arr2 overwriting those from arr1.
4 * @param arr1 - First array of objects with id property
5 * @param arr2 - Second array of objects with id property
6 * @returns Merged array of objects sorted by id
7 */
8function join(arr1: ArrayType[], arr2: ArrayType[]): ArrayType[] {
9    // Reducer function to convert array to object map with id as key
10    const arrayToMapReducer = (accumulator: ObjectMap, currentItem: ArrayType): ObjectMap => {
11        accumulator[currentItem.id] = currentItem;
12        return accumulator;
13    };
14  
15    // Convert arr1 to a map object for O(1) lookup by id
16    const idToObjectMap: ObjectMap = arr1.reduce(arrayToMapReducer, {});
17
18    // Iterate through arr2 and merge or add objects
19    arr2.forEach((item: ArrayType) => {
20        if (idToObjectMap[item.id]) {
21            // If id exists, merge properties from arr2 item into existing object
22            Object.assign(idToObjectMap[item.id], item);
23        } else {
24            // If id doesn't exist, add new object to map
25            idToObjectMap[item.id] = item;
26        }
27    });
28  
29    // Convert map back to array and return
30    return Object.values(idToObjectMap);
31}
32
33// Type definition for JSON values (primitive types, arrays, and objects)
34type JSONValue = null | boolean | number | string | JSONValue[] | { [key: string]: JSONValue };
35
36// Type definition for array elements - objects with required 'id' field and other JSON properties
37type ArrayType = { id: number } & Record<string, JSONValue>;
38
39// Type definition for the intermediate map object used for merging
40type ObjectMap = Record<number, ArrayType>;
41

Time and Space Complexity

Time Complexity: O(n + m)

The time complexity breaks down as follows:

  • arr1.reduce(r, {}): Iterates through all elements in arr1, performing constant time operations (object property assignment) for each element. This takes O(n) time where n is the length of arr1.
  • arr2.forEach(...): Iterates through all elements in arr2. For each element:
    • Checking if d[x.id] exists: O(1) average case for object property lookup
    • Object.assign(d[x.id], x): O(k) where k is the number of properties in object x. Since the number of properties is typically bounded and small relative to array size, we can consider this O(1) for practical purposes
    • Direct assignment d[x.id] = x: O(1)
    • Total for this loop: O(m) where m is the length of arr2
  • Object.values(d): Creates an array from object values, which takes O(n + m) in the worst case when all IDs are unique

Overall time complexity: O(n) + O(m) + O(n + m) = O(n + m)

Space Complexity: O(n + m)

The space complexity analysis:

  • Object d: Stores at most n + m entries (worst case when all IDs are unique between both arrays), requiring O(n + m) space
  • The returned array from Object.values(d): Contains at most n + m elements, requiring O(n + m) space
  • The reducer function and other variables use constant space: O(1)

Overall space complexity: O(n + m)

Common Pitfalls

1. Mutating Original Objects

The most significant pitfall in this solution is that it modifies the original objects from arr1 when merging. Both the TypeScript (Object.assign(d[x.id], x)) and Python (id_to_object_map[item['id']].update(item)) implementations directly mutate the objects that were originally in arr1.

Why this is problematic:

  • If the original arrays are used elsewhere in the code, unexpected side effects occur
  • The caller may not expect their input data to be modified
  • Makes debugging harder when objects change unexpectedly

Example of the issue:

arr1 = [{'id': 1, 'x': 1}]
arr2 = [{'id': 1, 'y': 2}]
result = join(arr1, arr2)
# arr1 is now [{'id': 1, 'x': 1, 'y': 2}] - MODIFIED!

Solution - Create deep copies:

def join(arr1, arr2):
    # Create deep copies to avoid mutating original objects
    id_to_object_map = {item['id']: dict(item) for item in arr1}
  
    for item in arr2:
        if item['id'] in id_to_object_map:
            id_to_object_map[item['id']].update(item)
        else:
            # Also copy arr2 items to be consistent
            id_to_object_map[item['id']] = dict(item)
  
    return sorted(id_to_object_map.values(), key=lambda x: x['id'])

2. Shallow Copy Issues with Nested Objects

Even with the fix above using dict(item), if the objects contain nested structures (lists, dictionaries), only a shallow copy is created. Modifying nested properties will still affect the original.

Example:

arr1 = [{'id': 1, 'data': {'value': 10}}]
arr2 = [{'id': 1, 'data': {'value': 20}}]
# Even with dict() copy, nested 'data' objects may share references

Solution - Use deep copy for nested structures:

import copy

def join(arr1, arr2):
    # Use deepcopy for complete isolation from original data
    id_to_object_map = {item['id']: copy.deepcopy(item) for item in arr1}
  
    for item in arr2:
        if item['id'] in id_to_object_map:
            # Deep merge for nested structures
            id_to_object_map[item['id']].update(copy.deepcopy(item))
        else:
            id_to_object_map[item['id']] = copy.deepcopy(item)
  
    return sorted(id_to_object_map.values(), key=lambda x: x['id'])

3. Assuming Numeric IDs for Sorting

The code assumes IDs are always integers and uses numeric sorting. If IDs could be strings or mixed types, the sorting behavior becomes unpredictable.

Solution - Handle different ID types:

def join(arr1, arr2):
    id_to_object_map = {item['id']: dict(item) for item in arr1}
  
    for item in arr2:
        if item['id'] in id_to_object_map:
            id_to_object_map[item['id']].update(item)
        else:
            id_to_object_map[item['id']] = dict(item)
  
    # Handle both numeric and string IDs properly
    def sort_key(x):
        id_val = x['id']
        # Try to convert to number for consistent sorting
        try:
            return (0, int(id_val))  # Numeric IDs come first
        except (ValueError, TypeError):
            return (1, str(id_val))  # String IDs come after
  
    return sorted(id_to_object_map.values(), key=sort_key)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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


Recommended Readings

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

Load More