Problem overview
The function merge receives two IntArrays, first and second, each already sorted in ascending order. It returns one new IntArray with all the numbers from both, also sorted. Duplicates stay: every element of both inputs appears in the result.
For first = [1, 4, 8] and second = [1, 3, 4, 17] the result is [1, 1, 3, 4, 4, 8, 17]. The statement asks for O(n + m) time and O(n + m) extra space, where n and m are the two sizes.
Approach: always take the smaller front element
The simplest idea is to put everything into one array and sort it. That costs O((n + m) log(n + m)) and ignores the fact that the inputs are already ordered. The faster idea uses that order.
The smallest number not yet merged is always at the front of one of the two arrays, because each array is sorted. So keep one index per array. Compare the two front elements, move the smaller one to the result, and advance that array's index. Each comparison places exactly one element, so the whole merge takes n + m steps.
At some point one array runs out. The other one may still have elements, and they are all at least as large as everything placed so far and already in order, so they can be copied over without any more comparing.
Walking through the example, the fronts are 1 and 1. They are equal, so the one from second goes first (the comparison is a strict <), then 1 from first, then 3, then the two 4s, and 8. By then first is used up, and the remaining 17 from second is copied at the end.
Step by step
- Make the result list. An
ArrayList<Int>grows as elements are added, which suits a result whose size is the sum of two inputs. It's avalbecause only its contents change. - Create two indices.
ipoints intofirstandjintosecond. They arevars starting at 0. - Loop while both arrays have elements. The condition is
i < first.size && j < second.size, so neither array is read out of bounds. - If
first[i]is smaller, add it to the result and incrementi. - Otherwise add
second[j]and incrementj. This branch also handles equal values. - Drain
first. A loop oni < first.sizecopies whatever is left of it. - Add the rest of
firstone element at a time, movingiforward. - Drain
second. A loop onj < second.sizedoes the same for the other array. Only one of the two drain loops has anything to do, since the main loop stopped when one array was used up. - Add the rest of
secondand movejforward. - Convert and return. The function returns an
IntArray, so calltoIntArray()on the list.
The solution in Kotlin
class Solution {
fun merge(first: IntArray, second: IntArray): IntArray {
val result = ArrayList<Int>()
var i = 0
var j = 0
while (i < first.size && j < second.size) {
if (first[i] < second[j]) {
result.add(first[i])
i++
} else {
result.add(second[j])
j++
}
}
while (i < first.size) {
result.add(first[i])
i++
}
while (j < second.size) {
result.add(second[j])
j++
}
return result.toIntArray()
}
}
Kotlin details worth knowing:
IntArrayversusArrayList<Int>. AnIntArrayhas a fixed size and stores plain ints. AnArrayList<Int>can grow, but it stores boxedIntegerobjects. The solution builds in the list because it adds elements one by one, then converts once at the end.toIntArray()copies the list into a newIntArray.valon a list.val resultmeans the name can't be pointed at another list.result.add(...)still works, because it changes the list, not the variable.sizeis a property. Writefirst.sizewithout parentheses, unlike a method call.- Allocating the result directly. Since the final size is known to be
first.size + second.size, anIntArray(first.size + second.size)with a write index would avoid the list and the conversion. The lesson's version is simpler to read.
Complexity
Time: O(n + m). Each loop iteration adds one element to the result and advances an index, and no index ever moves backward. In total the three loops run n + m iterations. The final toIntArray() is another O(n + m) pass.
Space: O(n + m). The result list holds every element, and the converted array holds them again. Both are proportional to n + m.
Edge cases and common mistakes
- One or both arrays empty. With
first = []andsecond = [2, 5]the main loop never runs and the second drain loop copies everything, giving[2, 5]. Two empty arrays give an empty array. - All of one array before the other.
[1, 2]and[3, 4]finish the main loop after consumingfirst; the drain loop appends[3, 4]. The result is[1, 2, 3, 4]. - Forgetting a drain loop. Without it, the leftover elements, like the 17 in the example, are silently dropped.
- Forgetting to advance an index. The loop would then add the same element forever.
- Duplicates. Equal values from both arrays all appear in the result, so this isn't a set union.
- Returning the list. The return type is
IntArray, so aList<Int>won't compile there.
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 = [3, 4]gives[1, 2, 3, 4].first = [],second = []gives[].