Implementing Quick Select Algorithm - Finding kth largest/smallest element

Implementing Quick Select Algorithm - Finding kth largest/smallest element

Today I am going to write about a special type of algorithm for finding a kth largest or smallest element in the array. This is my favorite data structure question due to intricacy involved in solving the problem. There are many variants of this problem including more efficient using max/min heap. But I am going to pass that solution and prefer quick-select algorithm solely for the purpose of simplicity.

For the sake of this post, I am going to use Swift as a primary programming language for this exercise, but algorithm can be implemented in any programming language

Quick select is a special form of quick sort algorithm. However, instead of sorting the full array, it partially sorts it until it finds a desired pivot position. For example, if you input an array to quick-select and ask it to find kth smallest/largest element, as soon as it finds the pivot index equal to k, it returns an element at that index as an answer.

Algorithm can be summarized as follows,

For the sake of simplicity, I am going to use the case to find kth smallest element. With slight change we can use it to find kth largest element with similar flow

Source: https://carbon.now.sh/

Please note that indices low and high are inclusive of both ends. Which means for the initial iteration low = 0 and high = arraySize - 1

To help better understand the code, let's dry run it on random input and see how it outputs. For the sake of example, our input would be

var input = [100, 300, 2, 3, 1000, 67, 89, 10, 764, 1, 546]

At the start, input will be our input array, with low = 0 and high = 10 and value of k = 3 that is we want to find kth smallest element.

Please note that this is an in-place algorithm. Which means as it finds the element, it also reorders element. So array used on first iteration may not be exactly same as the one used during next iteration

First iteration,

k = 3

-- – – – – – – – – -- – – – – – – – – -- – – – – – – – – -- -- -- - --
| 100 |  300  |  2  |  3  |  67  |  89  |  10  |  1  |  546  |  1000  |  764  |
-- – – – – – – – – -- – – – – – – – – -- – – – – – – – – -- -- -- - --
⬆                                                                             ⬆                        ⬆
low                                                                        pivot                    high

low != high and pivot - low != k hence continue to next iteration

Second iteration

k = 3

-- – – – – – – – – -- – – – – – – – – -- – – – – – – – – -- -- -- - -
|  1  | 300 |  2  |  3   |  67  |  89  |  10  |  100  |  546  | 1000 |  764  |
-- – – – – – – – – -- – – – – – – – – -- – – – – – – – – -- -- -- - -
⬆                                                                 ⬆ ️
pivot                                                          high
low

low != high and pivot - low != k hence continue to next iteration

Third Iteration

k = 2

-- – – – – – – – – -- – – – – – – – – -- – – – – – – – – -- -- -- - --
|  1  |  2  |  3  |  67  |  89  |  10  |  100  |  300  |  546  | 1000 |  764  |
-- – – – – – – – – -- – – – – – – – – -- – – – – – – – – -- -- -- - --
         ⬆                                            ⬆         ⬆
        low                                         pivot     high

low != high and pivot - low != k hence continue to next iteration

Fourth iteration (Solution)

k = 2

-- – – – – – – – – -- – – – – – – – – -- – – – – – – – – -- -- -- - --
|  1  |  2  |  3  |  10  |  89  |  67  |  100  |  300  |  546  | 1000 |  764  |
-- – – – – – – – – -- – – – – – – – – -- – – – – – – – – -- -- -- - --
         ⬆             ⬆               ⬆
        low         pivot           high

pivot - low == k hence we found the solution. Return the element situated at index pivot


And now let's head back to implementation. The code is written in Swift which is the primary language I currently use.


func findKthSmallestElement(input: inout [Int], l: Int, r: Int, which: Int) -> Int {
    if l == r {
        return input[l]
    }

    if which >= 0 && which <= r - l {
        let pivot = findPivot(input: &input, l: l, r: r)
        if pivot - l == which {
            return input[pivot]
        } else {
            if which < pivot - l {
                return findKthSmallestElement(input: &input, l: l, r: pivot - 1, which: which)
            } else {
                return findKthSmallestElement(input: &input, l: pivot + 1, r: r, which: which - pivot - 1 + l)
            }
        }
    }
    return Int.max;
}

func findPivot(input: inout [Int], l: Int, r: Int) -> Int {
    var min = l - 1
    let pivot = input[r]

    for i in l..<r {
        if input[i] <= pivot {
            min += 1
            swapI(input: &input ,l: min, r: i)
        }
    }
    min += 1
    swapI(input: &input, l: min, r: r)
    return min
}

To verify the output, let's write some unit tests,

func testFindSmallestKthElement() {
    let array = [100, 300, 2, 3, 1000, 67, 89, 10, 764, 1, 546]
    var input = array
    XCTAssertEqual(findKthSmallestElement(input: &input, l: 0, r: input.count - 1, which: 0), 1)

    input = array
    XCTAssertEqual(findKthSmallestElement(input: &input, l: 0, r: input.count - 1, which: 3), 10)

    input = array
    XCTAssertEqual(findKthSmallestElement(input: &input, l: 0, r: input.count - 1, which: 5), 89)

    input = array
    XCTAssertEqual(findKthSmallestElement(input: &input, l: 0, r: input.count - 1, which: 7), 300)

    input = array
    // Request to find out of bounds kth minimum element
    XCTAssertEqual(findKthSmallestElement(input: &input, l: 0, r: input.count - 1, which: 13), Int.max)
}

Time Complexity:

For best and average cases quick-select runs in linear time. Since algorithm keeps dividing array in half after every iteration, the size of input to inspect keep getting split in half. For array of size n, time complexity for average and best cases could be calculated with following formula,

n + (n/2) + (n/4) +..... 1 = O(2n) = O(n) using Infinite Geometric series formula.

For worst case scenarios, this algorithm works same way as quick sort algorithm leading to O(n^2) time complexity for the input of size n. This happens when the pivot element keep falling on extreme end and algorithm runs until it has only one element to process which is desired kth smallest element.

You can easily simulate this scenario by using sorted (in increasing order) array as an input asking algorithm to find 0th smallest element. The element is situated at the very first position, but ironically it still takes O(n^2) time to find the element.


Finding kth largest element

So this is fine, but how do we find kth largest element in the input array. It's exactly same except for a one line change in func findPivot function. The condition where we check if if input[i] <= pivot only needs to be changed to if input[i] >= pivot.

This works because earlier we were selectively sorting input array in increasing order so that we can grab kth smallest element from left. Now we still want to search from left, but instead array will be reverse sorted. So the place where we used to get kth smallest element, we will now be getting kth largest element.


Reference: GeeksForGeeks.com