Top K
Top-K pattern — общий приём для задач «найди k лучших»
Что это за задачи
В интервью часто встречается формулировка вида:
«Дан набор объектов и число k. Верните k “лучших” объектов, где “лучше” определено условием задачи (частота появления, наибольший/наименьший вес, максимальный рейтинг и т. д.).»
Классический «топ-K» появляется в задачах про частоты слов, самые большие/маленькие значения, лучшие результаты и т. п.
Наивное решение
- Посчитать нужную метрику для каждого объекта.
- Отсортировать весь набор по этой метрике.
- Взять первые
kэлементов.
Сложность — O(n log n), где n — размер входа.
Улучшение с помощью кучи
Если k ≪ n, сортировать всё невыгодно.
Приём «движущегося мини-куча» сокращает логарифм до log k:
-
Создаём min-heap (или max-heap, если нужно искать
kнаименьших). -
Итерируем вход:
- помещаем объект в кучу;
- как только размер >
k, делаемpop()— удаляется «худший» элемент.
-
После прохода кучу составляют ровно
k«лучших» объектов.
Сложность становится O(n log k) по времени и O(k) по памяти, что выигрывает у сортировки, когд а k заметно меньше n.
Ключевая идея: ограничиваем размер приоритетной очереди
k, чтобы все операции над ней стоилиlog k, а неlog n.
Когда применять
kзначительно меньшеn;- нужно вернуть топ без строгого порядка (если важна сортировка результата, его можно отсортировать в конце — это
k log k, обычно несущественно); - в условии нет удаления произвольных элементов (при удалениях сложнее, лучше multiset/treap).
Такой шаблон лежит в основе решений для задач «Top K Frequent Elements», «K Closest Points to Origin», «Kth Largest Element in an Array» и множества вариаций.
Top K Frequent Elements
Условие задачи
Дан массив целых чисел nums и число k.
Нужно вернуть k элементов, которые встречаются чаще всего.
Гарантируется, что ответ единственен (частоты различаются или наборы не пересекаются).
Ключевая идея
-
Посчитать частоту каждого числа хеш-картой
freq. -
Поддерживать min-кучу размера ≤
k, где ключ — частота, значение — число.- Пока обходим
freq, кладём в кучу пару(frequency, value). - Как только размер стал
k + 1, делаемpop()— уходит элемент с минимальной частотой.
- Пока обходим
-
После обхода в куче останутся ровно
kсамых частотных чисел.
Поскольку размер очереди ограничен k, все операции с ней стоят O(log k), а не O(log n).
Почему не хватает одной только сортировки
Сортировка словаря по убыванию частоты даёт O(n log n).
С кучей получаем O(n log k) (потому что log k < log n), что особенно ощутимо, когда k во много раз меньше n.
Алгоритм
-
freq = Map()→ для каждогоnumувеличиваем счётчик. -
Создать
MinPriorityQueueс приоритетомpair => pair[0](частота). -
Для каждой
[val, f]изfreq:enqueue([f, val])- если
queue.size() > k→dequeue()
-
Извлечь из очереди
kзначений и вернуть их в любом порядке.
Реализация на JavaScript
npm i @datastructures-js/priority-queue
/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function (nums, k) {
const map = new Map();
const minHeap = new MinPriorityQueue((keyVal) => keyVal[1]);
const ans = [];
for (let i = 0; i < nums.length; i++) {
map.set(nums[i], (map.get(nums[i]) || 0) + 1);
}
for (const [key, val] of map) {
minHeap.enqueue([key, val]);
if (minHeap.size() > k) {
minHeap.dequeue();
}
}
while (minHeap.size() > 0) {
ans.push(minHeap.dequeue()[0]);
}
return ans;
};
Анализ сложности
| Этап | Время | Память |
|---|---|---|
| Подсчёт частот | O(n) | O(n) |
Работа c очередью (n вставок) | O(n log k) | O(k) |
| Итого | O(n log k) | O(n + k) |
n = nums.length. Хеш-карта хранит все частоты, очередь — лишь k элементов, что обеспечивает улучшение по времени по сравнению с полной сортировкой.