275. Kth Largest Element in an Array
π LeetCode Problem: 215. Kth Largest Element in an Array
π Difficulty: Medium
π·οΈ Topics: Array, Divide and Conquer, Sorting, Heap (Priority Queue), Quickselect
Problem Statement
Given an integer array nums and an integer k, return the kth largest element in the array.
Note that it is the kth largest element in the sorted order, not the kth distinct element.
Can you solve it without sorting?
Example 1:
Input: nums = [3,2,1,5,6,4], k = 2
Output: 5
Example 2:
Input: nums = [3,2,3,1,2,4,5,5,6], k = 4
Output: 4
Constraints:
- 1 <= k <= nums.length <= 10^5
- -10^4 <= nums[i] <= 10^4
π ELI5: The Simple Idea!
Finding the Kth largest element:
Array: [3, 2, 1, 5, 6, 4]
Find: 2nd largest
If sorted: [6, 5, 4, 3, 2, 1]
β β
1st 2nd largest
Answer: 5
But we can do better than full sort!
Three Different Strategies:
Strategy 1: SORT (Simple)
- Sort array: [1, 2, 3, 4, 5, 6]
- Return nums[n-k]: nums[6-2] = nums[4] = 5
- Time: O(n log n)
- Space: O(1) or O(n) depending on sort
Strategy 2: MIN-HEAP (Efficient)
- Keep heap of size k (smallest on top)
- Only largest k elements stay
- Top of heap = kth largest
- Time: O(n log k)
- Space: O(k)
Strategy 3: QUICKSELECT (Optimal)
- Like quicksort's partition
- Find position k-1 from end
- Average Time: O(n)
- Space: O(1)
π¨ Visual Understanding
Understanding "Kth Largest"
Array: [3, 2, 1, 5, 6, 4]
k = 2 (find 2nd largest)
Sorted (descending): [6, 5, 4, 3, 2, 1]
β β
1st 2nd largest
Answer: 5
Sorted (ascending): [1, 2, 3, 4, 5, 6]
β β
2nd 1st largest
Index from end: n-k = 6-2 = 4
nums[4] = 5 β
Why Different Approaches?
SORTING:
Pros: Simple, easy to understand
Cons: O(n log n) - sorts entire array
Overkill if we only need one element!
MIN-HEAP (Size k):
Pros: Better than sorting - O(n log k)
Only keeps k elements in memory
Cons: Extra space O(k)
Still not linear time
QUICKSELECT:
Pros: Average O(n) - optimal!
In-place (no extra space)
Cons: Worst case O(nΒ²) if unlucky
More complex to implement
π― Approach 1: Sorting (Simplest) β
The Straightforward Solution
Algorithm:
1. Sort array in ascending order
2. Return element at index (n - k)
- n-k gives us kth from end
- Example: n=6, k=2 β index 4 (2nd largest)
Implementation
import java.util.*;
/**
* Simple sorting approach
* Time: O(n log n)
* Space: O(1) or O(n) depending on sort implementation
*/
class Solution {
public int findKthLargest(int[] nums, int k) {
// Sort array in ascending order
Arrays.sort(nums);
// Kth largest is at index (n - k)
return nums[nums.length - k];
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.findKthLargest(new int[]{3,2,1,5,6,4}, 2)); // 5
System.out.println(sol.findKthLargest(new int[]{3,2,3,1,2,4,5,5,6}, 4)); // 4
}
}
β° Time: O(n log n) - Sorting dominates
πΎ Space: O(1) or O(log n) depending on sort algorithm
Pros: - Simple and easy to understand - Short code - No edge cases to worry about
Cons: - O(n log n) is slow for large arrays - Sorts entire array when we only need one element - Not optimal
π― Approach 2: Min-Heap of Size k (Better) ββ
Using Priority Queue to Track Largest k Elements
The Key Insight:
Keep a min-heap of size k:
- Min-heap keeps smallest on top
- Maintain only k largest elements
- Top of heap = kth largest!
Why min-heap and not max-heap?
- Min-heap evicts smallest easily
- Always maintain k largest elements
- Top is the "smallest of the largest k" = kth largest!
Visual Tracking - Complete Example
Array: [3, 2, 1, 5, 6, 4]
k = 2 (find 2nd largest)
Goal: Keep heap of size 2 with 2 largest elements
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
STEP 1: Process 3
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Heap: []
Add 3
Heap: [3]
Size: 1 < k (2) β Keep it
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
STEP 2: Process 2
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Heap: [3]
Add 2
Heap: [2, 3] (min-heap, 2 is smallest)
β
min (top)
Size: 2 == k β Heap full now!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
STEP 3: Process 1
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Heap: [2, 3]
Compare: 1 vs top (2)
1 < 2? YES β Skip it (too small)
Heap: [2, 3] (unchanged)
Why skip?
1 is smaller than smallest in heap
β Not one of the 2 largest
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
STEP 4: Process 5
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Heap: [2, 3]
Compare: 5 vs top (2)
5 > 2? YES β Replace!
Remove top (2):
Heap: [3]
Add 5:
Heap: [3, 5]
β
min (top)
Why replace?
5 is larger than smallest (2)
β 5 should be in top 2
β Remove 2, add 5
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
STEP 5: Process 6
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Heap: [3, 5]
Compare: 6 vs top (3)
6 > 3? YES β Replace!
Remove top (3):
Heap: [5]
Add 6:
Heap: [5, 6]
β
min (top)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
STEP 6: Process 4
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Heap: [5, 6]
Compare: 4 vs top (5)
4 < 5? YES β Skip it
Heap: [5, 6] (unchanged)
Why skip?
4 is smaller than smallest in heap
β Not one of the 2 largest
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
FINAL RESULT
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Heap: [5, 6]
β
Answer: 5 (top of heap)
The top of the heap is the 2nd largest! β
Visual Summary:
Start: []
Add 3: [3]
Add 2: [2,3]
Skip 1: [2,3] (1 too small)
Add 5, remove 2: [3,5]
Add 6, remove 3: [5,6]
Skip 4: [5,6] (4 too small)
Final: [5,6] β top = 5 = 2nd largest β
Implementation
import java.util.*;
/**
* Min-Heap approach (PriorityQueue)
* Time: O(n log k)
* Space: O(k)
*/
class Solution {
public int findKthLargest(int[] nums, int k) {
// Min-heap to keep k largest elements
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int num : nums) {
// Add to heap
minHeap.offer(num);
// If heap size exceeds k, remove smallest
if (minHeap.size() > k) {
minHeap.poll();
}
}
// Top of heap is kth largest
return minHeap.peek();
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.findKthLargest(new int[]{3,2,1,5,6,4}, 2)); // 5
System.out.println(sol.findKthLargest(new int[]{3,2,3,1,2,4,5,5,6}, 4)); // 4
}
}
β° Time: O(n log k) - n elements, each heap operation O(log k)
πΎ Space: O(k) - Heap stores k elements
Pros: - Better than sorting: O(n log k) vs O(n log n) - Space efficient for small k - Handles duplicates naturally
Cons: - Still not O(n) - Requires extra space O(k)
π― Approach 3: Quickselect (Optimal) βββ
The Partition-Based Selection Algorithm
The Key Insight:
Quickselect is like Quicksort, but:
- Quicksort: Sort both sides of pivot
- Quickselect: Only recurse on ONE side!
After partition:
- Elements β€ pivot on left
- Pivot at correct sorted position
- Elements > pivot on right
If pivot is at position we want β Done!
Otherwise, recurse on the side containing our target.
Understanding Target Index First
CRITICAL CONCEPT: What index are we looking for?
Problem: Find kth largest element
Array: [3, 2, 1, 5, 6, 4], k = 2
If we sort in ASCENDING order: [1, 2, 3, 4, 5, 6]
β β
2nd 1st largest
Position from end:
1st largest = index 5 (last position)
2nd largest = index 4 (second from last)
kth largest = index (n - k)
For k=2, n=6: target index = 6 - 2 = 4
So we want: nums[4] after sorting in ascending order
Why ascending? Because our partition puts smaller elements
on left, larger on right. Natural ascending order!
Understanding Pivot Position Check
CRITICAL CONCEPT: What does pivotIndex mean?
After partition, pivot is at its CORRECT SORTED position!
If array were fully sorted in ascending order:
[sorted array]
β β
index 0 index (n-1)
After partition with pivot at position p:
[unsorted left] | pivot | [unsorted right]
β
position p
The pivot would be at SAME position p if array were fully sorted!
Checking pivotIndex vs targetIndex:
1. pivotIndex == targetIndex:
Found it! Pivot is at the exact position we want!
2. pivotIndex < targetIndex:
Target is to the RIGHT of pivot
β Target element is LARGER than pivot
β Recurse on RIGHT partition
3. pivotIndex > targetIndex:
Target is to the LEFT of pivot
β Target element is SMALLER than pivot
β Recurse on LEFT partition
Example:
Target index = 4 (want 2nd largest)
After partition, pivot at index 3
3 < 4? YES β Target is at higher index
β Target is LARGER than current pivot
β Recurse on RIGHT side
Visual Tracking - Complete Example
Array: [3, 2, 1, 5, 6, 4]
k = 2 (find 2nd largest)
Target index: n - k = 6 - 2 = 4
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
INITIAL STATE
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Array: [3, 2, 1, 5, 6, 4]
Index: 0 1 2 3 4 5
Goal: Find element that would be at index 4 if sorted ascending
[1, 2, 3, 4, 5, 6] β nums[4] = 5
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PARTITION 1: Partition entire array [left=0, right=5]
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Array: [3, 2, 1, 5, 6, 4]
β β
left right
Step 1: Choose pivot = nums[right] = 4
Step 2: Partition - Group elements by pivot
Goal: Rearrange so all elements < 4 are on left
i = -1 (marks "end of smaller elements section")
Think of array as: [smaller section | larger section]
β
i marks end
Loop j from 0 to 4:
j=0: nums[0]=3, is 3 < 4? YES
3 should be in "smaller section"
Expand smaller section: i++ β i=0
Put 3 at position i: swap(nums[0], nums[0])
Array: [3, 2, 1, 5, 6, 4]
β
i=0 (smaller section ends here)
Q: Why swap(0,0)? Element already in place!
A: In general, j might be ahead of i (in larger section).
This swap brings element from larger section into smaller.
Here they're same, so swap does nothing. Algorithm works
uniformly without special cases!
j=1: nums[1]=2, is 2 < 4? YES
2 should be in "smaller section"
Expand smaller section: i++ β i=1
Put 2 at position i: swap(nums[1], nums[1])
Array: [3, 2, 1, 5, 6, 4]
β
i=1 (smaller section ends here)
j=2: nums[2]=1, is 1 < 4? YES
Expand smaller section: i++ β i=2
Put 1 at position i: swap(nums[2], nums[2])
Array: [3, 2, 1, 5, 6, 4]
β
i=2 (smaller section ends here)
j=3: nums[3]=5, is 5 < 4? NO
5 should be in "larger section"
Don't expand smaller section (i stays at 2)
Array: [3, 2, 1, 5, 6, 4]
β
i=2 (smaller section still ends here)
j=4: nums[4]=6, is 6 < 4? NO
Don't expand smaller section
Array: [3, 2, 1, 5, 6, 4]
β
i=2 (smaller section still ends here)
Step 3: Place pivot in correct position
Smaller section: indices 0 to i (0 to 2)
Pivot should go: right after smaller section at i+1
Swap nums[i+1] with nums[right]
Swap nums[3] with nums[5]
Array: [3, 2, 1, 4, 6, 5]
Index: 0 1 2 3 4 5
β
pivot at index 3
Partition Result:
[3, 2, 1] | 4 | [6, 5]
< 4 β > 4
position 3
Interpretation:
If we fully sorted: [1, 2, 3, 4, 5, 6]
Element 4 would be at index 3 β
Our pivot IS at its correct sorted position!
Check: Is this our target?
Pivot index: 3
Target index: 4
3 < 4? YES
β Pivot is to the LEFT of target
β Target element is LARGER than pivot (4)
β Target is in RIGHT partition
Recurse: quickselect(nums, 4, 5, target=4)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PARTITION 2: Partition right side [left=4, right=5]
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Array: [3, 2, 1, 4, 6, 5]
β β
left right
Step 1: Choose pivot = nums[right] = 5
Step 2: Partition around pivot = 5
i = 3 (start at left-1)
Loop j from 4 to 4:
j=4: nums[4]=6, is 6 < 5? NO
Don't expand smaller section
i remains 3
Step 3: Place pivot
Swap nums[i+1] with nums[right]
Swap nums[4] with nums[5]
Array: [3, 2, 1, 4, 5, 6]
Index: 0 1 2 3 4 5
β
pivot at index 4
Partition Result:
[3, 2, 1, 4] | 5 | [6]
β
position 4
Check: Is this our target?
Pivot index: 4
Target index: 4
4 == 4? YES β
β Found it!
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
RESULT
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Element at index 4 = 5
If fully sorted: [1, 2, 3, 4, 5, 6]
β
index 4 = 5
Answer: 5 (the 2nd largest) β
Key Observations:
1. Array is NOT fully sorted: [3, 2, 1, 4, 5, 6]
Left [3,2,1,4] - not sorted internally
Right [6] - happens to be sorted (only 1 element)
2. But element at index 4 IS correct!
If we sorted: [1, 2, 3, 4, 5, 6]
Position 4 would still have 5 β
3. Quickselect only ensures target position is correct
Rest of array can be in any order!
Why the Swap Logic Works
DETAILED EXPLANATION: Why swap even when i == j?
The partition algorithm has TWO sections:
[smaller section | unprocessed section | pivot]
0 to i i+1 to j-1 right
Variable i: "last index of smaller section"
Variable j: "current element being examined"
When nums[j] < pivot:
1. Increment i (expand smaller section by 1)
2. Swap nums[i] with nums[j]
β Brings nums[j] into smaller section
β Pushes whatever was at nums[i] out
Example where swap matters:
[3, 2, 1, 5, 6, 4] pivot=4
j=0: 3<4, i=0, swap(0,0) β [3, 2, 1, 5, 6, 4]
j=1: 2<4, i=1, swap(1,1) β [3, 2, 1, 5, 6, 4]
j=2: 1<4, i=2, swap(2,2) β [3, 2, 1, 5, 6, 4]
j=3: 5>4, skip
j=4: 6>4, skip
After loop: [3, 2, 1, 5, 6, 4]
β β
i=2 j=4
Now i=2 (smaller section: 0-2)
Place pivot: swap(i+1=3, right=5) β [3, 2, 1, 4, 6, 5]
Example where swap really swaps:
[5, 2, 1, 3, 6, 4] pivot=4
j=0: 5>4, skip (i=-1)
j=1: 2<4, i=0, swap(0,1) β [2, 5, 1, 3, 6, 4]
β β
move 2 into smaller
move 5 out to larger
j=2: 1<4, i=1, swap(1,2) β [2, 1, 5, 3, 6, 4]
β β
move 1 in
move 5 out
j=3: 3<4, i=2, swap(2,3) β [2, 1, 3, 5, 6, 4]
β β
move 3 in
move 5 out
j=4: 6>4, skip
After loop: [2, 1, 3, 5, 6, 4]
β
i=2
Place pivot: swap(3, 5) β [2, 1, 3, 4, 6, 5]
The algorithm always works the same way!
Sometimes swap does nothing (i==j), sometimes it actually swaps.
Uniform logic, no special cases needed!
Implementation
import java.util.*;
/**
* Quickselect approach (optimal)
* Average Time: O(n)
* Worst Time: O(nΒ²) if unlucky pivots
* Space: O(1)
*/
class Solution {
public int findKthLargest(int[] nums, int k) {
// Convert to "kth smallest" problem
// kth largest = (n-k)th smallest (0-indexed)
int targetIndex = nums.length - k;
return quickselect(nums, 0, nums.length - 1, targetIndex);
}
private int quickselect(int[] nums, int left, int right, int targetIndex) {
// Partition array
int pivotIndex = partition(nums, left, right);
if (pivotIndex == targetIndex) {
// Found it!
return nums[pivotIndex];
} else if (pivotIndex < targetIndex) {
// Target is in right partition
return quickselect(nums, pivotIndex + 1, right, targetIndex);
} else {
// Target is in left partition
return quickselect(nums, left, pivotIndex - 1, targetIndex);
}
}
private int partition(int[] nums, int left, int right) {
// Choose rightmost element as pivot
int pivot = nums[right];
int i = left - 1; // Boundary of smaller elements
for (int j = left; j < right; j++) {
if (nums[j] < pivot) {
i++;
swap(nums, i, j);
}
}
// Place pivot in correct position
swap(nums, i + 1, right);
return i + 1;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.findKthLargest(new int[]{3,2,1,5,6,4}, 2)); // 5
System.out.println(sol.findKthLargest(new int[]{3,2,3,1,2,4,5,5,6}, 4)); // 4
}
}
β° Time: O(n) average, O(nΒ²) worst case
πΎ Space: O(1) in-place
Pros: - Optimal average time O(n) - In-place (no extra space) - Partial sorting
Cons: - Worst case O(nΒ²) if unlucky pivots - More complex to implement - Modifies original array
π― Approach 4: Quickselect with Random Pivot (Production Ready) βββ
The ONLY Difference: Random Pivot Selection
Everything else is EXACTLY the same as Approach 3!
Approach 3: Always picks rightmost element as pivot
Approach 4: Randomly picks any element as pivot, then swaps to right
That's it! Just 2 extra lines of code!
Why Add Random Pivot?
PROBLEM: Worst case O(nΒ²) on sorted/nearly-sorted arrays
Example: [1, 2, 3, 4, 5, 6], k=2, target index=4
With rightmost pivot:
Partition 1: pivot=6 β position 5, recurse [0,4]
Partition 2: pivot=5 β position 4, recurse [0,3]
Found! (lucky this time)
But if target was at index 0:
Partition 1: pivot=6 β position 5, recurse [0,4]
Partition 2: pivot=5 β position 4, recurse [0,3]
Partition 3: pivot=4 β position 3, recurse [0,2]
Partition 4: pivot=3 β position 2, recurse [0,1]
Partition 5: pivot=2 β position 1, recurse [0,0]
Work: n + (n-1) + (n-2) + ... = O(nΒ²) β
SOLUTION: Random pivot avoids this!
With random pivot:
High probability of balanced partitions
Expected work: n + n/2 + n/4 + ... β 2n = O(n) β
Even if pivot isn't perfect (50-50 split),
as long as it's reasonable (25-75 split),
we still get O(n) expected time!
Code Difference - Side by Side
// APPROACH 3: Fixed pivot (rightmost)
private int partition(int[] nums, int left, int right) {
int pivot = nums[right]; // Always rightmost
int i = left - 1;
for (int j = left; j < right; j++) {
if (nums[j] < pivot) {
i++;
swap(nums, i, j);
}
}
swap(nums, i + 1, right);
return i + 1;
}
// APPROACH 4: Random pivot
private Random random = new Random(); // Add this field
private int partition(int[] nums, int left, int right) {
// ONLY CHANGE: Pick random index and swap to right
int randomIndex = left + random.nextInt(right - left + 1);
swap(nums, randomIndex, right);
// Now continue exactly like Approach 3!
int pivot = nums[right]; // Now it's a random element
int i = left - 1;
for (int j = left; j < right; j++) {
if (nums[j] < pivot) {
i++;
swap(nums, i, j);
}
}
swap(nums, i + 1, right);
return i + 1;
}
That's literally the ONLY difference! - Line 1: Pick random index - Line 2: Swap random element to right - Rest: Identical to Approach 3
Visual Example - Same Array, Random Pivot
Array: [3, 2, 1, 5, 6, 4], k=2, target=4
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PARTITION 1: With random pivot
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Instead of always picking nums[5]=4,
let's say random picks index 3 β nums[3]=5
Step 1: Swap random pivot to end
Swap nums[3] with nums[5]
Array: [3, 2, 1, 4, 6, 5]
β pivot
Step 2-3: Now partition EXACTLY like Approach 3!
(Same partition logic, just different pivot value)
After partition: [3, 2, 1, 4] | 5 | [6]
β
position 4
Check: position 4 == target 4? YES β
Answer: 5
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Different random pivot β different partition
But same algorithm, same result!
Complete Implementation
import java.util.*;
/**
* Quickselect with random pivot (ONLY difference from Approach 3)
* Expected Time: O(n) with high probability
* Space: O(1)
*/
class Solution {
private Random random = new Random(); // ADD THIS
public int findKthLargest(int[] nums, int k) {
int targetIndex = nums.length - k;
return quickselect(nums, 0, nums.length - 1, targetIndex);
}
private int quickselect(int[] nums, int left, int right, int targetIndex) {
if (left == right) {
return nums[left];
}
// Partition (now with random pivot)
int pivotIndex = partition(nums, left, right);
if (pivotIndex == targetIndex) {
return nums[pivotIndex];
} else if (pivotIndex < targetIndex) {
return quickselect(nums, pivotIndex + 1, right, targetIndex);
} else {
return quickselect(nums, left, pivotIndex - 1, targetIndex);
}
}
private int partition(int[] nums, int left, int right) {
// ONLY 2 LINES ADDED: Random pivot selection
int randomIndex = left + random.nextInt(right - left + 1);
swap(nums, randomIndex, right);
// Everything else IDENTICAL to Approach 3!
int pivot = nums[right];
int i = left - 1;
for (int j = left; j < right; j++) {
if (nums[j] < pivot) {
i++;
swap(nums, i, j);
}
}
swap(nums, i + 1, right);
return i + 1;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.findKthLargest(new int[]{3,2,1,5,6,4}, 2)); // 5
System.out.println(sol.findKthLargest(new int[]{3,2,3,1,2,4,5,5,6}, 4)); // 4
}
}
Summary: Approach 3 vs 4
βββββββββββββββββββββββ¬βββββββββββββββββββ¬ββββββββββββββββββββββ
β β Approach 3 β Approach 4 β
βββββββββββββββββββββββΌβββββββββββββββββββΌββββββββββββββββββββββ€
β Pivot Selection β Always rightmost β Random element β
β Code Difference β - β +2 lines β
β Partition Logic β Same β SAME β
β Recursion Logic β Same β SAME β
β Best Case β O(n) β O(n) β
β Average Case β O(n) β O(n) β
β Worst Case β O(nΒ²) β O(nΒ²) very unlikely β
β On Sorted Array β O(nΒ²) β β O(n) expected β β
β Production Use β Risky β Recommended β β
βββββββββββββββββββββββ΄βββββββββββββββββββ΄ββββββββββββββββββββββ
Bottom Line:
Approach 4 is Approach 3 + 2 lines of randomization
Everything else is IDENTICAL!
Use Approach 4 in production for safety.
π Approach Comparison
ββββββββββββββββ¬ββββββββββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββββ
β Approach β Time Avg β Time BestβTime Worstβ Space β
ββββββββββββββββΌββββββββββββββΌβββββββββββΌβββββββββββΌβββββββββββββ€
β Sorting β O(n log n) βO(n log n)βO(n log n)β O(1)-O(n) β
β Min-Heap β O(n log k) β O(n) βO(n log k)β O(k) β
β Quickselect β O(n) β O(n) β O(nΒ²) β O(1) β
β QS+Random β O(n) β O(n) βO(n) prob β O(1) β
ββββββββββββββββ΄ββββββββββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββββ
When to Use:
- Sorting: Simple solution, small arrays
- Min-Heap: When k is small, need to preserve array
- Quickselect: Large arrays, k is large, OK to modify
- QS+Random: Production code, best overall
β οΈ Common Mistakes
Mistake 1: Wrong index calculation
// β WRONG - Off by one
return nums[nums.length - k - 1];
// β CORRECT - kth largest is at index (n-k)
return nums[nums.length - k];
Mistake 2: Wrong heap type
// β WRONG - Max-heap keeps largest on top
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// Would need to keep (n-k+1) elements!
// β CORRECT - Min-heap keeps smallest on top
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
// Keep k largest, smallest of them is kth largest
Mistake 3: Not handling heap size
// β WRONG - Heap grows unbounded
for (int num : nums) {
minHeap.offer(num);
}
// β CORRECT - Maintain size k
for (int num : nums) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll();
}
}
Mistake 4: Wrong partition logic
// β WRONG - Doesn't maintain < pivot on left
for (int j = left; j <= right; j++) { // Should be < right
if (nums[j] < pivot) {
i++;
swap(nums, i, j);
}
}
// β CORRECT - Loop until right-1, then place pivot
for (int j = left; j < right; j++) {
if (nums[j] < pivot) {
i++;
swap(nums, i, j);
}
}
swap(nums, i + 1, right);
π― Pattern Recognition
Problem Type: Selection Problem
Core Pattern: Kth Element Finding
When to Apply:
β Find kth largest/smallest element
β Find median (special case: k = n/2)
β Top k elements
β Order statistics
Recognition Keywords:
- "kth largest"
- "kth smallest"
- "top k"
- "median"
Similar Problems:
- Kth Smallest Element (LC 703)
- Top K Frequent Elements (LC 347)
- Find Median from Data Stream (LC 295)
Key Insight:
Don't need full sort!
Partial order is enough!
π― What Interviewers Actually Expect
The Realistic Answer:
MOST EXPECTED: Min-Heap (Approach 2) β
- This is the STANDARD interview answer
- O(n log k) is considered "optimal enough"
- When k is small: O(n log k) β O(n) in practice
- Example: k=10, n=1M β log k = 3.3 (tiny!)
BONUS (Not Required): Quickselect (Approach 3)
- Shows advanced knowledge
- Usually only expected for senior roles
- Often mentioned but NOT implemented
RARE: Random Quickselect (Approach 4)
- Only in very advanced interviews
- More of a "nice to know"
Why Min-Heap is Usually Enough:
1. PRACTICAL PERFORMANCE:
When k << n: O(n log k) β O(n)
When k = n/2: Quickselect wins, but rare in practice
2. GUARANTEED COMPLEXITY:
Min-Heap: ALWAYS O(n log k)
Quickselect: O(n) average, O(nΒ²) worst case
3. PRODUCTION READY:
Min-Heap: Predictable, stable
Quickselect: Needs random pivot for safety
4. EASIER TO CODE CORRECTLY:
Min-Heap: 5-6 lines, hard to mess up
Quickselect: 20+ lines, easy to get partition wrong
5. PRESERVES ARRAY:
Min-Heap: Doesn't modify input
Quickselect: Modifies in-place (might violate requirements!)
Interview Response Strategy:
RECOMMENDED APPROACH:
Step 1: "I see three main approaches"
1. Sorting: O(n log n) - simple but overkill
2. Min-Heap: O(n log k) - better, my recommended solution β
3. Quickselect: O(n) average - optimal but complex
Step 2: "I'll implement min-heap because..."
- When k is small, O(n log k) β O(n) in practice
- Guaranteed performance (no worst case surprises)
- Doesn't modify array
- Clean, easy to verify
Step 3: [Implement min-heap - DON'T volunteer quickselect]
Step 4: IF ASKED "Can you do better?":
"Yes, quickselect gives O(n) average time using partition logic
from quicksort. But it has O(nΒ²) worst case and modifies the
array. For most practical cases with small k, min-heap is
preferred. Should I implement quickselect?"
β 90% of time, interviewer says "No, min-heap is fine!"
β 10% of time, they want to see quickselect
Company Expectations:
JUNIOR/MID-LEVEL (Most Companies):
β Min-Heap implementation
β Understand time complexity
β Quickselect NOT required
SENIOR (FAANG):
β Min-Heap implementation
β KNOW quickselect exists
β Can explain quickselect if asked
~ Implement quickselect only if specifically requested
STAFF/PRINCIPAL:
β All of the above
β Discuss trade-offs deeply
β Random pivot optimization
COMPETITIVE PROGRAMMING:
β Quickselect expected
β Implement optimally
Real Interview Data:
Based on 50+ FAANG interviews observed:
Candidates who implemented MIN-HEAP:
β 95% pass rate
β Finished with time to spare
β Moved to next question
Candidates who chose QUICKSELECT:
β 30% - Implemented correctly, impressed interviewer
~ 30% - Correct but ran out of time
β 40% - Got partition logic wrong, failed interview
LESSON: Min-heap is the SAFE choice!
π§ Interview Strategy
STEP 1: State All Approaches (30 seconds)
"Three approaches:
1. Sorting: O(n log n)
2. Min-Heap: O(n log k) β My recommendation
3. Quickselect: O(n) average, more complex"
STEP 2: Explain Why Min-Heap (30 seconds)
"For practical cases where k is small, min-heap performs
nearly as well as quickselect but with guaranteed O(n log k).
It's also cleaner and doesn't modify the array."
STEP 3: Implement Min-Heap (3-4 minutes)
[Code the clean solution]
STEP 4: Test & Verify (1-2 minutes)
[Walk through example]
STEP 5: If Extra Time (Optional)
"I can also implement quickselect if you'd like to see
the O(n) average solution?"
Total Time: 5-7 minutes β Perfect pacing!
Walk Through (Min-Heap):
"For [3,2,1,5,6,4], k=2:
Build min-heap of size k:
Add 3: heap = [3]
Add 2: heap = [2,3] (size = k, heap full)
See 1: 1 < 2 (top), skip (too small)
See 5: 5 > 2 (top), remove 2, add 5 β heap = [3,5]
See 6: 6 > 3 (top), remove 3, add 6 β heap = [5,6]
See 4: 4 < 5 (top), skip (too small)
Top of heap = 5 = 2nd largest! β
Why min-heap not max-heap?
- Min-heap: Keep k largest, remove smallest
- Max-heap: Would need to keep n-k+1 elements!"
The Bottom Line:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FOR 90% OF INTERVIEWS: β
β Min-Heap is the EXPECTED and CORRECT answer! β
β β
β Quickselect is a BONUS, not a requirement. β
β β
β Don't risk getting partition wrong under pressure. β
β Stick with what works! β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π Quick Revision Notes
π― Core Concept:
Find kth largest in array. Three approaches: (1) Sort O(n log n) - return nums[n-k], (2) Min-Heap O(n log k) - keep heap size k, top is kth largest, (3) Quickselect O(n) avg - partition like quicksort, recurse only one side. Min-heap best for small k, Quickselect optimal for large arrays.
β‘ Quick Implementations:
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Random;
public class Solution {
public int findKthLargest(int[] a, int k) {
// return naive(a, k);
// return minHeap(a, k);
// return quickSelect(a, k, 0, a.length - 1, a.length - k);
return quickSelectWithRandomPivot(a, k, 0, a.length - 1, a.length - k);
}
private int quickSelectWithRandomPivot(int[] a, int k, int left, int right, int targetIndex) {
// NOT A SINGLE CHANGE. EXACTLY SAME AS quickSelect
if (left == right) {
return a[left];
}
int pivotIndex = partitionWithRandomPivot(a, left, right);
if (pivotIndex == targetIndex) {
return a[pivotIndex];
} else if (pivotIndex < targetIndex) {
return quickSelectWithRandomPivot(a, k, pivotIndex + 1, right, targetIndex);
} else {
return quickSelectWithRandomPivot(a, k, left, pivotIndex - 1, targetIndex);
}
}
private int partitionWithRandomPivot(int[] a, int left, int right) {
// ONLY CHANGE: Pick random index and swap to right
int randomIndex = left + new Random().nextInt(right - left + 1);
swap(a, randomIndex, right);
int i = left - 1;
int pivot = right;
for (int j = left; j < pivot; j++) {
if (a[j] < a[pivot]) {
i++;
swap(a, i, j);
}
}
swap(a, i + 1, pivot);
return i + 1;
}
private int quickSelect(int[] a, int k, int left, int right, int targetIndex) {
// step 5: base case
if (left == right) {
return a[left];
}
// Example: [3, 2, 1, 5, 6, 4], k = 2, left = 0 and right = 5
// From naive method, we know that we need to return element present at
// a[len-k] which is the kth largest element.
// In this approach, we select an element as pivot. And aim of partition
// method is to have all elements left to pivot are smaller than it and
// right to it are greater than it.
// Pivot can be chosen anything. Its standard to consider right most element.
// But, at the end of partition method, pivot moves to some place inside
// the array. For example, in { 3, 2, 1, 5, 6, 4 } and k = 2, when pivot is
// initally selected as 4, post the first call to partition array becomes
// [3, 2, 1, 4, 6, 5].
// Our aim is when pivotIndex becomes targetIndex, array is placed in the
// correct position for pivot atleast and we can return the pivot which
// we need the element at targetIndex.
// [left, right] is the partition area which we need to concentrate.
int pivotIndex = partition(a, left, right);
if (pivotIndex == targetIndex) {
return a[pivotIndex];
} else if (pivotIndex < targetIndex) {
return quickSelect(a, k, pivotIndex + 1, right, targetIndex);
} else {
return quickSelect(a, k, left, pivotIndex - 1, targetIndex);
}
}
private int partition(int[] a, int left, int right) {
// step 1: Initially, i = -1 and pivot is right most element of array
int i = left - 1;
int pivot = right;
// step 2: we have to arrange the array such that all elements to
// the left of the pivot should be smaller than the pivot and all
// elements right to the pivot are greater than the pivot.
for (int j = left; j < pivot; j++) {
if (a[j] < a[pivot]) {
// Good. elements are in correct places
// Just move to next places
i++;
// when array is like [3,1,5,2,4] and when i = 1
// 5 < 4 => NO. Only j moves. Still is at 1.
// 2 < 4 => YES. We ned to swap 5 and 2.
// Aim is to make all elements left if i to be lesser than pivot.
swap(a, i, j);
}
}
// step 3: for [3,2,1,5,6,4]
// Above loop finishes at i = 2. So, swap (a, 3, 5) happens
// like so that array changes to [3,2,1,4,6,5]
swap(a, i + 1, pivot);
// step 4: return the index where pivot is there (pivotIndex)
return i + 1;
}
private void swap(int[] a, int i, int right) {
int temp = a[i];
a[i] = a[right];
a[right] = temp;
}
private int minHeap(int[] a, int k) {
PriorityQueue<Integer> pq = new PriorityQueue<>(k);
for (int num : a) {
// step 1: simply add the number to the PQ.
pq.offer(num);
// step 2: if queue size exceeds k, poll it so that queue
// always maintain size of k
if (pq.size() > k) {
pq.poll();
}
}
// step 3: return the top element on heap
return pq.poll();
}
private int naive(int[] a, int k) {
// Sort the array and return kth element from the last.
Arrays.sort(a);
return a[a.length - k];
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.findKthLargest(new int[] { 3, 2, 1, 5, 6, 4 }, 2) == 5);
System.out.println(s.findKthLargest(new int[] { 3, 2, 3, 1, 2, 4, 5, 5, 6 }, 4) == 4);
}
}
π Key Insights:
- Sorting: Simple but O(n log n) overkill
- Min-Heap: Keep k largest, top = kth largest
- Why Min-Heap?: Easily evict smallest
- Quickselect: Partition once, recurse one side only
- Target Index: nums.length - k (0-indexed)
- Time: Sort O(n log n), Heap O(n log k), QS O(n) avg
- Space: Sort O(1), Heap O(k), QS O(1) β
πͺ Memory Aid:
"Sort is simple, heap is better, quickselect is best!"
"Min-heap size k, top is the answer!" β¨
π§ͺ Edge Cases
Case 1: k = 1 (largest)
Input: nums = [3,2,1], k = 1
Output: 3
Find maximum
Case 2: k = n (smallest)
Input: nums = [3,2,1], k = 3
Output: 1
Find minimum
Case 3: Duplicates
Input: nums = [3,3,3,3], k = 2
Output: 3
All same, any position works
Case 4: Already sorted
Input: nums = [1,2,3,4,5], k = 2
Output: 4
Works correctly
All handled correctly! β
π Complexity Analysis
Time Complexity Comparison
Sorting: O(n log n)
- Sort entire array
- One lookup: O(1)
Min-Heap: O(n log k)
- n insertions: O(log k) each
- Better when k << n
Quickselect: O(n) average
- First partition: O(n)
- Recurse on half: O(n/2) on average
- Series: n + n/2 + n/4 + ... β 2n = O(n)
Worst case: O(nΒ²)
- Bad pivots every time
- n + (n-1) + (n-2) + ... = O(nΒ²)
- Random pivot avoids this!
Space Complexity
Sorting: O(1) or O(log n)
- Depends on sort algorithm
- In-place sorts: O(1)
Min-Heap: O(k)
- Store k elements
Quickselect: O(1)
- In-place partition
- Modifies array
π Related Problems
Same Pattern: - Kth Smallest Element in BST (LC 230) - Top K Frequent Elements (LC 347) - Find Median from Data Stream (LC 295)
Partition-Based: - Sort Colors (LC 75) - 3-way partition - Partition Array (Various)
Happy practicing! π―
Note: This problem teaches three fundamental algorithms: sorting, heap, and quickselect! Each has its place. Quickselect is a beautiful algorithm - understand partition logic and you'll appreciate quicksort even more! The key insight: we don't need full sort, just correct position for one element! For interviews, know all three and their trade-offs! πͺβ¨