Repository navigation
Expand file tree
/
Copy pathkthLargest.js
More file actions
301 lines (272 loc) · 8.22 KB
/
Copy pathkthLargest.js
File metadata and controls
301 lines (272 loc) · 8.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/**
* ============================================================================
* KTH LARGEST ELEMENT IN AN ARRAY (QuickSelect + Min-Heap)
* ============================================================================
*
* PROBLEM STATEMENT:
* Given an integer array `nums`, find the k'th largest element.
* (k = 1 means the largest element, k = nums.length means the smallest element)
*
* PATTERN:
* - **QuickSelect** (Selection Algorithm) — average O(n), in-place
* - **Top-K / Heap** — O(n log k), stable performance, no worst-case surprises
*
* WHEN TO USE WHICH:
* - QuickSelect: When you want the fastest average performance and can tolerate
* the fact that it mutates the array (or you can pass a copy).
* - Min-Heap of size k: When you want predictable performance and/or you are
* streaming data and only care about top k.
*
* REAL-WORLD ANALOGIES:
* - HR / Recruiting: "Find the 5th highest interview score from applicants."
* - Monitoring: "Find the 3rd largest CPU spike value in last 24h samples."
* - Sales Ops: "Find the 10th largest deal amount closed this quarter."
*/
// ============================================================================
// Input validation helper
// ============================================================================
/**
* @param {number[]} nums
* @param {number} k
*/
function validateKthInputs(nums, k) {
if (!Array.isArray(nums)) {
throw new TypeError("nums must be an array of numbers");
}
if (!Number.isInteger(k)) {
throw new TypeError("k must be an integer");
}
if (k < 1 || k > nums.length) {
throw new RangeError("k must be between 1 and nums.length");
}
}
// ============================================================================
// Approach 1: QuickSelect (average O(n), in-place)
// ============================================================================
/**
* Find the k'th largest element using QuickSelect (in-place).
*
* Key idea:
* - The k'th largest is the \((n - k)\)'th element in sorted (ascending) order.
* - QuickSelect partitions the array like QuickSort, but only recurses/iterates
* into the side that contains the desired index.
*
* Why this is "pattern-worthy":
* - It's a **selection** problem (kth order statistic), not a full sorting
* problem. Sorting is extra work.
*
* @example
* // Real-world: Hiring pipeline ranking
* // You want the 3rd highest interview score to set a cutoff.
* const scores = [72, 88, 91, 65, 88, 79];
* findKthLargestQuickSelect(scores, 3);
* // Returns: 88
* // Interpretation: Only two scores are higher than 88 (91 and 88/88 tie handling
* // is natural because we treat duplicates as separate items).
*
* @example
* // Real-world: Monitoring peaks
* // Find the 2nd largest spike value.
* findKthLargestQuickSelect([7, 4, 6, 3, 9, 1], 2);
* // Returns: 7
*
* @param {number[]} nums - Input array (will be mutated)
* @param {number} k - 1-based rank (1 = largest)
* @returns {number} The k'th largest value
*
* Time Complexity:
* - Average: O(n)
* - Worst-case: O(n²) (rare; mitigated by randomized pivot)
*
* Space Complexity: O(1) extra space (in-place)
*/
export function findKthLargestQuickSelect(nums, k) {
validateKthInputs(nums, k);
const targetIndex = nums.length - k; // kth largest == (n-k)th smallest
let left = 0;
let right = nums.length - 1;
while (left <= right) {
const pivotIndex = partitionWithRandomPivot(nums, left, right);
if (pivotIndex === targetIndex) return nums[pivotIndex];
if (pivotIndex < targetIndex) left = pivotIndex + 1;
else right = pivotIndex - 1;
}
// Should never be reached if inputs are valid.
throw new Error("Unexpected state in QuickSelect");
}
/**
* Partition `arr[left..right]` around a randomized pivot.
* After partitioning:
* - all elements < pivotValue are to the left of the pivot
* - all elements >= pivotValue are to the right
*
* @param {number[]} arr
* @param {number} left
* @param {number} right
* @returns {number} Final pivot index
*/
function partitionWithRandomPivot(arr, left, right) {
const pivotIndex = left + Math.floor(Math.random() * (right - left + 1));
swap(arr, pivotIndex, right); // move pivot to end
return lomutoPartition(arr, left, right);
}
/**
* Lomuto partition scheme using `arr[right]` as pivot.
* @param {number[]} arr
* @param {number} left
* @param {number} right
* @returns {number}
*/
function lomutoPartition(arr, left, right) {
const pivotValue = arr[right];
let storeIndex = left;
for (let i = left; i < right; i++) {
if (arr[i] < pivotValue) {
swap(arr, i, storeIndex);
storeIndex++;
}
}
swap(arr, storeIndex, right);
return storeIndex;
}
/**
* @param {any[]} arr
* @param {number} i
* @param {number} j
*/
function swap(arr, i, j) {
if (i === j) return;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
// ============================================================================
// Approach 2: Min-Heap of size k (O(n log k), does not mutate input)
// ============================================================================
/**
* Find the k'th largest element using a Min-Heap of size k.
*
* Mental model:
* - Keep the **top k largest** elements seen so far.
* - The smallest among those top k is exactly the k'th largest overall.
*
* @example
* // Real-world: Top-k deals q
* // Keep the 5 biggest deals. The smallest among them is your 5th biggest deal.
* findKthLargestMinHeap([120, 80, 300, 200, 150], 2);
* // Returns: 200 (2nd largest)
*
* @param {number[]} nums
* @param {number} k
* @returns {number}
*
* Time Complexity: O(n log k)
* Space Complexity: O(k)
*/
export function findKthLargestMinHeap(nums, k) {
validateKthInputs(nums, k);
const heap = new MinHeap();
for (const value of nums) {
if (heap.size() < k) {
heap.push(value);
continue;
}
// If current value is bigger than the smallest in top-k,
// it deserves a spot in the heap.
if (value > heap.peek()) {
heap.replaceTop(value);
}
}
return heap.peek();
}
/**
* A minimal Min-Heap implementation for numbers.
* (Perfect for top-k problems.)
*/
export class MinHeap {
constructor() {
/** @type {number[]} */
this.data = [];
}
/** @returns {number} */
size() {
return this.data.length;
}
/** @returns {number} */
peek() {
if (this.data.length === 0) {
throw new Error("Heap is empty");
}
return this.data[0];
}
/** @param {number} value */
push(value) {
this.data.push(value);
this.bubbleUp(this.data.length - 1);
}
/** @returns {number} */
pop() {
if (this.data.length === 0) {
throw new Error("Heap is empty");
}
const top = this.data[0];
const last = this.data.pop();
if (this.data.length > 0 && last !== undefined) {
this.data[0] = last;
this.bubbleDown(0);
}
return top;
}
/**
* Replace the heap root with `value` (faster than pop+push).
* @param {number} value
*/
replaceTop(value) {
if (this.data.length === 0) {
this.data.push(value);
return;
}
this.data[0] = value;
this.bubbleDown(0);
}
/** @param {number} index */
bubbleUp(index) {
let i = index;
while (i > 0) {
const parent = Math.floor((i - 1) / 2);
if (this.data[parent] <= this.data[i]) break;
swap(this.data, parent, i);
i = parent;
}
}
/** @param {number} index */
bubbleDown(index) {
let i = index;
const n = this.data.length;
while (true) {
const left = 2 * i + 1;
const right = 2 * i + 2;
let smallest = i;
if (left < n && this.data[left] < this.data[smallest]) {
smallest = left;
}
if (right < n && this.data[right] < this.data[smallest]) {
smallest = right;
}
if (smallest === i) break;
swap(this.data, i, smallest);
i = smallest;
}
}
}
// ============================================================================
// Convenience export (default strategy)
// ============================================================================
/**
* Default helper: uses QuickSelect (fast average, in-place).
*
* @param {number[]} nums
* @param {number} k
* @returns {number}
*/
export function findKthLargest(nums, k) {
return findKthLargestQuickSelect(nums, k);
}