Start learning free

LESSON 280 OF 415 · BINARY SEARCH

Smallest K Integers in Swift: O(n log n) Binary Search on Values

Practice O(n log n) Read the full explanation

Given an array of positive integers nums, return the smallest k values, in any order you want.

Example:

Input: nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5], k = 4
Output: [1, 2, 2, 3]
Explanation: Smallest number is 1, 2nd smallest is 2, 
            3rd smallest is 2, 4th smallest is 3
The result can be in any order, [2, 1, 3, 2] is also a correct answer.

Note:

For this lesson, your algorithm should run in O(n log V) time, where V is the range of values in nums (from the smallest to the largest value).


STEP 1 OF 23
    Ctrl + Enter to check

    SWIFT · BINARY SEARCH

    Smallest K in Swift, explained

    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. For nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5] and k = 4, the result contains 1, 2, 2 and 3. Duplicates count separately, so the two 2s are both in the answer and only one of the two 3s is.

    The statement asks for O(n log n) time. This lesson reaches it by binary search on the values themselves, without sorting the array.

    Approach: find the k-th smallest value, then collect

    If we knew the value of the k-th smallest element, call it kthNum, collecting the answer would be easy. Every element smaller than kthNum belongs to the answer. If fewer than k elements are smaller, the missing places are filled with copies of kthNum, because it may occur several times.

    To find kthNum, binary search over the range of values from the minimum to the maximum of nums. For a candidate value val, count how many elements are strictly smaller than it. If that count is below k, then val is not too large to be the k-th smallest: fewer than k elements precede it. So save it as the best answer so far and try bigger values. Otherwise at least k elements are smaller than val, so it is too big, and the search goes lower. The largest value that passes the test is the k-th smallest.

    In the example with k = 4, the value 3 has three smaller elements (1, 2, 2), fewer than 4, so it passes. The value 4 has five smaller elements (1, 2, 2, 3, 3), so it fails. The k-th smallest is 3.

    Step by step

    1. Write howManyLess above kSmallestNum. It takes _ nums: [Int] and _ val: Int and returns an Int. It starts var counter = 0, loops over nums with for num in nums, adds one when val > num, and returns counter.
    2. Write kSmallestNum, which takes _ nums: [Int] and _ k: Int and returns the value of the k-th smallest.
    3. Find the minimum and the maximum. Start var left and var right at nums[0] and update them with min and max in a loop.
    4. Start var result at left.
    5. Binary search while left <= right: take the middle value val, and store in smaller how many numbers are less than it, using howManyLess. Both are let, since each iteration makes new ones.
    6. If smaller < k, save val in result and set left = val + 1. Otherwise set right = val - 1.
    7. Return result.
    8. Finish kSmallest. Put the value from kSmallestNum into a constant kthNum, and make an empty var result of type [Int].
    9. Append everything smaller than kthNum.
    10. Pad with kthNum while result.count < k, and return result.

    The solution in Swift

    class Solution {
        func kSmallestNum(_ nums: [Int], _ k: Int) -> Int {
            var left = nums[0]
            var right = nums[0]
            for num in nums {
                left = min(left, num)
                right = max(right, num)
            }
            var result = left
            while left <= right {
                let val = left + (right - left) / 2
                let smaller = howManyLess(nums, val)
                if smaller < k {
                    result = val
                    left = val + 1
                } else {
                    right = val - 1
                }
            }
            return result
        }
    
        func howManyLess(_ nums: [Int], _ val: Int) -> Int {
            var counter = 0
            for num in nums {
                if val > num {
                    counter += 1
                }
            }
            return counter
        }
    
        func kSmallest(_ nums: [Int], _ k: Int) -> [Int] {
            let kthNum = kSmallestNum(nums, k)
            var result = [Int]()
            for val in nums {
                if val < kthNum {
                    result.append(val)
                }
            }
            while result.count < k {
                result.append(kthNum)
            }
            return result
        }
    }
    

    The Swift details worth a closer look:

    • Two result variables. kSmallestNum and kSmallest each have a local result, of type Int and [Int]. They are separate and don't clash.
    • [Int]() creates an empty array of integers, and append adds one element at the end. The array is a var because it grows.
    • Value semantics. The nums parameter is never modified, and passing it to howManyLess on every iteration does not copy the elements: Swift shares the storage until someone mutates it.
    • Midpoint without overflow. left + (right - left) / 2 avoids adding two potentially large values. Swift traps on overflow, so for values near Int.max this form matters.
    • An alternative count. nums.filter { $0 < val }.count gives the same number, but builds a temporary array first.

    Complexity

    Let n be the length of nums and M the largest value.

    Time: O(n log M). The search halves the value range [min, max] every iteration, so it runs about log M times, and each iteration counts the smaller elements in O(n). For integers that fit in a fixed width this is within the O(n log n) the statement allows. The final collection pass is O(n).

    Space: O(k) for the returned array, and no other extra memory.

    Edge cases and common mistakes

    • Duplicates of the k-th value. For [4, 4, 4, 4] and k = 2 no element is smaller than 4, so the result is filled with two 4s. Without the padding step the result would be empty.
    • k equals 1. The k-th smallest is the minimum, nothing is smaller, and the result is one copy of it.
    • k equals the length of the array. The k-th smallest is the maximum, and the result holds all the elements smaller than it plus enough copies of the maximum.
    • A single element. [7] with k = 1 returns [7].
    • Counting values that are at most val. Using <= in howManyLess changes the meaning of the test and shifts the answer to the wrong value. The test must count strictly smaller elements.
    • Empty input. nums[0] on an empty array crashes at run time, so the method needs at least one element.

    Testing

    • nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5], k = 4 gives the values 1, 2, 2 and 3 in some order; this code returns [2, 1, 2, 3].
    • nums = [4, 4, 4, 4], k = 2 gives [4, 4].
    • nums = [7], k = 1 gives [7].
    • nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5], k = 1 gives [1].
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free