Finding Top K Elements
1. Understanding Heap-based Selection
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Min-Heap | Use a min-heap of size k to keep track of the top k elements. For each new element, if it's larger than the smallest in the heap, replace it. | O(n log k) |
O(k) |
| Max-Heap | Use a max-heap to store all elements. Extract the top k elements by popping from the heap. | O(n log n) |
O(n) |
Example: Using Min-Heap to Find Top K Elements
const findKthLargest = (nums, k) => {
const minHeap = new MinHeap();
for (let num of nums) {
if (minHeap.size() < k) {
minHeap.push(num);
} else if (num > minHeap.top()) {
minHeap.pop();
minHeap.push(num);
}
}
return minHeap.top();
};
class MinHeap {
constructor() {
this.heap = [];
}
push(val) {
this.heap.push(val);
this.bubbleUp(this.heap.length - 1);
}
pop() {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop();
const min = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return min;
}
top() {
return this.heap[0];
}
size() {
return this.heap.length;
}
bubbleUp(index) {
if (index === 0) return;
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex] > this.heap[index]) {
[this.heap[parentIndex], this.heap[index]] = [this.heap[index], this.heap[parentIndex]];
this.bubbleUp(parentIndex);
}
}
bubbleDown(index) {
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
let minIndex = index;
if (leftChild < this.heap.length && this.heap[leftChild] < this.heap[minIndex]) {
minIndex = leftChild;
}
if (rightChild < this.heap.length && this.heap[rightChild] < this.heap[minIndex]) {
minIndex = rightChild;
}
if (minIndex !== index) {
[this.heap[index], this.heap[minIndex]] = [this.heap[minIndex], this.heap[index]];
this.bubbleDown(minIndex);
}
}
}
Note: The min-heap approach is preferred for top-k problems when k < n, as it avoids sorting all elements.
2. Finding K Largest Elements
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Sorting | Sort the array in descending order and pick first K elements. | O(n log n) | O(1) |
| Min-Heap | Use a min-heap of size K to keep track of the largest K elements. | O(n log k) | O(k) |
| QuickSelect | Use the QuickSelect algorithm to find the Kth largest element and partition around it. | O(n) average, O(n²) worst | O(1) |
Example: Using Min-Heap
const findKLargest = (nums, k) => {
const minHeap = new MinHeap();
for (let num of nums) {
if (minHeap.size() < k) {
minHeap.push(num);
} else if (num > minHeap.top()) {
minHeap.pop();
minHeap.push(num);
}
}
return minHeap.toArray();
};
class MinHeap {
constructor() { this.heap = []; }
push(val) { /* implementation */ }
pop() { /* implementation */ }
top() { return this.heap[0]; }
size() { return this.heap.length; }
toArray() { return [...this.heap]; }
}
Example: Using QuickSelect
const findKLargest = (nums, k) => {
const kthLargestIndex = nums.length - k;
quickSelect(nums, 0, nums.length - 1, kthLargestIndex);
return nums.slice(kthLargestIndex).sort((a, b) => b - a);
};
const quickSelect = (nums, left, right, k) => {
if (left === right) return;
const pivotIndex = partition(nums, left, right);
if (k === pivotIndex) return;
else if (k < pivotIndex) quickSelect(nums, left, pivotIndex - 1, k);
else quickSelect(nums, pivotIndex + 1, right, k);
};
const partition = (nums, left, right) => {
const pivot = nums[right];
let i = left;
for (let j = left; j < right; j++) {
if (nums[j] < pivot) {
[nums[i], nums[j]] = [nums[j], nums[i]];
i++;
}
}
[nums[i], nums[right]] = [nums[right], nums[i]];
return i;
};
Note: The QuickSelect approach is efficient for large datasets when K is small relative to N.
3. Finding Kth Largest Element
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Sorting | Sort the array in descending order and return the element at index k-1. | O(n log n) |
O(1) |
| Min-Heap | Use a min-heap of size k to keep track of the k largest elements. The root of the heap is the kth largest element. | O(n log k) |
O(k) |
| Quickselect | Use the quickselect algorithm to find the kth largest element in average O(n) time. | O(n) average |
O(1) |
Example: Using Min-Heap
const findKthLargest = (nums, k) => {
const minHeap = new MinHeap();
for (let num of nums) {
if (minHeap.size() < k) {
minHeap.push(num);
} else if (num > minHeap.top()) {
minHeap.pop();
minHeap.push(num);
}
}
return minHeap.top();
};
class MinHeap {
constructor() {
this.heap = [];
}
push(val) {
this.heap.push(val);
this.bubbleUp(this.heap.length - 1);
}
pop() {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop();
const min = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return min;
}
top() {
return this.heap[0];
}
size() {
return this.heap.length;
}
bubbleUp(index) {
if (index === 0) return;
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex] > this.heap[index]) {
[this.heap[parentIndex], this.heap[index]] = [this.heap[index], this.heap[parentIndex]];
this.bubbleUp(parentIndex);
}
}
bubbleDown(index) {
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
let minIndex = index;
if (leftChild < this.heap.length && this.heap[leftChild] < this.heap[minIndex]) {
minIndex = leftChild;
}
if (rightChild < this.heap.length && this.heap[rightChild] < this.heap[minIndex]) {
minIndex = rightChild;
}
if (minIndex !== index) {
[this.heap[index], this.heap[minIndex]] = [this.heap[minIndex], this.heap[index]];
this.bubbleDown(minIndex);
}
}
}
Example: Using Quickselect
const findKthLargest = (nums, k) => {
return quickSelect(nums, 0, nums.length - 1, k);
};
const quickSelect = (nums, left, right, k) => {
if (left === right) return nums[left];
const pivotIndex = partition(nums, left, right);
if (k === pivotIndex) {
return nums[k];
} else if (k < pivotIndex) {
return quickSelect(nums, left, pivotIndex - 1, k);
} else {
return quickSelect(nums, pivotIndex + 1, right, k);
}
};
const partition = (nums, left, right) => {
const pivot = nums[right];
let i = left;
for (let j = left; j < right; j++) {
if (nums[j] > pivot) {
[nums[i], nums[j]] = [nums[j], nums[i]];
i++;
}
}
[nums[i], nums[right]] = [nums[right], nums[i]];
return i;
};
4. Finding K Closest Points
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Max Heap | Use a max heap of size k to keep track of the k closest points. For each point, calculate distance and maintain heap. | O(n log k) |
O(k) |
| Quickselect | Use quickselect algorithm to find the kth closest point, then return all points up to that threshold. | O(n) average |
O(1) |
Example: Max Heap Implementation
function kClosest(points, k) {
const maxHeap = new MaxHeap();
for (const point of points) {
const dist = point[0] * point[0] + point[1] * point[1];
if (maxHeap.size() < k) {
maxHeap.push({ point, dist });
} else if (dist < maxHeap.peek().dist) {
maxHeap.pop();
maxHeap.push({ point, dist });
}
}
return maxHeap.toArray().map(item => item.point);
}
Example: Quickselect Implementation
function kClosest(points, k) {
const distances = points.map(p => ({
point: p,
dist: p[0] * p[0] + p[1] * p[1]
}));
quickSelect(distances, 0, distances.length - 1, k - 1);
return distances.slice(0, k).map(item => item.point);
}
function quickSelect(arr, left, right, k) {
if (left === right) return;
const pivotIndex = partition(arr, left, right);
if (pivotIndex === k) return;
else if (pivotIndex > k) quickSelect(arr, left, pivotIndex - 1, k);
else quickSelect(arr, pivotIndex + 1, right, k);
}
Note: The max heap approach is more intuitive and easier to understand, while quickselect offers better average-case performance for large datasets.
5. Finding Top K Frequent Elements
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| HashMap + Min-Heap | Count frequencies using HashMap, maintain a min-heap of size k to track top k elements | O(n log k) | O(n + k) |
| QuickSelect | Use partitioning to find elements with frequencies in top k range | O(n) average | O(n) |
| Sorting | Sort elements by frequency in descending order, take first k | O(n log n) | O(n) |
Example: Using HashMap + Min-Heap
const topKFrequent = (nums, k) => {
const freqMap = new Map();
for (const num of nums) {
freqMap.set(num, (freqMap.get(num) || 0) + 1);
}
const minHeap = new MinHeap();
for (const [num, freq] of freqMap) {
if (minHeap.size() < k) {
minHeap.push({ num, freq });
} else if (freq > minHeap.peek().freq) {
minHeap.pop();
minHeap.push({ num, freq });
}
}
return minHeap.toArray().map(item => item.num);
};
Example: Using QuickSelect
const topKFrequent = (nums, k) => {
const freqMap = new Map();
for (const num of nums) {
freqMap.set(num, (freqMap.get(num) || 0) + 1);
}
const freqArray = Array.from(freqMap.entries()).map(([num, freq]) => ({ num, freq }));
quickSelect(freqArray, 0, freqArray.length - 1, k);
return freqArray.slice(0, k).map(item => item.num);
};
const quickSelect = (arr, left, right, k) => {
if (left < right) {
const pivotIndex = partition(arr, left, right);
if (pivotIndex === k - 1) return;
else if (pivotIndex < k - 1) {
quickSelect(arr, pivotIndex + 1, right, k);
} else {
quickSelect(arr, left, pivotIndex - 1, k);
}
}
};
const partition = (arr, left, right) => {
const pivot = arr[right].freq;
let i = left - 1;
for (let j = left; j < right; j++) {
if (arr[j].freq > pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
return i + 1;
};
Note: The QuickSelect approach has O(n) average time complexity but can be O(n^2) in worst case. The heap-based approach is more predictable and generally preferred for interviews.
6. Sorting Array by Frequency
| Method | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| HashMap + Sorting | Count frequencies using a hash map, then sort keys by frequency. | O(n log n) |
O(n) |
| Heap (Min-Heap) | Use a min-heap of size k to keep top k frequent elements. | O(n log k) |
O(n) |
| Bucket Sort | Group elements by frequency in buckets, then collect from highest to lowest. | O(n) |
O(n) |
Example: Using HashMap + Sorting
const nums = [1, 1, 2, 2, 3];
const k = 2;
const freqMap = new Map();
for (let num of nums) {
freqMap.set(num, (freqMap.get(num) || 0) + 1);
}
const sorted = [...freqMap.entries()].sort((a, b) => b[1] - a[1]);
const result = sorted.slice(0, k).map(pair => pair[0]);
console.log(result); // [1, 2]
Example: Using Min-Heap
const nums = [1, 1, 2, 2, 3];
const k = 2;
const freqMap = new Map();
for (let num of nums) {
freqMap.set(num, (freqMap.get(num) || 0) + 1);
}
class MinHeap {
constructor() {
this.heap = [];
}
push(val) {
this.heap.push(val);
this.bubbleUp(this.heap.length - 1);
}
pop() {
if (this.heap.length === 0) return null;
const min = this.heap[0];
const last = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = last;
this.bubbleDown(0);
}
return min;
}
bubbleUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex][1] <= this.heap[index][1]) break;
[this.heap[parentIndex], this.heap[index]] = [this.heap[index], this.heap[parentIndex]];
index = parentIndex;
}
}
bubbleDown(index) {
const len = this.heap.length;
while (true) {
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
let minIndex = index;
if (leftChild < len && this.heap[leftChild][1] < this.heap[minIndex][1]) {
minIndex = leftChild;
}
if (rightChild < len && this.heap[rightChild][1] < this.heap[minIndex][1]) {
minIndex = rightChild;
}
if (minIndex === index) break;
[this.heap[index], this.heap[minIndex]] = [this.heap[minIndex], this.heap[index]];
index = minIndex;
}
}
size() {
return this.heap.length;
}
}
const minHeap = new MinHeap();
for (let [num, freq] of freqMap.entries()) {
if (minHeap.size() < k) {
minHeap.push([num, freq]);
} else if (freq > minHeap.heap[0][1]) {
minHeap.pop();
minHeap.push([num, freq]);
}
}
const result = [];
while (minHeap.size() > 0) {
result.push(minHeap.pop()[0]);
}
console.log(result); // [1, 2]
7. Finding K Closest Numbers
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Two Pointers | Use two pointers to find the k closest elements from a sorted array to a target value. | O(n) |
O(1) |
| Binary Search + Heap | Use binary search to find the insertion point, then use a max-heap to maintain k closest elements. | O(log n + k log k) |
O(k) |
| Min-Heap | Use a min-heap to keep track of elements based on their distance from the target. | O(n log n) |
O(n) |
Example: Two Pointers Approach
function findClosestElements(arr, k, x) {
let left = 0;
let right = arr.length - 1;
while (right - left >= k) {
if (Math.abs(arr[left] - x) > Math.abs(arr[right] - x)) {
left++;
} else {
right--;
}
}
return arr.slice(left, right + 1);
}
// Example usage:
const arr = [1, 2, 3, 4, 5];
const k = 4;
const x = 3;
console.log(findClosestElements(arr, k, x)); // [1, 2, 3, 4]
Example: Binary Search + Heap Approach
function findClosestElements(arr, k, x) {
let left = 0;
let right = arr.length - 1;
// Binary search to find the insertion point
while (left < right) {
let mid = Math.floor((left + right) / 2);
if (arr[mid] < x) {
left = mid + 1;
} else {
right = mid;
}
}
let start = Math.max(0, left - k);
let end = Math.min(arr.length - 1, left + k);
// Create a min-heap based on distance from x
const heap = new MinHeap();
for (let i = start; i <= end; i++) {
heap.push({ value: arr[i], distance: Math.abs(arr[i] - x) });
}
// Extract k closest elements
const result = [];
for (let i = 0; i < k; i++) {
result.push(heap.pop().value);
}
return result.sort((a, b) => a - b);
}
class MinHeap {
constructor() {
this.heap = [];
}
push(val) {
this.heap.push(val);
this.bubbleUp(this.heap.length - 1);
}
pop() {
if (this.heap.length === 0) return null;
const min = this.heap[0];
const last = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = last;
this.bubbleDown(0);
}
return min;
}
bubbleUp(index) {
if (index === 0) return;
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex].distance > this.heap[index].distance) {
[this.heap[parentIndex], this.heap[index]] = [this.heap[index], this.heap[parentIndex]];
this.bubbleUp(parentIndex);
}
}
bubbleDown(index) {
const leftChild = 2 * index + 1;
const rightChild = 2 * index + 2;
let minIndex = index;
if (leftChild < this.heap.length && this.heap[leftChild].distance < this.heap[minIndex].distance) {
minIndex = leftChild;
}
if (rightChild < this.heap.length && this.heap[rightChild].distance < this.heap[minIndex].distance) {
minIndex = rightChild;
}
if (minIndex !== index) {
[this.heap[index], this.heap[minIndex]] = [this.heap[minIndex], this.heap[index]];
this.bubbleDown(minIndex);
}
}
}
// Example usage:
const arr = [1, 2, 3, 4, 5];
const k = 4;
const x = 3;
console.log(findClosestElements(arr, k, x)); // [1, 2, 3, 4]
Note: The two pointers approach is generally preferred for its simplicity and linear time complexity.
8. Finding Maximum Distinct Elements
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Hash Set | Use a hash set to store unique elements and count distinct values. | O(n) |
O(n) |
| Sorting + Counting | Sort the array and count distinct elements by comparing adjacent elements. | O(n log n) |
O(1) (if sorting in-place) |
| Heap + Hash Map | Use a min-heap to keep track of top k distinct elements and a hash map for frequency. | O(n log k) |
O(k) |
Example: Using Hash Set
const findDistinctElements = (arr) => {
const set = new Set(arr);
return set.size;
};
// Example usage:
const array = [1, 2, 2, 3, 4, 4, 5];
console.log(findDistinctElements(array)); // Output: 5
Example: Using Sorting
const findDistinctElements = (arr) => {
if (arr.length === 0) return 0;
arr.sort((a, b) => a - b);
let count = 1;
for (let i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i - 1]) {
count++;
}
}
return count;
};
// Example usage:
const array = [1, 2, 2, 3, 4, 4, 5];
console.log(findDistinctElements(array)); // Output: 5
Note: The hash set approach is preferred for its simplicity and linear time complexity.
9. Finding Sum of K Elements
| Approach | Description | Time Complexity | Space Complexity |
|---|---|---|---|
| Sorting | Sort the array and take the first K elements. Sum them. | O(n log n) |
O(1) |
| Min-Heap | Use a min-heap of size K to keep track of the largest elements. | O(n log k) |
O(k) |
| QuickSelect | Use QuickSelect to find the Kth largest element, then sum all elements >= it. | O(n) average |
O(1) |
Example: Using Min-Heap
const findSumOfK = (arr, k) => {
const minHeap = new MinHeap();
for (let num of arr) {
if (minHeap.size() < k) {
minHeap.push(num);
} else if (num > minHeap.top()) {
minHeap.pop();
minHeap.push(num);
}
}
let sum = 0;
while (minHeap.size()) {
sum += minHeap.pop();
}
return sum;
};
Example: Using QuickSelect
const findSumOfK = (arr, k) => {
const kthLargest = quickSelect(arr, 0, arr.length - 1, arr.length - k);
let sum = 0;
for (let num of arr) {
if (num >= kthLargest) sum += num;
}
return sum;
};
10. Rearranging String K Distance Apart
Given a string and an integer k, rearrange the string such that the same characters are at least k distance apart. If it's not possible, return an empty string.
| Property | Description | Example |
|---|---|---|
| Input | String and integer k |
"aabbcc", k = 3 |
| Output | Rearranged string or empty string if impossible | "abcabc" |
Example: Rearranging String
function rearrangeStringKDistanceApart(s, k) {
const charCount = {};
for (let char of s) {
charCount[char] = (charCount[char] || 0) + 1;
}
const maxHeap = new MaxHeap();
for (let [char, count] of Object.entries(charCount)) {
maxHeap.push({ char, count });
}
const result = [];
const queue = [];
while (!maxHeap.isEmpty()) {
const current = maxHeap.pop();
result.push(current.char);
queue.push({ char: current.char, count: current.count - 1 });
if (queue.length >= k) {
const front = queue.shift();
if (front.count > 0) {
maxHeap.push({ char: front.char, count: front.count });
}
}
}
return result.length === s.length ? result.join('') : '';
}
class MaxHeap {
constructor() {
this.heap = [];
}
push(val) {
this.heap.push(val);
this.bubbleUp(this.heap.length - 1);
}
pop() {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop();
const max = this.heap[0];
this.heap[0] = this.heap.pop();
this.bubbleDown(0);
return max;
}
isEmpty() {
return this.heap.length === 0;
}
bubbleUp(index) {
if (index === 0) return;
const parentIndex = Math.floor((index - 1) / 2);
if (this.heap[parentIndex].count < this.heap[index].count) {
[this.heap[parentIndex], this.heap[index]] = [this.heap[index], this.heap[parentIndex]];
this.bubbleUp(parentIndex);
}
}
bubbleDown(index) {
const left = 2 * index + 1;
const right = 2 * index + 2;
let maxIndex = index;
if (left < this.heap.length && this.heap[left].count > this.heap[maxIndex].count) {
maxIndex = left;
}
if (right < this.heap.length && this.heap[right].count > this.heap[maxIndex].count) {
maxIndex = right;
}
if (maxIndex !== index) {
[this.heap[index], this.heap[maxIndex]] = [this.heap[maxIndex], this.heap[index]];
this.bubbleDown(maxIndex);
}
}
}
// Example usage:
console.log(rearrangeStringKDistanceApart("aabbcc", 3)); // "abcabc"
console.log(rearrangeStringKDistanceApart("aaabc", 3)); // ""
Note: If
k = 0, any arrangement is valid. If k >= s.length, it's impossible unless all characters are the same.
Key Points
- Use a max heap to always select the most frequent character.
- Use a queue to keep track of characters that are too recent to be reused.
- If the queue size exceeds
k, it's safe to reuse a character. - Return empty string if rearrangement is impossible.
11. Scheduling Tasks with Constraints
| Task Property | Description | Constraints |
|---|---|---|
| task.id | Unique identifier for the task | Must be unique within the schedule |
| task.duration | Time required to complete the task | Non-negative integer |
| task.dependencies | List of tasks that must complete before this one starts | Array of task IDs or empty array |
| task.resources | Resources required to execute the task | Array of resource names or empty array |
| task.deadline | Latest time the task can finish | ISO 8601 timestamp or null |
Example: Basic Task Scheduling
const tasks = [
{
id: "task1",
duration: 2,
dependencies: [],
resources: ["cpu"],
deadline: "2026-04-15T18:00:00Z"
},
{
id: "task2",
duration: 3,
dependencies: ["task1"],
resources: ["cpu", "memory"],
deadline: "2026-04-15T20:00:00Z"
}
];
function scheduleTasks(tasks) {
const schedule = [];
const readyTasks = tasks.filter(task => task.dependencies.length === 0);
while (readyTasks.length > 0) {
const task = readyTasks.shift();
schedule.push(task);
// Update dependencies for other tasks
tasks.forEach(t => {
const index = t.dependencies.indexOf(task.id);
if (index !== -1) {
t.dependencies.splice(index, 1);
if (t.dependencies.length === 0) {
readyTasks.push(t);
}
}
});
}
return schedule;
}
const scheduled = scheduleTasks(tasks);
console.log(scheduled);
Note: This example uses a simple topological sort to schedule tasks with dependencies. For more complex constraints like resource availability or time windows, additional algorithms such as constraint satisfaction or integer programming may be required.
12. Finding Frequency Sort
| Property | Description | Usage |
|---|---|---|
| frequencyMap | Map storing element frequencies | const freq = new Map(); |
| topKElements | Result array of top K elements by frequency | const result = []; |
Example: Frequency Sort
const nums = [1, 1, 2, 2, 3];
const k = 2;
// Step 1: Build frequency map
const freq = new Map();
for (let num of nums) {
freq.set(num, (freq.get(num) || 0) + 1);
}
// Step 2: Sort by frequency descending
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
// Step 3: Take top K elements
const result = sorted.slice(0, k).map(pair => pair[0]);
console.log(result); // [1, 2]
Note: This approach has O(n log n) time complexity due to sorting.