|
1 | 1 | # k-th Largest Element Problem
|
2 | 2 |
|
3 |
| -You're given an integer array `a`. Write an algorithm that finds the kth largest element in the array. |
| 3 | +You're given an integer array `a`. Write an algorithm that finds the *k*-th largest element in the array. |
4 | 4 |
|
5 |
| -The following solution is semi-naive. Its time complexity is O(n log n) since it first sorts the array, and uses an additional O(n) space. Better solutions using heaps exist that run in O(n) time. |
| 5 | +For example, the 1-st largest element is the maximum value that occurs in the array. If the array has *n* elements, the *n*-th largest element is the minimum. The median is the *n/2*-th largest element. |
| 6 | + |
| 7 | +## The naive solution |
| 8 | + |
| 9 | +The following solution is semi-naive. Its time complexity is **O(n log n)** since it first sorts the array, and uses an additional **O(n)** space. Better solutions using heaps exist that run in **O(n)** time. |
6 | 10 |
|
7 | 11 | ```swift
|
8 | 12 | func kthLargest(a: [Int], k: Int) -> Int? {
|
9 |
| - let len = a.count |
| 13 | + let len = a.count |
10 | 14 |
|
11 |
| - if (k <= 0 || k > len || len < 1) { return nil } |
| 15 | + if k <= 0 || k > len || len < 1 { return nil } |
12 | 16 |
|
13 |
| - let sorted = a.sort() |
| 17 | + let sorted = a.sort() |
14 | 18 |
|
15 |
| - return sorted[len - k] |
| 19 | + return sorted[len - k] |
16 | 20 | }
|
17 | 21 | ```
|
18 | 22 |
|
19 |
| -The `kthLargest()` function takes two parameters: the array `a` with consisting of integers, and `k`, in order to find the kth largest element. It returns the kth largest element. |
| 23 | +The `kthLargest()` function takes two parameters: the array `a` consisting of integers, and `k`. It returns the k-th largest element. |
20 | 24 |
|
21 | 25 | Let's take a look at an example and run through the algorithm to see how it works. Given `k = 4` and the array:
|
22 | 26 |
|
23 | 27 | ```swift
|
24 | 28 | [ 7, 92, 23, 9, -1, 0, 11, 6 ]
|
25 | 29 | ```
|
26 | 30 |
|
27 |
| -Initially there's no direct way to find the kth largest element, but after sorting the array it's rather straightforward. Here's the sorted array: |
| 31 | +Initially there's no direct way to find the k-th largest element, but after sorting the array it's rather straightforward. Here's the sorted array: |
28 | 32 |
|
29 | 33 | ```swift
|
30 | 34 | [ -1, 0, 6, 7, 9, 11, 23, 92 ]
|
31 | 35 | ```
|
32 | 36 |
|
33 |
| -Now, all we must do is take the kth largest value, which is located at: `a[a.count - k] = a[8 - 4] = a[4] = 9` |
| 37 | +Now, all we must do is take the value at index `a.count - k`: |
| 38 | + |
| 39 | +```swift |
| 40 | +a[a.count - k] = a[8 - 4] = a[4] = 9 |
| 41 | +``` |
| 42 | + |
| 43 | +Of course, if you were looking for the k-th *smallest* element, you'd use `a[k]`. |
| 44 | + |
| 45 | +## A faster solution |
| 46 | + |
| 47 | +There is a clever algorithm that combines the ideas of [binary search](../Binary Search/) and [quicksort](../Quicksort/) to arrive at an **O(n)** solution. |
| 48 | + |
| 49 | +Recall that binary search splits the array in half over and over again, to quickly narrow in on the value you're searching for. That's what we'll do here too. |
| 50 | + |
| 51 | +Quicksort also splits up arrays. It uses partitioning to move all smaller values to the left of the pivot and all greater values to the right. After partitioning around a certain pivot, that pivot will already be in its final, sorted position. We can use that to our advantage here. |
| 52 | + |
| 53 | +We choose a random pivot, partition the array around that pivot, and then act like a binary search and only continue in the left or right partition. This repeats until we've found a pivot that happens to end up in the *k*-th position. |
| 54 | + |
| 55 | +Let's look at the original example again. We're looking for the 4-th largest element in this array: |
| 56 | + |
| 57 | + [ 7, 92, 23, 9, -1, 0, 11, 6 ] |
| 58 | + |
| 59 | +The algorithm is a bit easier to follow if we look for the k-th *smallest* item instead, so let's take `k = 4` and look for the 4-th smallest element. |
| 60 | + |
| 61 | +Note that we don't have to sort the array first. We pick a random pivot, let's say `11` and partition the array around that. We might end up with something like this: |
| 62 | + |
| 63 | + [ 7, 9, -1, 0, 6, 11, 92, 23 ] |
| 64 | + <------ smaller larger --> |
| 65 | + |
| 66 | +As you can see, all values smaller than `11` are on the left; all values larger are on the right. The pivot value is now in its final place. The index of the pivot is 5, so the 4-th smallest element must be in the left partition somewhere. We can ignore the rest of the array from now on: |
| 67 | + |
| 68 | + [ 7, 9, -1, 0, 6, x, x, x ] |
| 69 | + |
| 70 | +Again let's pick a random pivot, let's say `6`, and partition the array around it. We might end up with something like this: |
| 71 | + |
| 72 | + [ -1, 0, 6, 9, 7, x, x, x ] |
| 73 | + |
| 74 | +Pivot `6` ended up at index 2, so obviously the 4-th smallest item must be in the right partition. We can ignore the left partition: |
| 75 | + |
| 76 | + [ x, x, x, 9, 7, x, x, x ] |
| 77 | + |
| 78 | +Again we pick a pivot value at random, let's say `9`, and partition the array: |
| 79 | + |
| 80 | + [ x, x, x, 7, 9, x, x, x ] |
| 81 | + |
| 82 | +The index of pivot `9` is 4, and that's exactly the *k* we're looking for. We're done! Notice how this only took a few steps and we did not have to sort the array first. |
| 83 | + |
| 84 | +The following function implements these ideas: |
| 85 | + |
| 86 | +```swift |
| 87 | +public func randomizedSelect<T: Comparable>(array: [T], order k: Int) -> T { |
| 88 | + var a = array |
| 89 | + |
| 90 | + func randomPivot<T: Comparable>(inout a: [T], _ low: Int, _ high: Int) -> T { |
| 91 | + let pivotIndex = random(min: low, max: high) |
| 92 | + swap(&a, pivotIndex, high) |
| 93 | + return a[high] |
| 94 | + } |
| 95 | + |
| 96 | + func randomizedPartition<T: Comparable>(inout a: [T], _ low: Int, _ high: Int) -> Int { |
| 97 | + let pivot = randomPivot(&a, low, high) |
| 98 | + var i = low |
| 99 | + for j in low..<high { |
| 100 | + if a[j] <= pivot { |
| 101 | + swap(&a, i, j) |
| 102 | + i += 1 |
| 103 | + } |
| 104 | + } |
| 105 | + swap(&a, i, high) |
| 106 | + return i |
| 107 | + } |
| 108 | + |
| 109 | + func randomizedSelect<T: Comparable>(inout a: [T], _ low: Int, _ high: Int, _ k: Int) -> T { |
| 110 | + if low < high { |
| 111 | + let p = randomizedPartition(&a, low, high) |
| 112 | + if k == p { |
| 113 | + return a[p] |
| 114 | + } else if k < p { |
| 115 | + return randomizedSelect(&a, low, p - 1, k) |
| 116 | + } else { |
| 117 | + return randomizedSelect(&a, p + 1, high, k) |
| 118 | + } |
| 119 | + } else { |
| 120 | + return a[low] |
| 121 | + } |
| 122 | + } |
| 123 | + |
| 124 | + precondition(a.count > 0) |
| 125 | + return randomizedSelect(&a, 0, a.count - 1, k) |
| 126 | +} |
| 127 | +``` |
| 128 | + |
| 129 | +To keep things readable, it splits up the functionality into three inner functions: |
| 130 | + |
| 131 | +- `randomPivot()` picks a random number and puts it at the end of the current partition (this is a requirement of the Lomuto partitioning scheme, see the discussion on [quicksort](../Quicksort/) for more details). |
| 132 | + |
| 133 | +- `randomizedPartition()` is Lomuto's partitioning scheme from quicksort. When this completes, the randomly chosen pivot is in its final sorted position in the array. It returns the array index of the pivot. |
| 134 | + |
| 135 | +- `randomizedSelect()` does all the hard work. It first calls the partitioning function and then decides what to do next. If the index of the pivot is equal to the k-th number we're looking for, we're done. If `k` is less than the pivot index, it must be in the left partition and we'll recursively try again there. Likewise for when the k-th number must be in the right partition. |
| 136 | + |
| 137 | +Pretty cool, huh? |
| 138 | + |
| 139 | +> **Note:** This function calculates the *k*-th smallest item in the array, where *k* starts at 0. If you want the *k*-th largest item, call it with `a.count - k`. |
34 | 140 |
|
35 |
| -*Written by Daniel Speiser* |
| 141 | +*Written by Daniel Speiser. Additions by Matthijs Hollemans.* |
0 commit comments