Start learning free

LESSON 317 OF 415 · HEAP

Smallest K Integers in Swift: O(n log k) with a Max Heap

Practice O(n log k) 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 k) time and use O(k) extra space.
(There are faster solutions which we will discuss in future lessons)


STEP 1 OF 8
    Ctrl + Enter to check

    SWIFT · HEAP

    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. Duplicates count separately.

    In the example, nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5] and k = 4. Sorted, the array begins 1, 2, 2, 3, so the answer is those four values. The order doesn't matter, so [2, 1, 3, 2] is also correct.

    The lesson asks for O(n log k) time and O(k) extra space.

    Approach: keep the k smallest seen so far

    Sorting the whole array and taking the first k values works, but it costs O(n log n) time and ignores that k may be small. A heap does better by remembering only k values at any moment.

    The trick is to use a max heap, where the greatest element sits on top. Read the numbers one by one:

    • While the heap holds fewer than k values, add the new one.
    • Once it is full, the top is the greatest of the k smallest values so far. A new value smaller than the top belongs in the set, and the top is the one that no longer does. Remove the top and add the new value.
    • A new value that is not smaller than the top changes nothing.

    Each push or pop costs O(log k) because the heap never holds more than k values, and there are at most two per element, so the loop costs O(n log k). At the end the heap holds exactly the answer.

    Swift's standard library has no heap type. The lesson supplies a MaxHeap struct with push, pop, peek and count.

    Step by step

    1. Create the heap. var smallestKNums = MaxHeap(). It is a var because push and pop change the struct.
    2. Loop over the values. for value in nums.
    3. Fill the heap first. If smallestKNums.count < k, push value.
    4. Compare with the top when full. Otherwise write } else if value < smallestKNums.peek(). The method peek() reads the top without removing it.
    5. Swap the values. Inside that branch, call pop() to remove the greatest value, assigning its result to _ because it isn't needed, then push value. The heap again holds exactly k values.
    6. Empty the heap into an array. Declare var result = [Int](). While smallestKNums.count > 0, append the result of pop() to it.
    7. Return result. The values come out from largest to smallest, which is fine because any order is accepted.

    The solution in Swift

    struct MaxHeap {
        private var data = [Int]()
        var count: Int { return data.count }
        func peek() -> Int { return data[0] }
        mutating func push(_ value: Int) {
            data.append(value)
            var i = data.count - 1
            while i > 0 {
                let parent = (i - 1) / 2
                if data[parent] >= data[i] { break }
                data.swapAt(parent, i)
                i = parent
            }
        }
        mutating func pop() -> Int {
            let top = data[0]
            let last = data.removeLast()
            if !data.isEmpty {
                data[0] = last
                var i = 0
                let size = data.count
                while true {
                    let left = 2 * i + 1
                    let right = 2 * i + 2
                    var largest = i
                    if left < size && data[left] > data[largest] { largest = left }
                    if right < size && data[right] > data[largest] { largest = right }
                    if largest == i { break }
                    data.swapAt(i, largest)
                    i = largest
                }
            }
            return top
        }
    }
    
    class Solution {
        func kSmallest(_ nums: [Int], _ k: Int) -> [Int] {
            var smallestKNums = MaxHeap()
            for value in nums {
                if smallestKNums.count < k {
                    smallestKNums.push(value)
                } else if value < smallestKNums.peek() {
                    _ = smallestKNums.pop()
                    smallestKNums.push(value)
                }
            }
            var result = [Int]()
            while smallestKNums.count > 0 {
                result.append(smallestKNums.pop())
            }
            return result
        }
    }
    

    The Swift details worth a closer look:

    • The heap is an array. The element at index i has its children at 2 * i + 1 and 2 * i + 2 and its parent at (i - 1) / 2. push adds at the end and moves the value up while it is larger than its parent. pop moves the last element to the root and sinks it down, swapping with the larger child.
    • mutating. A struct is a value type, so a method that changes data must be marked mutating, and it can only be called on a var. peek() and count only read, so they need neither.
    • private var data. Code outside the struct can't touch the array, so only push and pop can break the heap order.
    • _ = smallestKNums.pop(). pop() returns a value and isn't marked to allow ignoring it, so a bare call produces a compiler warning about an unused result. Assigning to _ says the discard is deliberate.
    • Value semantics. Assigning a MaxHeap to another variable copies it, so the two heaps then change independently.
    • swapAt. data.swapAt(i, j) exchanges two elements in place.

    Complexity

    Time: O(n log k). Each of the n values costs a comparison with the top and, at most, one pop and one push on a heap of size k, each O(log k). Draining the heap at the end costs k pops, which is O(k log k) and no more than the main loop.

    Space: O(k). The heap never holds more than k values, and the result array also has k.

    Edge cases and common mistakes

    • Duplicates. The comparison is strict, so a value equal to the top is skipped, which is fine because swapping equal values changes nothing. For [4, 4, 4] with k = 2 the result is [4, 4].
    • k larger than the array. The heap never fills, every value is pushed, and the method returns all values. [5, 9, 3] with k = 5 gives [9, 5, 3].
    • Empty array. The method returns an empty array.
    • k = 0. The reference does not handle it: the heap stays empty, and the first value reaches peek(), which reads data[0] of an empty array and crashes the program. Guard against it with if k <= 0 { return [] } when k can be 0.
    • Using a min heap. The top would be the smallest value, and you would be discarding the wrong element. The set of k smallest values needs its greatest member on top.
    • Expecting sorted output. The values come out in descending order. Sort the result if you need ascending order.

    Testing

    • nums = [5, 9, 3, 6, 2, 1, 3, 2, 7, 5], k = 4 gives [3, 2, 2, 1], which has the same values as the example.
    • nums = [5, 9, 3], k = 5 gives [9, 5, 3].
    • nums = [4, 4, 4], k = 2 gives [4, 4].
    • nums = [], k = 3 gives [].
    Keep going with SwiftAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free