Problem overview
The method kSmallest receives an array of positive integers, nums, and a number k. It returns the k smallest values, in any order. The statement asks for O(n) time and O(log n) extra space.
For nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5] and k = 4, the sorted array starts 1, 2, 2, 3, so those four values are the answer. Because any order is accepted, [2, 1, 3, 2] is correct as well. Duplicates count separately: the 2 appears twice.
Approach: partition only the side that matters
Sorting the whole array and taking the first k values costs O(n log n). This lesson doesn't need a full sort, because the answer is a set, not an ordered list.
The tool is the same partition step Quick Sort uses. Pick a pivot, and rearrange the range so that values not larger than the pivot come first, then the pivot, then the larger ones. If the pivot lands at index pivot in the range starting at left, then exactly pivot - left + 1 values (the pivot and everything before it) are the smallest of the range. Compare that count with k:
- If the count is at least
k, theksmallest values are all inside[left, pivot]. Continue on[left, pivot - 1]with the samek. The pivot is the largest value in its group, so if the count equalsk, there is nothing more to do. - Otherwise, the whole of
[left, pivot]is part of the answer, and the search continues on[pivot + 1, right]for the remainingk - (pivot - left + 1)values.
Quick Sort recurses into both sides. This method recurses into one, and with a random pivot each step shrinks the range geometrically on average. The work is about n + n/2 + n/4 + ..., which is O(n) expected. This technique is called quickselect.
An example: with [4, 1, 3] and k = 2, suppose the pivot value is 3. The values 1 and 3 are not larger than it, so the pivot lands at index 1 and the count is 2, which is at least k. The method recurses into [0, 0], which holds a single value, and stops. The first two positions hold 1 and 3.
Step by step
- Declare
smallestKNumswith aninout [Int], the boundsleftandright, andk. It rearranges the array so theksmallest values of the range come first, and returns nothing. - Stop on tiny ranges: when
left >= right, there is at most one value, so return. - Declare
partitionwith the array and bounds. It returns the pivot's final index. - Pick a random pivot:
left + Int.random(in: 0...(right - left)). A random pivot keeps the expected time linear even on sorted input. - Write
swapVals, which swaps two elements through a temporary constant. Its name avoids confusion with Swift's globalswap. - Move the pivot to
rightwithswapVals. - Set up the scan:
valueis the pivot value (alet) andcorrectIndexstarts atleft(avar). - Loop with
var ifromleftthroughrightin awhileloop, adding 1 toiat the end of each pass. - Move small values left: if
nums[i] <= value, swap it withnums[correctIndex]and add 1 tocorrectIndex. - Return
correctIndex - 1. The pivot was the last value moved, so it sits just beforecorrectIndex. - Partition the range in
smallestKNumsand save the result aspivot. - Branch on the count. If
pivot - left + 1 >= k, recurse on[left, pivot - 1]with the samek. Otherwise recurse on[pivot + 1, right]withk - (pivot - left + 1). - In
kSmallest, copy the parameter intovar arr, runsmallestKNumson the copy, and return the firstkelements as an array.
The solution in Swift
class Solution {
func swapVals(_ nums: inout [Int], _ i: Int, _ j: Int) {
let temp = nums[i]
nums[i] = nums[j]
nums[j] = temp
}
func partition(_ nums: inout [Int], _ left: Int, _ right: Int) -> Int {
let randomIndex = left + Int.random(in: 0...(right - left))
swapVals(&nums, randomIndex, right)
let value = nums[right]
var correctIndex = left
var i = left
while i <= right {
if nums[i] <= value {
swapVals(&nums, i, correctIndex)
correctIndex += 1
}
i += 1
}
return correctIndex - 1
}
func smallestKNums(_ nums: inout [Int], _ left: Int, _ right: Int, _ k: Int) {
if left >= right {
return
}
let pivot = partition(&nums, left, right)
if pivot - left + 1 >= k {
smallestKNums(&nums, left, pivot - 1, k)
} else {
smallestKNums(&nums, pivot + 1, right, k - (pivot - left + 1))
}
}
func kSmallest(_ nums: [Int], _ k: Int) -> [Int] {
var arr = nums
smallestKNums(&arr, 0, arr.count - 1, k)
return Array(arr[0..<k])
}
}
The Swift details worth a closer look:
- Parameters are constants.
kSmallestcan't sort its ownnumsparameter, and trying to write into it givescannot assign through subscript: 'nums' is a 'let' constant. Copying intovar arrsolves it. Arrays are value types, so the copy is independent: the caller's array is not changed. Swift copies the storage lazily, whenarris first modified. inoutand&. The helper methods take the array asinoutso they can rearrange it, and each call passes it as&arror&nums.- Slices versus arrays.
arr[0..<k]is anArraySlice<Int>, a view intoarr. The function must return[Int], soArray(...)converts the slice into a real array. - Half-open ranges.
0..<kexcludesk, which gives exactlykelements, while0...(right - left)includes its upper end, so the random offset can reachright - left. - Why a
whileloop. The loop could be written asfor i in left...right. The lesson useswhilewith a manuali += 1, which does the same thing here.
Complexity
Time: O(n) expected. The first partition costs n. After it, the range holds fewer values, on average about half, and each later partition works on a range that is still smaller. The costs add up to a constant multiple of n. A very unlucky run of pivots could take O(n²), but random pivots make that rare.
Space: O(log n) expected. The array is rearranged in place, apart from the single copy that kSmallest makes of its input. The recursion depth follows the number of partitions, about log n on average.
Edge cases and common mistakes
kequal to the length. Every value is in the answer.[4, 1, 3]withk = 3returns all three, in some order.k = 0. The branchpivot - left + 1 >= kis always true, so the search keeps shrinking to the left and ends.Array(arr[0..<0])is an empty array.klarger than the length. The slicearr[0..<k]goes past the end, and Swift stops the program with an index-out-of-range error. The statement implieskis within the array.- Duplicates.
[4, 4, 4]withk = 2returns[4, 4]. Equal values go to the left side of the pivot because of<=, which keeps the counts exact. - Single element.
[1]withk = 1returns at once becauseleft >= right, and the result is[1]. - Expecting sorted output. Only the first
kpositions are guaranteed to hold the right values. Their order is not defined, so compare as sets, or sort the result first. - Recursing on both sides. That would turn the method into Quick Sort and cost
O(n log n).
Testing
[5, 9, 3, 6, 2, 1, 3, 2, 7, 5]withk = 4gives[1, 2, 2, 3]after sorting the result.[4, 4, 4]withk = 2gives[4, 4].[4, 1, 3]withk = 0gives[].[4, 1, 3]withk = 3gives[1, 3, 4]after sorting.[1]withk = 1gives[1].
