Problem overview
The method kSmallest receives an Array[Int] called nums and an integer k. It returns the k smallest values, in any order. For nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5] and k = 4 the answer holds 1, 2, 2 and 3; [2, 1, 3, 2] is just as correct as [1, 2, 2, 3].
The target is O(n) time and O(log n) extra space.
Approach: partition, then follow only the side that matters
Sorting the whole array and taking the first k values costs O(n log n), and keeping a heap of size k costs O(n log k). This lesson goes further, using the idea behind Quick Sort. It is known as quickselect.
A partition picks a pivot and rearranges a range so that the values less than or equal to the pivot come first, then the pivot, then the larger values. The pivot lands at its final sorted index pivot. That means the range [left, pivot] holds the pivot - left + 1 smallest values of the range. Compare that count with k:
- If the count is at least
k, allkwanted values are inside[left, pivot]. The pivot is the largest value there, so keep searching in[left, pivot - 1]with the samek. - If the count is smaller, every value in
[left, pivot]belongs to the answer, and onlyk - countmore are needed from[pivot + 1, right].
Unlike Quick Sort, it recurses into one side only. With random pivots the range shrinks by a constant factor on average, so the total work is about n + n/2 + n/4 + ..., which is O(n).
Step by step
- Declare
smallestKNums(nums, left, right, k)returningUnit. It rearrangesnumsin place so that theksmallest values of the range come first. - Base case: if
left >= right, there is at most one value and nothing to rearrange, so return. - Declare
partition(nums, left, right)returning the pivot's final index. - Pick a random pivot index:
leftplus(scala.util.Random.nextDouble() * (right - left + 1)).toInt.nextDouble()returns a number from 0 up to but not including 1, so the product, truncated, is a whole number from 0 toright - left. - Write
swap(abovepartition) to exchange two array elements through a temporaryval. - Move the pivot to the end by swapping
randomIndexwithright. - Set up the scan:
val value = nums(right)is the pivot, andvar correctIndex = leftis where the next value that is at mostvaluegoes. - Scan with a
whileloop overifromlefttorightinclusive. The pivot, atright, is visited last. - Swap qualifying values forward. If
nums(i) <= value, swapnums(i)withnums(correctIndex)and add one tocorrectIndex. - Return
correctIndex - 1. The pivot was the last value moved, so it sits just beforecorrectIndex. - Back in
smallestKNums, callpartitionand store the result aspivot. - Branch on the count
pivot - left + 1 >= k: recurse on[left, pivot - 1]with the samek. - Otherwise recurse on
[pivot + 1, right]withk - (pivot - left + 1). - In
kSmallest, callsmallestKNums(nums, 0, nums.length - 1, k). - Return
nums.slice(0, k), the firstkpositions, as a new array.
The solution in Scala
class Solution {
def swap(nums: Array[Int], i: Int, j: Int): Unit = {
val temp = nums(i)
nums(i) = nums(j)
nums(j) = temp
}
def partition(nums: Array[Int], left: Int, right: Int): Int = {
val randomIndex = left + (scala.util.Random.nextDouble() * (right - left + 1)).toInt
swap(nums, randomIndex, right)
val value = nums(right)
var correctIndex = left
var i = left
while (i <= right) {
if (nums(i) <= value) {
swap(nums, i, correctIndex)
correctIndex += 1
}
i += 1
}
correctIndex - 1
}
def smallestKNums(nums: Array[Int], left: Int, right: Int, k: Int): Unit = {
if (left >= right) {
return
}
val 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))
}
}
def kSmallest(nums: Array[Int], k: Int): Array[Int] = {
smallestKNums(nums, 0, nums.length - 1, k)
nums.slice(0, k)
}
}
Scala details:
whileinstead offor. The loop usesvar iandi += 1because Scala has no++. Afor (i <- left to right)would also work.- Random numbers.
scala.util.Random.nextInt(n)gives a number from 0 to n - 1 directly. The lesson multipliesnextDouble()instead, and.toInttruncates the fraction. slice(from, until)excludes theuntilindex and returns a new array, so the result is a copy.- The input is rearranged. After the call, the caller's
numsis in a different order. For the example it becomes2,1,2,3,3,5,5,6,7,9(the exact order depends on the random pivots). - Result of a method.
partitionandkSmallestend with an expression instead ofreturn. The earlyreturninsmallestKNumsworks because its result type is declared.
Complexity
Time: O(n) expected. The first partition scans n values. A random pivot lands in the middle part of the range often enough that the range we continue with is a constant fraction smaller on average, so the scans add up to a constant multiple of n. The worst case, with consistently bad pivots, is O(n²).
Space: O(log n) expected for the recursion, not counting the k-element result array.
Edge cases and common mistakes
kequal to the length. For[4, 4, 1]andk = 3the answer is all three values,1, 4, 4.klarger than the length. The search runs out of values, andslicestops at the end of the array, so you get every element.k = 0. The result has length 0.- Single element or empty array.
[7]withk = 1returns[7], and an empty array withk = 0returns an empty array. - Duplicates. Equal values are fine, as in the example with two 2s. But if every value is the same, the test
<=sends them all to the left, the pivot ends atright, and each call shrinks the range by one element. With 1,000 equal values andk = 1the code callspartition999 times, which is quadratic work. - Already sorted input. Because the pivot is random, it stays fast. For 1,000 sorted values and
k = 1it needs only a handful of calls topartition, and the exact count varies from run to run. - Using the wrong
kin the second branch. After keepingcountvalues, onlyk - countremain to be found. Passing the originalkasks for too many. - Expecting sorted output. Only the set of
kvalues is guaranteed, not their order.
Testing
[5, 9, 3, 6, 2, 1, 3, 2, 7, 5]withk = 4gives the values 1, 2, 2, 3 in some order.[4, 4, 1]withk = 3gives 1, 4, 4 in some order.[7]withk = 1gives[7].[4, 4, 1]withk = 0gives an empty array.
