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:
- Create a combined array that includes all unique
id
values from both arrays - Sort the result in ascending order by
id
- Handle unique ids: If an
id
exists in only one array, include that object as-is in the result - 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 fromarr1
)
For example:
- If
arr1 = [{id: 1, x: 1}]
andarr2 = [{id: 1, y: 2}]
, the result would be[{id: 1, x: 1, y: 2}]
- If
arr1 = [{id: 1, x: 1}]
andarr2 = [{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}]
andarr2 = [{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.
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:
- First, convert
arr1
into a dictionary structure where each object is indexed by itsid
. This creates our base collection. - Then iterate through
arr2
- for each object, check if itsid
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
- If it exists, merge the properties using
- 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 objectacc
and an array elementx
- The expression
(acc[x.id] = x, acc)
uses the comma operator to:- Assign the object
x
toacc[x.id]
(using the object'sid
as the key) - Return the accumulator for the next iteration
- Assign the object
- Use
reduce()
to iterate througharr1
and build a dictionaryd
where each object is indexed by itsid
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
inarr2
- Check if
d[x.id]
exists (meaning thisid
was already inarr1
):- If it exists: Use
Object.assign(d[x.id], x)
to merge properties. This copies all properties fromx
into the existing object atd[x.id]
, overriding any duplicate keys - If it doesn't exist: Simply add the object to the dictionary with
d[x.id] = x
- If it exists: Use
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 EvaluatorExample 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}
- Check if
-
Process
{id: 3, z: 30}
:- Check if
d[3]
exists? No, it doesn't - Add new entry:
d[3] = {id: 3, z: 30}
- Check if
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 inarr1
, performing constant time operations (object property assignment) for each element. This takesO(n)
time wheren
is the length ofarr1
.arr2.forEach(...)
: Iterates through all elements inarr2
. 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)
wherek
is the number of properties in objectx
. Since the number of properties is typically bounded and small relative to array size, we can consider thisO(1)
for practical purposes- Direct assignment
d[x.id] = x
:O(1)
- Total for this loop:
O(m)
wherem
is the length ofarr2
- Checking if
Object.values(d)
: Creates an array from object values, which takesO(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 mostn + m
entries (worst case when all IDs are unique between both arrays), requiringO(n + m)
space - The returned array from
Object.values(d)
: Contains at mostn + m
elements, requiringO(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)
Which technique can we use to find the middle of a linked list?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion 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!