Problem overview
The method merge receives two arrays of numbers, first and second, each already sorted in ascending order. It returns one new sorted array that contains every number from both. Duplicates are kept: no value is dropped or merged.
In the example, first = [1, 4, 8] and second = [1, 3, 4, 17]. The result is [1, 1, 3, 4, 4, 8, 17]: both 1s and both 4s appear.
The statement asks for O(n + m) time and O(n + m) extra space, where n and m are the lengths of the two arrays. The extra space is the result array itself.
Approach: always take the smaller front element
The simplest idea is to concatenate the arrays and sort the result. That costs O((n + m) log(n + m)) and ignores the fact that the inputs are already sorted. Because they are, the smallest number not yet placed must be at the front of one of the two arrays. So there are only two candidates to compare at any moment.
Keep one index in each array. Compare the two current elements, copy the smaller one to the result, and move that array's index forward. Each comparison places exactly one number, so the whole merge takes n + m steps.
At some point one array runs out. The other one may still have numbers left, and they are all at least as large as everything already placed, so they can be copied over as they are.
On first = [1, 4, 8] and second = [1, 3, 4, 17]:
- Compare 1 and 1: not smaller, so the 1 from
secondgoes in. Result:[1]. - Compare 1 and 3: the 1 from
firstgoes in. Result:[1, 1]. - Compare 4 and 3: 3 goes in. Then 4 against 4: the 4 from
secondgoes in. Then 4 against 17: the 4 fromfirstgoes in. Result:[1, 1, 3, 4, 4]. - Compare 8 and 17: 8 goes in, and
firstis used up. Result:[1, 1, 3, 4, 4, 8]. - Copy the rest of
second: 17.
Step by step
- Create
resultas an emptynumber[]. The annotation matters: with no elements to look at, TypeScript can't work out the element type of an empty array literal by itself. - Declare
iandj, both 0. They point at the next unused element offirstandsecond. - Loop while both indices are in range. The loop stops as soon as one array is used up.
- If
first[i] < second[j], pushfirst[i]and advancei. - Otherwise, push
second[j]and advancej. This branch also handles equal values. - Drain
first. A second loop, whilei < first.length, copies what is left of it. - Drain
second. A third loop does the same forj. At most one of these two loops runs, because the main loop ended when one array was exhausted. - Return
result.
The solution in TypeScript
class Solution {
merge(first: number[], second: number[]): number[] {
let result: number[] = [];
let i = 0, j = 0;
while (i < first.length && j < second.length) {
if (first[i] < second[j]) {
result.push(first[i]);
i++;
} else {
result.push(second[j]);
j++;
}
}
while (i < first.length) {
result.push(first[i]);
i++;
}
while (j < second.length) {
result.push(second[j]);
j++;
}
return result;
}
}
The TypeScript details worth a closer look:
number[]is the type of an array of numbers. Withlet result: number[] = [], the compiler knowspushaccepts numbers and that the return type matches.- Indices are plain numbers.
let i = 0, j = 0;declares two variables in one statement, and TypeScript infersnumberfor both. pushmodifies the array in place and appends at the end, which is a constant-time operation on average.- The inputs are never changed. Only reads and a new
resultarray are involved, so the caller's arrays are exactly as they were. - Reading past the end gives
undefined, not an error. That is why the bounds checks oniandjcome first. A comparison withundefinedis always false, so a missing check would quietly corrupt the result instead of failing.
Complexity
Time: O(n + m). Every loop iteration pushes one number and advances one index. Each index only moves forward, so the three loops together run at most n + m times.
Space: O(n + m). The result array holds all n + m numbers. The two indices use constant space.
Edge cases and common mistakes
- An empty array. If
firstis empty, the main loop never runs, and the second drain loop copies all ofsecond. Merging two empty arrays returns[]. - One array entirely smaller.
[1, 2, 3]and[7, 8]produce[1, 2, 3, 7, 8]: the main loop emptiesfirstand a drain loop copies the rest ofsecond. - Forgetting the drain loops. Without them, the numbers left over in the longer array are lost.
- Concatenating and calling
sort()with no comparator. The default sort compares elements as strings, so[5, 10, 1, 2]becomes[1, 10, 2, 5]. A comparator such as(a, b) => a - bfixes the order, but the result is still slower than the merge. - Dropping duplicates. Equal values from the two arrays must both appear. The
elsebranch takes one of them now and the other on a later iteration. - Forgetting to advance an index. If
i++orj++is missing, the loop pushes the same number forever.
Testing
first = [1, 4, 8],second = [1, 3, 4, 17]gives[1, 1, 3, 4, 4, 8, 17].first = [],second = [2, 5]gives[2, 5].first = [1, 2],second = []gives[1, 2].first = [5, 6],second = [1, 2]gives[1, 2, 5, 6].first = [],second = []gives[].