Problem overview
The function k_smallest receives a vector of positive integers, nums, and a number k. It returns a vector with the k smallest values of nums. Duplicates count separately, and the result may be in any order.
For nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5] and k = 4, the values in increasing order are 1, 2, 2, 3, 3, 5, 5, 6, 7, 9. The four smallest are 1, 2, 2 and 3, so the answer is [1, 2, 2, 3]. The second 2 is a separate element, and it counts as the third smallest.
The lesson asks for O(n log n) time. Faster solutions exist and come later in the course, but sorting is the natural first idea.
Approach: sort, then take a prefix
After sorting in ascending order, the smallest value is first, the second smallest is next, and so on. The k smallest values are exactly the first k elements. No searching or counting is needed, and ties between equal values don't matter, because equal values are interchangeable.
Sorting dominates the cost. Taking the first k elements is a copy of at most n values, which is cheaper than the sort. The total is therefore O(n log n).
Step by step
- Sort
numsin place withsort(). The parameter is declaredmut nums, so the function owns the vector and may reorder it. The caller's vector was moved into the function, so there is no other copy to disturb. - Return the first
kelements as a new vector. Slice the sorted vector up tokand callto_vec(), which copies the slice into an ownedVec<i32>. Because the line has no semicolon, it is the function's return value.
The solution in Rust
impl Solution {
fn k_smallest(mut nums: Vec<i32>, k: i32) -> Vec<i32> {
nums.sort();
nums[..k as usize].to_vec()
}
}
The Rust details worth a closer look:
- Slices and ranges.
nums[..k as usize]is the range from the start up to, but not including, indexk, so it haskelements. It is a borrowed view of the sorted vector, not a new vector, which is whyto_vec()is needed to produce something the function can return. - The cast. Indices and slice bounds must be
usize, whilekis ani32. The castk as usizebinds tighter than the range, so it convertskalone. - Casting is silent. For a negative
k,k as usizedoesn't fail:-1i32 as usizeis18446744073709551615on a 64-bit target, and the slice then panics for being out of range. A validkis never negative, so that case does not arise here. - Out-of-range slices panic. If
kwere larger than the length ofnums, the slice would stop the program with an error such asrange end index 3 out of range for slice of length 2. The lesson's inputs keepkwithin the length. sort()returns nothing. It reorders the vector in place and gives back(), so the next line usesnumsitself, not a returned value.sort_unstable()would work as well. For integers, equal values can't be told apart, so not keeping their original order costs nothing, and it is often a little faster.
Complexity
Time: O(n log n). Sorting n integers takes O(n log n), and copying k of them takes O(k) with k <= n.
Space. The vector is sorted in place, and the returned vector holds k elements. Rust's stable sort() may allocate a temporary buffer, so strictly its extra space can be O(n), while sort_unstable() sorts without one.
Edge cases and common mistakes
kequal to 0. The slicenums[..0]is empty, so the function returns[].kequal to the length. The slice covers the whole vector, so the answer is the entire sorted vector. For[5, 9]andk = 2the result is[5, 9].- Duplicates. With
[2, 2, 2, 1]andk = 3the answer is[1, 2, 2]. Each copy is its own element, so values are not removed when they repeat. - Slicing before sorting. Taking the first
kelements of the unsorted vector gives the firstkvalues in the input, not the smallest ones. - Sorting in the wrong direction. Sorting in descending order and then taking a prefix would give the largest
kvalues. - Using
kas an index. The slicenums[..k]without the cast doesn't compile, becausekis ani32and a slice bound must beusize.
Testing
nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5],k = 4gives[1, 2, 2, 3].nums = [5, 9],k = 0gives[].nums = [5, 9],k = 2gives[5, 9].nums = [2, 2, 2, 1],k = 3gives[1, 2, 2].
