Search in Rotated Sorted Array
Lista różnych liczb całkowitych została posortowana rosnąco, a następnie obrócona: pewną liczbę elementów, być może zero, przeniesiono z początku na koniec, zachowując ich kolejność. Na przykład [2, 5, 8, 11, 15, 19, 23] obrócona o 4 pozycje daje [15, 19, 23, 2, 5, 8, 11]. Otrzymujesz obróconą listę nums i liczbę całkowitą target. Zwróć indeks target w nums, licząc od 0, lub -1, jeśli go tam nie ma, w czasie O(log n).
Funkcja
- numsinteger-array
- obrócona posortowana lista różnych liczb całkowitych
- targetinteger
- wartość, której należy szukać
- Zwracainteger
- indeks elementu target w nums albo -1, jeśli go nie ma
Ograniczenia
1 ≤ nums.length ≤ 5000-104 ≤ nums[i], target ≤ 104- Wszystkie wartości w
numssą różne. numsto rosnąca lista obrócona o pewną wartośćk, gdzie0 ≤ k < nums.length;k = 0oznacza, że lista nie została obrócona.
Przykłady
- Wejście
- nums = [15, 19, 23, 2, 5, 8, 11]target = 5
- Wyjście
- 4
- Wyjaśnienie
- 5 znajduje się pod indeksem 4. Pierwszy środkowy element, indeks 3, zawiera 2, więc prawa połowa
[2, 5, 8, 11]jest posortowana, a 5 znajduje się między 2 a 11. Następny środkowy element, indeks 5, zawiera 8; posortowana lewa część[5, 8]zawiera 5, co prowadzi do indeksu 4.
- Wejście
- nums = [40, 50, 60, 70, 10, 20, 30]target = 65
- Wyjście
- -1
- Wyjaśnienie
- 65 powinno znaleźć się między 60 a 70, ale żaden element go nie zawiera. Pierwszy środkowy element, 70 o indeksie 3, umieszcza 65 w posortowanej lewej części
[40, 50, 60, 70]. Zakres zawęża się w tej części, aż stanie się pusty, więc funkcja zwraca-1.
- Wejście
- nums = [8, 13, 21, 1, 3, 5]target = 13
- Wyjście
- 1
- Wyjaśnienie
- Pierwszy środkowy element, indeks 2, zawiera 21. Lewa część
[8, 13, 21]jest posortowana, a 13 znajduje się między 8 a 21, więc cała prawa część zostaje odrzucona. Następnie wyszukiwanie znajduje 13 pod indeksem 1.
+23 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Jeśli nums może zawierać duplikaty, żaden algorytm nie może zagwarantować O(log n). Czy potrafisz to udowodnić? Utwórz obróconą listę jedynek z ukrytym w niej pojedynczym zerem, tak aby każde wyszukiwanie zera wymagało odczytania każdego elementu.
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Wybierz dowolny środkowy indeks i przyjrzyj się dwóm połówkom po obu jego stronach. Obrót utworzył jedno miejsce, w którym wartości maleją — od największej do najmniejszej. Czy obie połówki mogą zawierać ten spadek?
Co najmniej jedna połowa jest zawsze posortowana, a porównanie
nums[lo]znums[mid]pozwala ustalić, która to połowa. Dla posortowanej połowy możesz w jednym kroku sprawdzić, czytargetznajduje się między jej pierwszą a ostatnią wartością.Zachowuj
loihiwokół części, która może jeszcze zawieraćtarget. Na każdym kroku, jeśli zakres wartości posortowanej połowy zawieratarget, zachowaj tę połowę; w przeciwnym razie zachowaj drugą. Zakończ, gdy znajdziesztargetlub zakres będzie pusty.
Rozwiązanie
Obrócona posortowana lista składa się z dwóch posortowanych ciągów umieszczonych jeden za drugim: [15, 19, 23] i [2, 5, 8, 11]. Zwykłe wyszukiwanie binarne nie sprawdza się w jej przypadku, ponieważ porównanie target ze środkową wartością nie pozwala już ustalić, po której stronie znajduje się target. Rozwiązanie opiera się na jednym fakcie: niezależnie od tego, w którym miejscu przetniesz listę, co najmniej jedna z dwóch połówek jest w pełni posortowana, a w przypadku posortowanej połowy wystarczy jedno porównanie, aby stwierdzić, czy target może się w niej znajdować.
Skanuj każdy element
Intuicja
Sprawdź każdy indeks po kolei i zwróć pierwszy, którego wartość jest równa target. Jeśli pętla zakończy się bez znalezienia dopasowania, zwróć -1. Wartości są różne, więc pierwsze dopasowanie jest jedynym, a przeszukiwanie jest poprawne dla każdej listy, niezależnie od tego, czy jest obrócona.
Ignoruje to wszystko, o czym informuje cię treść zadania. Lista składa się z dwóch posortowanych ciągów, a mimo to przeszukiwanie liniowe odczytuje nawet wszystkie 5000 elementów, podczas gdy wyszukiwanie binarne wymaga około 13 porównań. Różnica rośnie wraz z rozmiarem danych wejściowych: milion elementów oznacza milion porównań, w porównaniu z około 20. Zadanie wymaga O(log n), więc jest to punkt wyjścia do ulepszenia, a nie odpowiedź.
Algorytm
- Dla każdego indeksu
iod 0 don-1porównajnums[i]ztarget. - Jeśli są równe, zwróć
i. - Po pętli zwróć
-1.
def search(nums, target):
for i, value in enumerate(nums):
if value == target:
return i
return -1Znajdź punkt obrotu, a następnie wyszukaj binarnie
Intuicja
Obrócona lista składa się z dwóch posortowanych części, a druga zaczyna się od najmniejszej wartości. Oznaczmy jej indeks jako k. Gdy znasz k, problem sprowadza się do zwykłego wyszukiwania binarnego: nums[k..n-1] jest posortowana i zawiera wartości od nums[k] do nums[n-1], a nums[0..k-1] jest posortowana i zawiera wszystkie większe wartości. Jedno porównanie target z nums[k] i nums[n-1] pozwala wybrać część do przeszukania.
Aby znaleźć k, wykonaj wyszukiwanie binarne, szukając spadku. Porównaj wartość środkową z ostatnią wartością zakresu, nums[hi]. Jeśli nums[mid] > nums[hi], wartości gdzieś za mid maleją, więc najmniejsza wartość znajduje się po prawej stronie: ustaw lo = mid + 1. W przeciwnym razie nums[mid..hi] rośnie bez spadku, więc najmniejsza wartość znajduje się na pozycji mid lub przed nią: ustaw hi = mid, pozostawiając mid w zakresie. Gdy lo zrówna się z hi, ten indeks to k.
Prześledź pierwszy przykład: [15, 19, 23, 2, 5, 8, 11] z target = 5. Środkowa wartość 2 nie jest większa niż 11, więc hi przyjmuje wartość 3; następnie 19 jest większe niż 2, więc lo przyjmuje wartość 2; potem 23 jest większe niż 2, więc lo przyjmuje wartość 3, a k = 3. Ponieważ 5 znajduje się między nums[3] = 2 a nums[6] = 11, przeszukaj indeksy od 3 do 6. Wyszukiwanie binarne znajduje 5 na indeksie 4. Dwa wyszukiwania binarne kosztują około 2 log2 n kroków.
Algorytm
- Ustaw
lo = 0ihi = n-1. Dopókilo < hi, obliczmid; jeślinums[mid] > nums[hi], ustawlo = mid + 1, w przeciwnym razie ustawhi = mid. - Oznacz końcowy indeks jako
k: znajduje się pod nim najmniejsza wartość. - Jeśli
nums[k] ≤ target ≤ nums[n-1], przeszukaj indeksy odkdon-1; w przeciwnym razie przeszukaj indeksy od 0 dok-1. - Wykonaj zwykłe wyszukiwanie binarne w tym zakresie i zwróć indeks
targetalbo-1, jeśli zakres jest pusty.
def search(nums, target):
n = len(nums)
# 1. Find k, the index of the smallest value, where the second run starts.
lo, hi = 0, n - 1
while lo < hi:
mid = (lo + hi) // 2
if nums[mid] > nums[hi]:
lo = mid + 1 # the drop is right of mid
else:
hi = mid # mid is in the low run: the minimum is at mid or left of it
k = lo
# 2. nums[k..n-1] and nums[0..k-1] are sorted: search the one whose range holds target.
if nums[k] <= target <= nums[n - 1]:
lo, hi = k, n - 1
else:
lo, hi = 0, k - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return -1Jedno wyszukiwanie binarne w posortowanej połowie
Intuicja
Nie musisz wiedzieć, gdzie znajduje się punkt obrotu. Zachowaj standardową obietnicę wyszukiwania binarnego: jeśli target znajduje się na liście, jego indeks leży między lo a hi. Spójrz na środkowy indeks mid. Wartości maleją tylko raz w całej liście, więc ten spadek występuje w co najwyżej jednej z dwóch połówek wokół mid, a druga połowa jest posortowana.
Znajdź posortowaną połowę za pomocą jednego porównania. Jeśli nums[lo] ≤ nums[mid], lewa połowa nums[lo..mid] nie zawiera spadku i jest posortowana. Skoro wiesz już, że nums[mid] nie jest równe target, target może znajdować się w tej połowie tylko wtedy, gdy nums[lo] ≤ target < nums[mid]. Jeśli tak jest, ustaw hi = mid - 1; jeśli nie, target może znajdować się tylko w drugiej połowie, więc ustaw lo = mid + 1. Gdy nums[lo] > nums[mid], spadek znajduje się po lewej stronie, prawa połowa nums[mid..hi] jest posortowana, a rozstrzyga lustrzany warunek nums[mid] < target ≤ nums[hi]. Nigdy nie rozumujesz bezpośrednio o nieposortowanej połowie: target trafia do niej dokładnie wtedy, gdy nie może znajdować się w posortowanej połowie.
Prześledź pierwszy przykład: [15, 19, 23, 2, 5, 8, 11] przy target = 5. Zakres od 0 do 6 ma środkowy indeks 3, a jego wartość to 2. Ponieważ 15 jest większe od 2, prawa połowa [2, 5, 8, 11] jest posortowana, a 5 się w niej znajduje, więc lo przyjmuje wartość 4. Zakres od 4 do 6 ma środkowy indeks 5, a jego wartość to 8. Teraz nums[4] = 5 ≤ 8, lewa połowa [5, 8] jest posortowana i zawiera 5, więc hi przyjmuje wartość 4. Pod indeksem 4 znajduje się 5: zwróć 4.
Każdy krok zmniejsza zakres o połowę, tak jak w zwykłym wyszukiwaniu binarnym, więc pętla wykonuje się najwyżej około log2(n) + 1 razy: 13 kroków dla 5000 elementów, przy użyciu dwóch dodatkowych indeksów pamięci.
Algorytm
- Ustaw
lo = 0ihi = n-1. - Dopóki
lo ≤ hi, obliczmid. Jeślinums[mid]jest równetarget, zwróćmid. - Jeśli
nums[lo] ≤ nums[mid], lewa połowa jest posortowana: jeślinums[lo] ≤ target < nums[mid], ustawhi = mid - 1, w przeciwnym razie ustawlo = mid + 1. - W przeciwnym razie prawa połowa jest posortowana: jeśli
nums[mid] < target ≤ nums[hi], ustawlo = mid + 1, w przeciwnym razie ustawhi = mid - 1. - Gdy pętla się zakończy, zwróć
-1.
def search(nums, target):
lo, hi = 0, len(nums) - 1 # target, if present, sits in nums[lo..hi]
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]:
# nums[lo..mid] is sorted: target is in it only if it fits its range
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else:
# nums[mid..hi] is sorted
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1
Pułapki i przypadki brzegowe
Wyszukiwanie w jednym przebiegu jest krótkie, a niemal każdy błąd dotyczy operatora porównania.
- Zapisywanie
nums[lo] < nums[mid]zamiast≤. Gdy zostają dwa elementy,midjest równelo, a lewa połowa zawiera jeden element, więc jest posortowana. Przy ścisłym warunku[9, 4]itarget = 4traktują[9, 4]jako posortowaną prawą połowę, szukają 4 poza zakresem od 9 do 4 i zwracają-1. - Porównywanie najpierw
targetznums[mid], jak w zwykłym wyszukiwaniu binarnym. W[15, 19, 23, 2, 5, 8, 11]przytarget = 19środkowa wartość 2 jest mniejsza od 19, więc wyszukiwanie przesuwa się w prawo i nigdy nie sprawdza indeksu 1. - Sprawdzanie tylko jednego końca posortowanej połowy. W
[40, 50, 60, 70, 80, 10, 20]przytarget = 80środkowa wartość wynosi 70, a lewa połowa[40, 50, 60, 70]jest posortowana. Sam warunektarget ≥ nums[lo]kieruje wyszukiwanie w lewo, ponieważ 80 jest większe od 40, ale 80 jest też większe od 70, więc znajduje się w prawej połowie. Sprawdź oba końce. - Pomijanie przypadku nieobróconej tablicy w podejściu dwuetapowym. Gdy
k = 0, drugi przebieg jest pusty, a jego zakres wynosi od0do-1. Przy indeksach ze znakiem to w porządku, ale przy indeksach bez znaku (usizew Rust)k - 1powoduje niedomiar, dlatego kod w Rust używa zakresów domkniętych z lewej i otwartych z prawej. - Zwracanie samej pozycji w Lua i R. Ich listy zaczynają się od 1, więc przed zwróceniem odejmij 1.
Najczęstsze pytania4
Jaka jest złożoność czasowa wyszukiwania w obróconej posortowanej tablicy?
Czas O(log n) i dodatkowa pamięć O(1). W każdym kroku pozostaje połowa bieżącego zakresu, tak samo jak w zwykłym wyszukiwaniu binarnym, więc lista 5000 elementów wymaga najwyżej 13 kroków. Wersja dwuetapowa, która najpierw znajduje punkt rotacji, również ma złożoność O(log n) i wymaga około dwa razy więcej kroków.
Skąd wiesz, która połowa obróconej tablicy jest posortowana?
Porównaj nums[lo] z nums[mid]. Wartości spadają tylko raz na całej liście. Jeśli nums[lo] ≤ nums[mid], ten spadek nie występuje między lo a mid, więc lewa połowa jest posortowana. W przeciwnym razie spadek znajduje się w lewej połowie, co oznacza, że prawa połowa, od mid do hi, nie zawiera spadku i jest posortowana.
Czy algorytm działa, gdy tablica zawiera duplikaty?
Nie w tej postaci. W [1, 0, 1, 1, 1] wartości nums[lo], nums[mid] i nums[hi] są równe 1, więc nie da się udowodnić, że którakolwiek połowa jest posortowana. Zwykle rozwiązuje się to przez przesunięcie lo o jeden do przodu, gdy nums[lo], nums[mid] i nums[hi] są równe. Dzięki temu odpowiedź pozostaje poprawna, ale złożoność w najgorszym przypadku wynosi O(n).
Czy najpierw znaleźć punkt obrotu, czy wyszukać element za jednym przejściem?
Obie działają w czasie O(log n). Najpierw znalezienie indeksu minimum rozdziela problem na dwa zwykłe wyszukiwania binarne, dzięki czemu każda część ponownie wykorzystuje kod, któremu już ufasz. Wyszukiwanie jednokrotnego przejścia wykonuje to samo zadanie w jednej pętli, wymagając mniejszej liczby kroków, i jest wersją, której najczęściej oczekują osoby prowadzące rozmowy kwalifikacyjne.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def search(nums, target):
# Napisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
nums = [15, 19, 23, 2, 5, 8, 11] target = 5
Oczekiwane
4