215. Kth Largest Element in an Array
Question
Given an unsorted integer array nums and an integer k, return the largest element in the array.
Note: It is the largest element in sorted order, not the distinct element.
Solution
Pattern
Quickselect (Hoare's / Lomuto Partitioning) A selection algorithm related to Quicksort. Instead of sorting both sides of a partition, we only recurse into the side containing our target index.
How to Identify
- Find the smallest/largest element.
- Requirement for average time complexity.
- Constraints suggest that sorting () is too slow or unnecessary.
Description
Step-by-step explanation:
- Target Index: In a sorted array of size , the largest element is at index
target = n - k. - Partitioning: Pick a pivot (randomly to avoid worst-case). Move all elements smaller than the pivot to its left and larger to its right.
- Check Pivot Position: After partitioning, the pivot is in its final sorted position
i.- If
i == target, you found the element. - If
i > target, search the left partition. - If
i < target, search the right partition.
- If
- Repeat: Continue until the search range narrows to the target index.
The Intuition
Think of Quickselect as "Goal-Oriented Quicksort". In Quicksort, you are trying to organize the entire room. In Quickselect, you only care about who sits in the chair. Once you partition the room, if the chair is on the left side, you completely ignore everyone on the right.
Why and not ?
- Level 1: Scan elements.
- Level 2: Scan elements.
- Level 3: Scan elements.
- Sum: , which is .
Complexity
| Label | Worst | Average |
|---|---|---|
| Time Complexity | ||
| Space Complexity |
Time Complexity
Average case is linear because the problem size reduces geometrically. Worst case (highly skewed partitions) is , though randomized pivoting makes this nearly impossible in practice.
Space Complexity
Strictly if implemented iteratively. Recursive implementations use stack space on average.
Code
import java.util.Random;
class Solution {
public int findKthLargest(int[] nums, int k) {
int n = nums.length;
int target = n - k; // Index in sorted array
int start = 0, end = n - 1;
Random rand = new Random();
while (start <= end) {
// Randomized pivot to prevent O(n^2)
int pivotIndex = start + rand.nextInt(end - start + 1);
int pivotPos = partition(nums, start, end, pivotIndex);
if (pivotPos == target) {
return nums[pivotPos];
} else if (pivotPos < target) {
start = pivotPos + 1;
} else {
end = pivotPos - 1;
}
}
return -1;
}
private int partition(int[] nums, int start, int end, int pivotIndex) {
int pivotValue = nums[pivotIndex];
swap(nums, pivotIndex, end); // Move pivot to end
int storeIndex = start;
for (int i = start; i < end; i++) {
if (nums[i] < pivotValue) {
swap(nums, i, storeIndex);
storeIndex++;
}
}
swap(nums, storeIndex, end); // Move pivot to its final place
return storeIndex;
}
private void swap(int[] a, int i, int j) {
int temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}Caveats
- Modifies Input: Quickselect is an in-place algorithm. If you cannot modify the original array, you must use a Min-Heap ( time, space).
- Duplicate Elements: Standard partitioning handles duplicates, but very large numbers of identical elements can lead to skewed partitions.
- Worst Case: Always mention that while average is , the theoretical worst case is .
Concepts to Think About
- Min-Heap Approach: Useful if is very small compared to or if data is streaming.
- Hoare vs. Lomuto: Hoare's partition uses two pointers from both ends and is generally faster with fewer swaps.
- Median of Medians: The "Introselect" algorithm uses this to ensure worst-case.
- Introselect: Used in C++
std::nth_element, which switches from Quickselect to Heapsort if recursion goes too deep.
Logical Follow-up
Question: What if the array is too large to fit in memory? Solution: Use a Min-Heap of size . Iterate through the file, keeping only the largest elements seen so far in the heap. Space: , Time: .
Question: What if the numbers are in a specific range (e.g., 1 to 1000)? Solution: Use Counting Sort logic. Count frequencies and iterate from 1000 downwards until you hit the element. Time: .
Solution 2 (using heap)
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for ( int i = 0; i < nums.length; ++i) {
minHeap.add(nums[i]);
// to ensure we keep the K smallest element
if (minHeap.size() > k) {
minHeap.remove();
}
}
return minHeap.peek();
}