Top K Frequent Elements
Дан массив целых чисел nums и целое число k. Верните k значений, которые чаще всего встречаются в nums, расположив сначала наиболее частые. Если два значения встречаются одинаковое число раз, первым идет меньшее значение.
Каждое значение появляется в ответе один раз, независимо от того, сколько раз оно встречается в nums, а k никогда не превышает количество различных значений.
Функция
- numsinteger-array
- значения для подсчёта
- kinteger
- сколько значений нужно вернуть
- Возвращаетinteger-array
- k наиболее часто встречающихся значений, начиная с наиболее частого; при равной частоте сначала указывается меньшее значение
Ограничения
1 ≤ nums.length ≤ 104-104 ≤ nums[i] ≤ 1041 ≤ k, иkне превышает количество различных значений вnums.
Примеры
- Ввод
- nums = [4, 1, 4, 2, 1, 4, 3, 1, 4]k = 2
- Вывод
- [4, 1]
- Пояснение
4встречается четыре раза,1— три раза, а2и3— по одному разу. Два наиболее часто встречающихся значения —4, затем1.
- Ввод
- nums = [5, -2, 7, -2, 7, 5, 9]k = 2
- Вывод
- [-2, 5]
- Пояснение
-2,5и7встречаются по два раза, а9— один раз. Три значения делят первое место, поэтому ответ — два меньших значения:-2и5.
- Ввод
- nums = [8]k = 1
- Вывод
- [8]
- Пояснение
- Есть одно значение, поэтому оно встречается чаще всего.
+16 скрытых тестов при отправке
Подсказки
Открывайте по одной. Каждая подсказывает чуть больше.
Начни с выяснения того, как часто встречается каждое значение. Какая структура данных за один проход сопоставляет значениям их количество?
Имея подсчёты, нужно выбрать
kлучших значений по одному правилу сортировки: сначала значения с бо́льшим количеством, а при равенстве — меньшее значение. Можно отсортировать все различные значения. Мини-куча размераkхранит только те значения, которые ещё могут попасть в ответ.Количество — это целое число от 1 до
n. Создай отдельную корзину для каждого количества: в корзинуcпомести значения, встречающиеся ровноcраз, а затем просматривай корзины от наибольшего количества к наименьшему. Заполняй корзины, проходя по значениям от наименьшего к наибольшему: благодаря этому внутри каждой корзины значения уже будут расположены в нужном порядке при равной частоте.
Решение
Подсчёт — это простая часть: одного прохода с хеш-таблицей достаточно, чтобы получить частоту каждого значения. Настоящий вопрос — как выбрать k лучших значений, не выполняя лишнюю работу. Сортировка всех d различных значений по частоте требует O(d log d), минимальная куча размера k снижает эту сложность до O(d log k), а поскольку частота — целое число от 1 до n, сортировка подсчётом упорядочивает значения по частоте вообще без сравнений.
Посчитайте, затем отсортируйте по количеству
Идея
Сначала подсчитайте. Один проход с хеш-таблицей, в которой каждому значению соответствует его количество, превращает [4, 1, 4, 2, 1, 4, 3, 1, 4] в 4 → 4, 1 → 3, 2 → 1, 3 → 1.
Затем расположите различные значения в порядке ответа: сначала значение с наибольшим количеством, а при равном количестве — меньшее значение. Задайте сортировке именно такое сравнение: количество — первый ключ, значение — второй, и первые k элементов отсортированного списка будут ответом. Здесь порядок — 4, 1, 2, 3, а k = 2 оставляет 4 и 1.
Подсчёт требует O(n). Сортировка d различных значений требует O(d log d), не более O(n log n), если все значения различны: для 10^4 значений потребуется около 1.3 × 10^5 сравнений, что быстро. Недостаток в том, что сортировка упорядочивает все значения, хотя важны только первые k.
Алгоритм
- Подсчитайте количество каждого значения в хеш-таблице.
- Поместите уникальные значения в список.
- Отсортируйте список по количеству от большего к меньшему, а при равном количестве — по значению от меньшего к большему.
- Верните первые
kзначений.
from collections import Counter
def topKFrequent(nums, k):
counts = Counter(nums)
# Most frequent first; equal counts put the smaller value first.
ordered = sorted(counts, key=lambda value: (-counts[value], value))
return ordered[:k]Храните k лучших элементов в минимальной куче
Идея
Тебе нужны только k лучших значений, поэтому храни только k кандидатов. Для каждого нового значения нужно проверить, превосходит ли оно самого слабого из хранимых кандидатов: слабее означает меньшее количество или то же количество и большее значение. Мини-куча, упорядоченная по этому правилу, держит слабейшего кандидата наверху: его можно прочитать за O(1) и заменить за O(log k).
Пройди по различным значениям. Пока в куче меньше k элементов, добавляй значение. После этого значение, которое превосходит верхний элемент, заменяет его, а значение, которое его не превосходит, отбрасывается, потому что k лучших значений уже сохранены. С библиотечной кучей короче добавлять каждое значение и извлекать элемент каждый раз, когда размер кучи превышает k: так сохраняются те же k значений.
В конце в куче будет ответ, но не в порядке ответа: куча отсортирована лишь частично. Извлечение возвращает сначала самое слабое значение, поэтому записывай ответ с последней позиции к первой.
Для каждого из d различных значений выполняется не более одной операции с кучей из k элементов, поэтому на выбор уходит O(d log k). Это быстрее сортировки, когда k намного меньше d, например при поиске 10 лучших среди 8000 различных значений.
Алгоритм
- Подсчитайте количество каждого значения в хеш-таблице.
- Для каждого уникального значения добавляйте его в кучу, пока в ней меньше
kзначений. - Когда куча заполнится, сравните значение с верхним элементом — самым слабым из сохранённых. Если новое значение сильнее, поместите его наверх и просейте вниз.
- Извлеките из кучи
kзначений, записывая каждое в ответ с последней позиции до первой.
import heapq
from collections import Counter
def topKFrequent(nums, k):
counts = Counter(nums)
# Entries are (count, -value). heapq keeps the smallest entry on top, which is
# the weakest value kept: the lowest count, and on a tie the larger value.
heap = []
for value, count in counts.items():
heapq.heappush(heap, (count, -value))
if len(heap) > k:
heapq.heappop(heap)
# Pops come out weakest first, so fill the answer from the back.
result = [0] * k
for i in range(k - 1, -1, -1):
result[i] = -heapq.heappop(heap)[1]
return resultПодсчитайте, затем выполните сортировку по корзинам на основе количества
Идея
Частота — это не любое число: это целое число от 1 до n. Это позволяет использовать сортировку по корзинам. Создайте по одной корзине для каждой частоты: в корзину c поместите значения, встречающиеся ровно c раз, а затем просматривайте корзины от корзины n вниз. Значения будут выдаваться в порядке убывания частоты, и сравнивать частоты между собой не придётся.
Правило разрешения равенства требует ещё одного условия: внутри корзины сначала должно идти меньшее значение. Значения находятся в диапазоне от -10^4 до 10^4, поэтому для подсчёта можно использовать массив из R = 2 × 10^4 + 1 счётчиков, где значение v находится по индексу v + 10^4. Пройдите по этому массиву от наименьшего значения к наибольшему и добавляйте каждое значение в корзину, соответствующую его частоте. Корзины заполняются в порядке возрастания значений — именно так, как требует правило разрешения равенства, поэтому сортировка не нужна.
Для [5, -2, 7, -2, 7, 5, 9] при проходе значения -2, 5, 7 попадут в корзину 2 именно в таком порядке, а 9 — в корзину 1. При просмотре корзин от корзины 7 первая непустая корзина — корзина 2, и k = 2 выбирает -2 и 5.
Работа алгоритма состоит из одного прохода по nums, одного прохода по R счётчикам и одного прохода по корзинам, то есть всего O(n + R): при фиксированном диапазоне значений сложность линейная. Если вместо массива счётчиков использовать хеш-таблицу, подсчёт тоже останется линейным, но корзины будут заполняться в порядке обхода хеш-таблицы, и для соблюдения правила разрешения равенства каждую из них придётся сортировать.
Алгоритм
- Подсчитайте каждое значение в массиве, индексированном по
value + 10^4. - Создайте корзины с 1 по
n, по одному списку для каждого возможного количества. - Просмотрите массив подсчётов от наименьшего значения к наибольшему и добавьте каждое встречающееся значение в корзину, соответствующую его количеству.
- Просматривайте корзины от количества
nдо 1, беря значения, пока не наберётеk.
def topKFrequent(nums, k):
OFFSET = 10000 # values run from -10^4 to 10^4
counts = [0] * (2 * OFFSET + 1)
for x in nums:
counts[x + OFFSET] += 1
# buckets[c] lists the values that occur exactly c times. Walking the
# values from smallest to largest fills every bucket in ascending order.
buckets = [[] for _ in range(len(nums) + 1)]
for i, c in enumerate(counts):
if c > 0:
buckets[c].append(i - OFFSET)
# Read the buckets from the highest count down until k values are taken.
result = []
for c in range(len(nums), 0, -1):
for value in buckets[c]:
result.append(value)
if len(result) == k:
return result
return result
Ловушки и крайние случаи
Подсчёты редко бывают неверными. Ошибочным оказывается порядок ответа.
- Разрешение равенства по первому появлению или по порядку в хеш-таблице. Во втором примере
-2,5и7встречаются по два раза, и только правило для меньшего значения делает[-2, 5]единственным правильным ответом. - Возврат массива кучи в том виде, в котором он есть. Куча упорядочена лишь частично, а её вершина — самое слабое значение, то, которое должно идти последним.
- Неверное понимание правила кучи для разрешения равенств. Из двух значений с одинаковой частотой большее значение слабее, поэтому min-heap для
(count, value)удалит не то значение. Используйте(count, -value)или сравнение, реализующее это правило. - Создание числа корзин, равного числу различных значений. Одно значение может встречаться
nраз, как в[3, 3, 3, 3], поэтому корзинаnдолжна существовать. - В Java сравнение двух значений типа
Integerс помощью!=. Так сравниваются ссылки, и это приводит к ошибке, когда счётчики превышают 127. Сначала преобразуйте их вint. - Извлечение всей корзины в конце. Остановитесь, как только получите
kзначений, даже если вы находитесь посреди корзины.
Частые вопросы4
Какова временная сложность задачи поиска K наиболее часто встречающихся элементов?
Подсчёт занимает O(n). Выбор k наибольших элементов затем требует O(d log d) при сортировке d различных значений, O(d log k) при использовании мин-кучи размера k и O(n) плюс один проход по диапазону значений при сортировке подсчётом. Поскольку d может достигать n, в худшем случае сортировка занимает O(n log n), а сортировка подсчётом работает за линейное время.
Можно ли найти K наиболее часто встречающихся элементов за время O(n)?
Да, с помощью сортировки распределением. Счётчики — целые числа от 1 до n, поэтому каждое значение помещается в корзину, соответствующую его счётчику, а чтение корзин от наибольшего счётчика к наименьшему перечисляет значения по частоте без какой-либо сортировки сравнением. Быстрый выбор по счётчикам в среднем также работает за O(n), но в худшем случае его сложность квадратичная.
Почему используется мин-куча, а не макс-куча?
Макс-куча из всех значений d тоже подойдёт: постройте её за O(d) и извлеките элемент k раз, затратив в общей сложности O(d + k log d). Мин-куча размера k хранит только k элементов и подходит для значений, поступающих по одному, поскольку её вершина — кандидат на удаление. Недостаток в том, что ответ она выдаёт в обратном порядке, поэтому результат нужно заполнять с конца.
Как разрешать ничьи в задаче «Топ-K наиболее частых элементов»?
Выбери одно правило и применяй его везде; здесь при одинаковом количестве сначала идет меньшее значение, что делает ответ единственным. При сортировке сравнивай количество, а затем значения. В куче из двух элементов с одинаковым количеством более слабым считается элемент с большим значением. При сортировке подсчётом заполняй корзины в порядке возрастания значений, и элементы в каждой корзине уже расположены в порядке разрешения равенства.
Похожие задачи
Задачи на те же идеи. Решите две или три, и приём запомнится.
Python
def topKFrequent(nums, k):
# Напишите код здесьСлучай 1
Случай 2
Случай 3
Ввод
nums = [4, 1, 4, 2, 1, 4, 3, 1, 4] k = 2
Ожидается
[4, 1]