Kth Largest Element in an Array
Otrzymujesz tablicę liczb całkowitych nums i liczbę całkowitą k. Zwróć k-tą największą wartość w nums: wartość na pozycji k, licząc od 1, po posortowaniu tablicy od największej do najmniejszej.
Równe wartości liczą się osobno. W [5, 5, 1] największa wartość to 5, a druga co do wielkości również wynosi 5.
Funkcja
- numsinteger-array
- wartości do uporządkowania
- kinteger
- którą największą wartość zwrócić, 1 oznacza największą
- Zwracainteger
- k-ta co do wielkości wartość, z uwzględnieniem duplikatów
Ograniczenia
1 ≤ k ≤ nums.length ≤ 104-104 ≤ nums[i] ≤ 104- Równe wartości są liczone jako osobne wartości.
Przykłady
- Wejście
- nums = [7, 2, 9, 4, 9, 1]k = 2
- Wyjście
- 9
- Wyjaśnienie
- Wartości od największej do najmniejszej to
9, 9, 7, 4, 2, 1. Dwie dziewiątki liczą się osobno, więc druga co do wielkości wartość to9, a nie7.
- Wejście
- nums = [5, -3, 8, 0, 2]k = 4
- Wyjście
- 0
- Wyjaśnienie
- Od największej do najmniejszej wartości to
8, 5, 2, 0, -3, a czwartą z nich jest0.
- Wejście
- nums = [6]k = 1
- Wyjście
- 6
- Wyjaśnienie
- Przy jednej wartości i
k = 1ta wartość jest największa.
+15 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Wartości docierają teraz pojedynczo. Czy potrafisz podać medianę wszystkich wartości otrzymanych do tej pory po każdym kolejnym napływie, w czasie O(log n) dla każdej wartości?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Po posortowaniu od największej do najmniejszej odpowiedź znajduje się na znanej pozycji. Na której? I czy potrzebujesz każdej innej wartości, żeby ją poznać?
Wartość k-ta co do wielkości jest najmniejszą z
knajwiększych wartości. Jeśli zachowujesz tylkoknajwiększych dotychczas napotkanych wartości, którą z nich porównujesz z nową wartością?Utrzymuj kopiec minimalny zawierający najwyżej
kwartości. Nowa wartość zastępuje element na szczycie, gdy jest większa, a element na szczycie na końcu jest odpowiedzią. Aby uzyskać średni czasO(n), podziel dane względem losowego pivota, tak jak robi to quicksort, i zachowaj tylko tę część, która zawiera indeksn-k.
Rozwiązanie
Sortowanie i odczytanie jednej pozycji odpowiada na pytanie i w tym przypadku jest wystarczająco szybkie. Osoba prowadząca rozmowę kwalifikacyjną chce zobaczyć, jak dużą część tego porządkowania możesz pominąć, ponieważ potrzebujesz jednej pozycji, a nie wszystkich n. Kopiec minimalny o rozmiarze k przechowuje tylko te wartości, które nadal mogą być odpowiedzią, a quickselect dzieli dane tak jak quicksort, ale przeszukuje tylko tę część, w której znajduje się odpowiedź, co obniża średni czas do O(n).
Sortuj i odczytaj jedną pozycję
Intuicja
Wartość k-ta od największej jest określona przez kolejność sortowania, więc uzyskaj tę kolejność. Posortowana od największej do najmniejszej lista [7, 2, 9, 4, 9, 1] zmienia się w [9, 9, 7, 4, 2, 1], a k-ta od największej znajduje się pod indeksem k-1. Dla k = 2 jest to indeks 1, czyli druga 9. Jeśli sortowanie umieszcza najmniejsze wartości na początku, odczytaj zamiast tego indeks n-k: pod indeksem 4 w [1, 2, 4, 7, 9, 9] znajduje się ta sama 9.
Duplikaty nie wymagają specjalnego traktowania: sortowanie zachowuje każdą kopię, a każda kopia zajmuje własną pozycję.
Dla n = 10^4 sortowanie wykonuje około n log n ≈ 1.3 × 10^5 porównań, co wystarcza, by przejść wszystkie testy. Wadą jest to, że porządkuje wszystkie n wartości, mimo że liczy się tylko jedna pozycja. Dwa kolejne podejścia wykonują mniej tej pracy.
Algorytm
- Skopiuj
nums, aby tablica wywołującego pozostała bez zmian. - Posortuj kopię. Użyj porównania numerycznego; niektóre języki domyślnie porównują liczby jako tekst.
- Zwróć indeks
k-1w kolejności od największego elementu albo indeksn-kw kolejności od najmniejszego elementu.
def findKthLargest(nums, k):
# Largest first: the k-th largest sits at index k-1.
ordered = sorted(nums, reverse=True)
return ordered[k - 1]Przechowuj k największych elementów w kopcu minimalnym
Intuicja
Wartość k-ta co do wielkości jest najmniejszą z k największych wartości. Przejdź więc raz przez nums i zachowuj w kopcu minimalnym tylko k największych dotąd napotkanych wartości. Szczyt kopca minimalnego to jego najmniejsza wartość, czyli dokładnie kandydat na odpowiedź.
Gdy pojawi się wartość x, a kopiec zawiera mniej niż k wartości, dodaj ją. W przeciwnym razie porównaj x z wartością na szczycie. Jeśli x nie jest większe, to co najmniej k zachowanych wartości jest nie mniejszych niż x, więc x nie może być odpowiedzią i pomijasz je. Jeśli x jest większe, wartość ze szczytu wypada z k największych: zastąp ją przez x. W przykładzie 2, dla k = 4, pierwsze cztery wartości wypełniają kopiec wartościami 5, -3, 8, 0, a na szczycie znajduje się -3. Następnie 2 jest większe niż -3 i zastępuje tę wartość, na szczycie pojawia się 0, a odpowiedzią jest 0.
Każda wartość wymaga co najwyżej jednej operacji na kopcu o złożoności O(log k), więc całość zajmuje O(n log k) czasu i O(k) pamięci. To szybsze niż sortowanie, gdy k jest małe, a rozwiązanie działa też na strumieniu: nie musisz mieć wszystkich wartości naraz. Python ma heapq, Java — PriorityQueue, C++ — priority_queue z greater, Go — container/heap, Rust — BinaryHeap z Reverse, a PHP — SplMinHeap. Kod w pozostałych językach implementuje kopiec w tablicy, gdzie dzieci indeksu i znajdują się pod indeksami 2i+1 i 2i+2 albo, w Lua i R, które indeksują od 1, pod indeksami 2i i 2i+1.
Algorytm
- Zacznij od pustego kopca typu min-heap.
- Dla każdej wartości
xdodaj ją, dopóki kopiec zawiera mniej niżkwartości. - Gdy kopiec zawiera już
kwartości, zastąp element na szczycie wartościąxtylko wtedy, gdyxjest większe od elementu na szczycie. - Po ostatniej wartości zwróć element ze szczytu kopca.
import heapq
def findKthLargest(nums, k):
# A min-heap of the k largest values so far; its top is the smallest of them.
heap = []
for x in nums:
if len(heap) < k:
heapq.heappush(heap, x)
elif x > heap[0]:
heapq.heapreplace(heap, x) # drop the top, add x
return heap[0]Szybkie wybieranie z podziałem na trzy części
Intuicja
Quicksort wybiera pivot i dzieli tablicę: mniejsze wartości trafiają na jego lewo, a większe na prawo. Po jednym podziale pivot znajduje się na swoim ostatecznym indeksie w posortowanej tablicy, choć żadna z części nie jest jeszcze posortowana. Quickselect wykorzystuje ten fakt. Przy kolejności od najmniejszych do największych odpowiedź znajduje się pod indeksem target = n-k. Po podziale target znajduje się albo na lewo od pivota, albo na jego pozycji, albo na prawo od niego, więc kontynuujesz po jednej stronie i odrzucasz drugą.
Dla [7, 2, 9, 4, 9, 1] i k = 2 wartość target wynosi 6-2 = 4. Podziel tablicę względem 4: 2 i 1 zajmują indeksy 0 i 1, 4 zajmuje indeks 2, a 7, 9, 9 zajmują indeksy od 3 do 5. Indeks 4 znajduje się po prawej, więc zachowujesz tylko indeksy od 3 do 5. Podziel je względem 9: 7 zajmuje indeks 3, a obie dziewiątki zajmują indeksy 4 i 5. Pod indeksem 4 znajduje się 9, więc odpowiedzią jest 9.
Użyj podziału na trzy części: wartości mniejsze od pivota, następnie wartości równe pivotowi, a na końcu wartości większe od niego, wyznaczane przez lt i gt. Blok równych wartości [lt, gt] znajduje się już na swoim miejscu w posortowanej tablicy, więc jeśli target znajduje się w jego obrębie, możesz zakończyć. Przy zwykłym podziale na dwie części tablica zawierająca 10^4 kopii 7 zmniejsza się o jedną wartość w każdej rundzie, co daje około 5 × 10^7 kroków; wersja z podziałem na trzy części znajduje odpowiedź w jednym przebiegu.
Wybierz pivot losowo. W połowie przypadków znajdzie się on w środkowej połowie zakresu, co zmniejsza zakres do najwyżej trzech czwartych, więc oczekiwana praca to kilka przebiegów po n wartościach: O(n). Najgorszy przypadek nadal ma złożoność O(n²), jeśli każdy pivot jest skrajną wartością, a stały wybór, na przykład pierwszego elementu, prowadzi do niego dla posortowanego wejścia. Kod działa na kopii, co wymaga O(n) pamięci; podział samego nums zmniejsza to do O(1), jeśli możesz zmienić dane wejściowe.
Algorytm
- Skopiuj
numsdoa, ustawtarget = n-k,lo = 0ihi = n-1. - Wybierz losowy element osiowy z
a[lo..hi]. - Podziel
a[lo..hi]na wartości mniejsze od elementu osiowego, równe mu i większe od niego, pozostawiając równe wartości wa[lt..gt]. - Jeśli
target < lt, ustawhi = lt-1; jeślitarget > gt, ustawlo = gt+1; w przeciwnym razie zwróć element osiowy. - Powtórz od kroku 2.
import random
def findKthLargest(nums, k):
a = list(nums)
target = len(a) - k # the answer's index once a is sorted smallest first
lo, hi = 0, len(a) - 1
while True:
pivot = a[random.randint(lo, hi)]
# Three-way partition of a[lo..hi]: < pivot, then == pivot, then > pivot.
lt, i, gt = lo, lo, hi
while i <= gt:
if a[i] < pivot:
a[lt], a[i] = a[i], a[lt]
lt += 1
i += 1
elif a[i] > pivot:
a[i], a[gt] = a[gt], a[i]
gt -= 1
else:
i += 1
# Now a[lt..gt] all equal pivot, and they are in their sorted places.
if target < lt:
hi = lt - 1
elif target > gt:
lo = gt + 1
else:
return pivot
Pułapki i przypadki brzegowe
Większość błędnych odpowiedzi wynika z usuwania duplikatów i mylenia dwóch sposobów liczenia pozycji.
- Najpierw usuwanie duplikatów. W zadaniu liczy się każda kopia: dla
[7, 2, 9, 4, 9, 1]przyk = 2odpowiedzią jest9, ale po zamianie tablicy na zbiór wynikiem jest7. - Odczytywanie niewłaściwego indeksu.
kliczy się od 1, więc odpowiedź znajduje się pod indeksemk-1w kolejności od największej do najmniejszej wartości, a pod indeksemn-kw kolejności od najmniejszej do największej, a nien-k-1. - Sortowanie liczb jak tekstu. W JavaScript i TypeScript
[10, 9, 2].sort()daje[10, 2, 9]. Przekaż(a, b) => a - b. - Używanie kopca maksymalnego o rozmiarze
k. Usuwanie największej wartości pozwala zachowaćknajmniejszych wartości i zwraca k-tą najmniejszą. - Quickselect z podziałem dwukierunkowym lub stałym elementem osiowym. Wiele równych wartości lub posortowana tablica powodują wtedy koszt
O(n²), co uwzględniają duże testy.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu „K-ty największy element w tablicy”?
Sortowanie zajmuje czas O(n log n). Kopiec minimum o rozmiarze k wymaga czasu O(n log k) i pamięci O(k). Quickselect z losowym elementem osiowym zajmuje średnio czas O(n), a w najgorszym przypadku O(n²), do którego wystąpienia losowy element osiowy czyni bardzo mało prawdopodobnym.
Dlaczego do znajdowania k-tego największego elementu używa się kopca minimalnego, a nie maksymalnego?
Kopiec przechowuje k największych dotychczas napotkanych wartości, a wartością, z którą musisz porównać kolejną i którą musisz usunąć, jest najmniejsza z nich. Kopiec minimalny umieszcza tę wartość na szczycie. Kopiec maksymalny działa tylko wtedy, gdy umieścisz w nim wszystkie n wartości i usuniesz element k-1 razy, co wymaga O(n) pamięci.
Czy do znalezienia k-tego największego elementu użyć kopca czy algorytmu quickselect?
Quickselect jest średnio szybszy — O(n) — ale wymaga przechowywania wszystkich wartości w pamięci i zmienia ich kolejność. Kopiec ma złożoność O(n log k), bez złego przypadku najgorszego, i sprawdza się, gdy wartości napływają pojedynczo i nie możesz przechować ich wszystkich. Na rozmowie kwalifikacyjnej wyjaśnij obie metody i zaimplementuj tę, o którą poproszą w pytaniu uzupełniającym.
Czy można znaleźć k-ty największy element w czasie liniowym w najgorszym przypadku?
Tak. Reguła mediany median wybiera element osiowy, który z gwarancją odcina stałą część wartości, dzięki czemu wybór ma złożoność O(n) w najgorszym przypadku, choć w praktyce jest wolniejszy niż wybór losowego elementu osiowego. Gdy wartości są ograniczone do zakresu od -10^4 do 10^4, możesz też zliczyć, ile razy występuje każda wartość, i przechodzić w dół od 10^4, aż miniesz k wartości, w czasie O(n + 2 × 10^4).
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def findKthLargest(nums, k):
# Wpisz tutaj kodPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
nums = [7, 2, 9, 4, 9, 1] k = 2
Oczekiwane
9