Top K Frequent Elements
Otrzymujesz tablicę liczb całkowitych nums oraz liczbę całkowitą k. Zwróć k wartości, które występują w nums najczęściej, w kolejności od najczęściej występującej. Jeśli dwie wartości występują tyle samo razy, pierwsza powinna być mniejsza z nich.
Każda wartość pojawia się w odpowiedzi tylko raz, niezależnie od tego, jak często występuje w nums, a k nigdy nie jest większe niż liczba różnych wartości.
Funkcja
- numsinteger-array
- wartości do zliczenia
- kinteger
- ile wartości zwrócić
- Zwracainteger-array
- k najczęściej występujących wartości, od najczęstszej do najrzadszej; w przypadku remisu najpierw mniejsza wartość
Ograniczenia
1 ≤ nums.length ≤ 104-104 ≤ nums[i] ≤ 1041 ≤ k, akjest co najwyżej liczbą różnych wartości wnums.
Przykłady
- Wejście
- nums = [4, 1, 4, 2, 1, 4, 3, 1, 4]k = 2
- Wyjście
- [4, 1]
- Wyjaśnienie
4występuje cztery razy,1trzy razy, a2i3po jednym razie. Dwie najczęściej występujące wartości to4, a następnie1.
- Wejście
- nums = [5, -2, 7, -2, 7, 5, 9]k = 2
- Wyjście
- [-2, 5]
- Wyjaśnienie
-2,5i7występują po dwa razy, a9raz. Trzy wartości zajmują pierwsze miejsce, więc odpowiedzią są dwie mniejsze:-2i5.
- Wejście
- nums = [8]k = 1
- Wyjście
- [8]
- Wyjaśnienie
- Jest jedna wartość, więc występuje najczęściej.
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Zacznij od ustalenia, jak często występuje każda wartość. Która struktura danych przypisuje każdej wartości jej liczbę wystąpień w jednym przebiegu?
Mając już zliczenia, chcesz wybrać
knajlepszych wartości według jednego porządku: najpierw większa liczba wystąpień, a przy remisie mniejsza wartość. Posortowanie wszystkich różnych wartości się sprawdzi. Kopiec minimalny o rozmiarzekzachowuje tylko te wartości, które wciąż mogą znaleźć się w odpowiedzi.Licznik to liczba całkowita od 1 do
n. Utwórz jedno wiadro dla każdego licznika, przy czym wiadroczawiera wartości występujące dokładniecrazy, a następnie odczytuj wiadra od największego licznika do najmniejszego. Wypełniaj wiadra, przechodząc po wartościach od najmniejszej do największej, a każde wiadro będzie już uporządkowane według kolejności przy remisie.
Rozwiązanie
Zliczanie to ta szybsza połowa: jedno przejście z użyciem mapy mieszającej pozwala policzyć wystąpienia każdej wartości. Prawdziwe pytanie brzmi, jak wybrać k najlepszych wartości, nie wykonując więcej pracy, niż to konieczne. Posortowanie wszystkich d różnych wartości według liczby wystąpień kosztuje O(d log d), kopiec minimum o rozmiarze k zmniejsza ten koszt do O(d log k), a ponieważ liczba wystąpień jest liczbą całkowitą z zakresu od 1 do n, sortowanie kubełkowe porządkuje wartości według liczby wystąpień bez żadnych porównań.
Policz, a następnie posortuj według liczby
Intuicja
Najpierw zlicz wystąpienia. Jedno przejście z mapą haszującą przechowującą liczbę wystąpień każdej wartości zamienia [4, 1, 4, 2, 1, 4, 3, 1, 4] w 4 → 4, 1 → 3, 2 → 1, 3 → 1.
Następnie ułóż różne wartości w kolejności, w jakiej mają znaleźć się w odpowiedzi: najpierw większa liczba wystąpień, a przy tej samej liczbie wystąpień — mniejsza wartość. Ustaw sortowanie dokładnie według tego porównania: liczba wystąpień jako pierwsze kryterium, a wartość jako drugie. Pierwsze k elementów posortowanej listy to odpowiedź. Tutaj kolejność to 4, 1, 2, 3, a k = 2 oznacza, że zostają 4 i 1.
Zliczanie wymaga O(n). Sortowanie d różnych wartości wymaga O(d log d), a gdy każda wartość jest inna — najwyżej O(n log n): 10^4 wartości oznacza około 1.3 × 10^5 porównań, co jest szybkie. Marnotrawstwo polega na tym, że sortowanie porządkuje wszystkie wartości, choć liczy się tylko pierwsze k.
Algorytm
- Policz każdą wartość w mapie haszującej.
- Umieść unikalne wartości na liście.
- Posortuj listę według liczby wystąpień od największej do najmniejszej, a przy równych liczbach — według wartości od najmniejszej do największej.
- Zwróć pierwsze
kwartości.
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]Zachowaj k najlepszych elementów w kopcu minimum
Intuicja
Potrzebujesz tylko k najlepszych wartości, więc przechowuj tylko k kandydatów. Dla każdej nowej wartości trzeba sprawdzić, czy jest lepsza od najsłabszego przechowywanego kandydata. Kandydat jest słabszy, gdy ma mniejszą liczność albo taką samą liczność i większą wartość. Kopiec minimum uporządkowany według tej reguły trzyma na szczycie najsłabszego kandydata, którego odczyt zajmuje O(1), a zastąpienie O(log k).
Przejdź przez różne wartości. Gdy kopiec zawiera mniej niż k elementów, dodaj wartość. Potem wartość lepsza od elementu na szczycie zastępuje go, a wartość, która nie jest lepsza, zostaje pominięta, ponieważ przechowujesz już k lepszych wartości. Korzystając z kopca dostępnego w bibliotece, możesz skrócić kod: wstawiaj każdą wartość i usuwaj element ze szczytu za każdym razem, gdy kopiec przekroczy rozmiar k. W ten sposób zachowasz te same k wartości.
Na końcu kopiec zawiera wynik, ale nie jest on uporządkowany: kopiec jest tylko częściowo posortowany. Usuwanie elementów zwraca najpierw najsłabszą wartość, więc zapisuj wynik od ostatniej pozycji do pierwszej.
Każda z d różnych wartości wymaga co najwyżej jednej operacji na kopcu zawierającym k elementów, więc wybór zajmuje O(d log k). Jest to szybsze niż sortowanie, gdy k jest dużo mniejsze niż d, na przykład przy wybieraniu 10 najlepszych spośród 8000 różnych wartości.
Algorytm
- Policz wystąpienia każdej wartości w mapie haszującej.
- Dla każdej unikalnej wartości dodaj ją, dopóki kopiec nie zawiera mniej niż
kwartości. - Gdy kopiec jest pełny, porównaj wartość z elementem na szczycie, czyli najsłabszą zachowywaną wartością. Jeśli nowa wartość jest silniejsza, umieść ją na szczycie i przesiej w dół.
- Zdejmij element z kopca
krazy, za każdym razem wpisując wartość do wyniku, zaczynając od ostatniej pozycji i kończąc na pierwszej.
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 resultPolicz, a następnie posortuj kubełkowo według liczby wystąpień
Intuicja
Licznik nie może być dowolną liczbą: jest liczbą całkowitą od 1 do n. Dzięki temu można zastosować sortowanie kubełkowe. Utwórz jeden kubełek dla każdego licznika — kubełek c będzie zawierał wartości występujące dokładnie c razy — i odczytaj kubełki, zaczynając od kubełka n. Wartości pojawią się od najczęstszych, a żadnych dwóch liczników nie trzeba porównywać.
Reguła rozstrzygania remisów wymaga jeszcze jednej rzeczy: w kubełku najpierw musi znaleźć się mniejsza wartość. Wartości mieszczą się w przedziale od -10^4 do 10^4, więc do zliczania wystarczy tablica R = 2 × 10^4 + 1 liczników, w której wartość v znajduje się pod indeksem v + 10^4. Przejdź przez tę tablicę od najmniejszej do największej wartości i dodaj każdą wartość do kubełka odpowiadającego jej licznikowi. Każdy kubełek zapełnia się w kolejności rosnącej, czyli w kolejności rozstrzygania remisów, więc nie trzeba niczego sortować.
Dla [5, -2, 7, -2, 7, 5, 9] przejście umieszcza kolejno -2, 5 i 7 w kubełku 2, a 9 w kubełku 1. Odczytując kubełki od kubełka 7 w dół, napotykamy pierwszy kubełek z wartościami — kubełek 2 — a k = 2 oznacza wybór -2 i 5.
Wykonujemy jedno przejście po nums, jedno przejście po R licznikach i jedno przejście po kubełkach, czyli łącznie O(n + R): czas liniowy dla ustalonego zakresu wartości. Jeśli zamiast tablicy zliczającej użyjesz mapy mieszającej, zliczanie nadal będzie liniowe, ale kubełki będą zapełniane w kolejności mapy i trzeba będzie posortować każdy z nich, aby zachować regułę rozstrzygania remisów.
Algorytm
- Zlicz każde wystąpienie wartości w tablicy indeksowanej przez
value + 10^4. - Utwórz kubełki od 1 do
n, po jednej liście dla każdej możliwej liczby wystąpień. - Przejdź przez tablicę zliczającą od najmniejszej wartości do największej i dodaj każdą występującą wartość do kubełka odpowiadającego jej liczbie wystąpień.
- Odczytuj kubełki w kolejności od liczby wystąpień
ndo 1, pobierając wartości, aż uzyskaszkwartości.
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
Pułapki i przypadki brzegowe
Liczenie rzadko jest błędne. Błędna bywa kolejność odpowiedzi.
- Rozstrzyganie remisów na podstawie kolejności pierwszego wystąpienia lub kolejności w mapie haszującej. W drugim przykładzie
-2,5i7występują po dwa razy, a tylko reguła mniejszej wartości sprawia, że[-2, 5]jest jedyną poprawną odpowiedzią. - Zwracanie tablicy kopca w takiej postaci, w jakiej jest. Kopiec jest tylko częściowo uporządkowany, a jego szczyt zawiera najsłabszą wartość, czyli tę, która powinna znaleźć się na końcu.
- Odwracanie reguły rozstrzygania remisów w kopcu. Spośród dwóch wartości o tej samej liczności słabsza jest większa, więc kopiec minimalny dla
(count, value)usuwa niewłaściwą wartość. Użyj(count, -value)albo porównania zgodnego z regułą. - Tworzenie tylko tylu kubełków, ile jest różnych wartości. Jedna wartość może wystąpić
nrazy, jak w[3, 3, 3, 3], dlatego kubełeknmusi istnieć. - W Javie porównywanie dwóch liczności typu
Integerza pomocą!=. To porównuje referencje i powoduje błędy, gdy liczności przekroczą 127. Najpierw rozpakuj je do typuint. - Wybieranie całego kubełka na końcu. Zatrzymaj się, gdy tylko uzyskasz
kwartości, nawet w środku kubełka.
Najczęstsze pytania3
Jaka jest złożoność czasowa problemu Top K Frequent Elements?
Zliczanie zajmuje O(n). Wybór k największych elementów kosztuje następnie O(d log d) przy sortowaniu d różnych wartości, O(d log k) przy użyciu kopca minimum o rozmiarze k oraz O(n) i jednego przejścia po zakresie wartości przy sortowaniu kubełkowym. Ponieważ d może osiągnąć n, sortowanie ma w najgorszym przypadku złożoność O(n log n), a sortowanie kubełkowe jest liniowe.
Czy problem Top K Frequent Elements można rozwiązać w czasie O(n)?
Tak, za pomocą sortowania kubełkowego. Liczności są liczbami całkowitymi od 1 do n, więc każda wartość trafia do kubełka odpowiadającego jej liczności, a odczytanie kubełków od największej liczności do najmniejszej pozwala uporządkować wartości według częstotliwości bez sortowania przez porównania. Quickselect dla liczności ma również średnio złożoność O(n), ale w najgorszym przypadku jego złożoność jest kwadratowa.
Dlaczego używać kopca minimum, a nie kopca maksimum?
Max-kopiec ze wszystkich wartości d też się sprawdzi: zbuduj go w czasie O(d) i wykonaj operację pop k razy; łącznie zajmie to O(d + k log d). Min-kopiec o rozmiarze k przechowuje tylko k elementów i nadaje się do wartości napływających pojedynczo, ponieważ jego wierzchołek wskazuje element do usunięcia. Ceną jest to, że zwraca wynik w odwrotnej kolejności, więc wypełniasz wynik od końca.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
def topKFrequent(nums, k):
# Wpisz kod tutajTest #1
Test #2
Test #3
Argumenty
nums = [4, 1, 4, 2, 1, 4, 3, 1, 4] k = 2
Oczekiwany wynik
[4, 1]