Problem overview
The function merge receives two vectors of integers, first and second, each already sorted in ascending order. It must return one sorted vector with all the numbers from both, duplicates included. For first = [1, 4, 8] and second = [1, 3, 4, 17] the answer 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 lengths. The extra space is the output vector.
Approach: always take the smaller front element
One simple way is to append both vectors and sort the result. That costs O((n + m) log(n + m)) and ignores the fact that the inputs are sorted. The linear approach uses that fact.
In a sorted vector, the smallest element not yet used is always the first unused one. So the smallest element not yet merged is one of two candidates: the first unused element of first, or the first unused element of second. Compare them, move the smaller into the output, and advance the index of the vector it came from. Repeat until one vector is used up. The other may still have elements, and since it is sorted and everything left is at least as large as what was merged, those can be copied over as they are.
On the example, with i pointing into first and j into second:
- 1 against 1: not less, so take the one from
second. Output[1]. - 1 against 3: take 1 from
first. Output[1, 1]. - 4 against 3: take 3. Output
[1, 1, 3]. - 4 against 4: take the one from
second. Output[1, 1, 3, 4]. - 4 against 17: take 4. Then 8 against 17: take 8. Now
firstis used up. - Copy the rest of
second: 17. The output is[1, 1, 3, 4, 4, 8, 17].
Step by step
- Create the output.
resultis an emptyVec<i32>made withVec::new()and declaredlet mut, because values will be pushed into it. - Declare the indices.
iandjare mutable and start at0usize. A vector is indexed withusize, and the suffix on the literal fixes the type. - Main loop. Run
while i < first.len() && j < second.len(), so it stops as soon as either vector has no unused elements. - Take from
first. Whenfirst[i] < second[j], pushfirst[i]and incrementi. - Otherwise take from
second. Pushsecond[j]and incrementj. This branch also covers equal values. - Drain
first. A loopwhile i < first.len()pushes what is left offirst. - Push and advance inside it:
result.push(first[i])andi += 1. - Drain
second. A loopwhile j < second.len()does the same forsecond. At most one of the two drain loops runs, because the main loop ended when one vector ran out. - Push and advance inside it as well.
- Return. End with
result, without a semicolon and withoutreturn.
The solution in Rust
impl Solution {
fn merge(first: Vec<i32>, second: Vec<i32>) -> Vec<i32> {
let mut result: Vec<i32> = Vec::new();
let mut i = 0usize;
let mut j = 0usize;
while i < first.len() && j < second.len() {
if first[i] < second[j] {
result.push(first[i]);
i += 1;
} else {
result.push(second[j]);
j += 1;
}
}
while i < first.len() {
result.push(first[i]);
i += 1;
}
while j < second.len() {
result.push(second[j]);
j += 1;
}
result
}
}
Rust details worth knowing:
- Ownership. The parameters are owned
Vec<i32>values, so the function owns both inputs. Reading an element such asfirst[i]copies ani32, which is why noclone()is needed. &&short-circuits. Both bounds are checked before anything is indexed, sofirst[i]andsecond[j]are always in range inside the loop.usizeeverywhere.len()returns ausize, and so must the indices. Mixing in ani32index doesn't compile without a cast.- Growth of the vector.
pushreallocates now and then as the vector fills, but the cost averages out to constant time per push. CallingVec::with_capacity(first.len() + second.len())would reserve the space up front. - Slices for the tail. A drain loop can be replaced by
result.extend_from_slice(&first[i..]), which copies the rest of the vector in one call. The loops in the lesson show the same idea explicitly.
Complexity
Time: O(n + m). Every iteration of every loop pushes one element and moves one index forward, and each index only moves forward. The total number of iterations is exactly n + m.
Space: O(n + m). The output holds all n + m numbers. Apart from it, the function uses two indices.
Edge cases and common mistakes
- One input is empty. With
first = []the main loop doesn't start, the first drain loop is skipped, and the second one copies all ofsecond.[]and[2, 5]give[2, 5]. - Both inputs empty. The result is an empty vector.
- One input entirely smaller.
[5, 6]and[1, 2]give[1, 2, 5, 6]: the main loop emptiessecond, then the first drain loop copiesfirst. - Forgetting the drain loops. Without them, the leftover elements are lost. The example would end at
[1, 1, 3, 4, 4, 8]and drop the 17. - Duplicates. Equal values are all kept. The
<test sends ties to theelsebranch, which takes the element fromsecondfirst. For plain integers it makes no difference which equal copy goes first. - Unsorted input. The merge relies on both inputs being sorted. With unsorted input the result can come out unsorted.
Testing
[1, 4, 8]and[1, 3, 4, 17]give[1, 1, 3, 4, 4, 8, 17].[]and[2, 5]give[2, 5].[1, 2]and[]give[1, 2].[1, 2, 3]and[7, 8]give[1, 2, 3, 7, 8].[5, 6]and[1, 2]give[1, 2, 5, 6].
