Muszę się zgodzić, że to dość dziwne, gdy po raz pierwszy widzisz algorytm O (log n) ... skąd u licha pochodzi ten logarytm? Okazuje się jednak, że istnieje kilka różnych sposobów na wyświetlenie terminu dziennika w notacji duże-O. Tu jest kilka:
Wielokrotne dzielenie przez stałą
Weź dowolną liczbę n; powiedz, 16. Ile razy możesz podzielić n przez dwa, zanim otrzymasz liczbę mniejszą lub równą jeden? Mamy to dla 16 lat
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
Zauważ, że kończy się to na czterech krokach. Co ciekawe, mamy również ten log 2 16 = 4. Hmmm ... a co ze 128?
128 / 2 = 64
64 / 2 = 32
32 / 2 = 16
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
Wymagało to siedmiu kroków i log 2 128 = 7. Czy to zbieg okoliczności? Nie! Jest ku temu dobry powód. Załóżmy, że dzielimy liczbę n przez 2 i razy. Wtedy otrzymujemy liczbę n / 2 i . Jeśli chcemy obliczyć wartość i, gdzie ta wartość wynosi co najwyżej 1, otrzymujemy
n / 2 i ≤ 1
n ≤ 2 i
log 2 n ≤ w
Innymi słowy, jeśli wybierzemy liczbę całkowitą i taką, że i ≥ log 2 n, to po podzieleniu n na pół i razy otrzymamy wartość, która wynosi najwyżej 1. Najmniejsze i, dla którego jest to gwarantowane, to z grubsza log 2 n, więc jeśli mamy algorytm, który dzieli przez 2, aż liczba stanie się wystarczająco mała, wówczas możemy powiedzieć, że kończy się O (log n) krokami.
Ważnym szczegółem jest to, że nie ma znaczenia, przez jaką stałą dzielisz n (o ile jest większa niż jeden); jeśli podzielisz przez stałą k, osiągnięcie liczby 1 zajmie log k n kroków. Zatem każdy algorytm, który wielokrotnie dzieli rozmiar wejściowy przez pewien ułamek, będzie wymagał O (log n) iteracji do zakończenia. Te iteracje mogą zająć dużo czasu, więc czas wykonania netto nie musi wynosić O (log n), ale liczba kroków będzie logarytmiczna.
Więc gdzie to się dzieje? Jednym z klasycznych przykładów jest wyszukiwanie binarne , szybki algorytm wyszukiwania wartości w posortowanej tablicy. Algorytm działa w ten sposób:
- Jeśli tablica jest pusta, zwróć, że elementu nie ma w tablicy.
- Inaczej:
- Spójrz na środkowy element tablicy.
- Jeśli jest równy elementowi, którego szukamy, zwróć sukces.
- Jeśli jest większy niż element, którego szukamy:
- Wyrzuć drugą połowę tablicy.
- Powtarzać
- Jeśli jest mniejszy niż element, którego szukamy:
- Wyrzuć pierwszą połowę tablicy.
- Powtarzać
Na przykład, aby wyszukać 5 w tablicy
1 3 5 7 9 11 13
Najpierw przyjrzymy się środkowemu elementowi:
1 3 5 7 9 11 13
^
Ponieważ 7> 5, a tablica jest posortowana, wiemy na pewno, że liczba 5 nie może znajdować się w tylnej połowie tablicy, więc możemy ją po prostu odrzucić. To odchodzi
1 3 5
Spójrzmy teraz na środkowy element tutaj:
1 3 5
^
Ponieważ 3 <5, wiemy, że 5 nie może pojawić się w pierwszej połowie tablicy, więc możemy wrzucić pierwszą połowę tablicy, aby wyjść
5
Ponownie patrzymy na środek tej tablicy:
5
^
Ponieważ jest to dokładnie ta liczba, której szukamy, możemy zgłosić, że w tablicy rzeczywiście znajduje się 5.
Więc jak wydajne jest to? Cóż, przy każdej iteracji wyrzucamy co najmniej połowę pozostałych elementów tablicy. Algorytm zatrzymuje się, gdy tablica jest pusta lub gdy znajdziemy żądaną wartość. W najgorszym przypadku elementu nie ma, więc zmniejszamy o połowę rozmiar tablicy, aż skończą się elementy. Jak długo to trwa? Cóż, ponieważ w kółko przecinamy tablicę na pół, wykonamy co najwyżej O (log n) iteracji, ponieważ nie możemy przeciąć tablicy o połowę więcej niż O (log n) razy przed uruchomieniem poza elementami tablicy.
Algorytmy stosujące ogólną technikę dziel i rządź (dzielenie problemu na części, rozwiązywanie tych części, a następnie składanie go z powrotem) mają tendencję do posiadania wyrażeń logarytmicznych z tego samego powodu - nie możesz dalej przecinać jakiegoś obiektu pół więcej niż O (log n) razy. Możesz spojrzeć na sortowanie przez scalanie jako świetny przykład tego.
Przetwarzanie wartości po jednej cyfrze na raz
Ile cyfr składa się z liczby n o podstawie 10? Cóż, jeśli liczba zawiera k cyfr, wówczas największa cyfra jest wielokrotnością 10 tys . Największa liczba k-cyfr to 999 ... 9, k razy, a to jest równe 10 k + 1 - 1. W konsekwencji, jeśli wiemy, że n ma w sobie k cyfr, to wiemy, że wartość n wynosi co najwyżej 10 k + 1 - 1. Jeśli chcemy obliczyć k na podstawie n, otrzymamy
n ≤ 10 k + 1 - 1
n + 1 ≤ 10 k + 1
log 10 (n + 1) ≤ k + 1
(log 10 (n + 1)) - 1 ≤ k
Z którego otrzymujemy, że k jest w przybliżeniu logarytmem o podstawie 10 z n. Innymi słowy, liczba cyfr w n wynosi O (log n).
Na przykład pomyślmy o złożoności dodawania dwóch dużych liczb, które są zbyt duże, aby pasowały do słowa maszynowego. Załóżmy, że mamy te liczby reprezentowane przez podstawę 10 i nazwiemy liczby m i n. Jednym ze sposobów ich dodawania jest metoda podstawowa - zapisuj liczby po jednej cyfrze, a następnie pracuj od prawej do lewej. Na przykład, aby dodać 1337 i 2065, zaczniemy od zapisania liczb jako
1 3 3 7
+ 2 0 6 5
==============
Dodajemy ostatnią cyfrę i przenosimy 1:
1
1 3 3 7
+ 2 0 6 5
==============
2
Następnie dodajemy przedostatnią („przedostatnią”) cyfrę i przenosimy 1:
1 1
1 3 3 7
+ 2 0 6 5
==============
0 2
Następnie dodajemy przedostatnią („antepenultimate”) cyfrę:
1 1
1 3 3 7
+ 2 0 6 5
==============
4 0 2
Na koniec dodajemy przedostatnią cyfrę („preantepenultimate”… Uwielbiam angielski):
1 1
1 3 3 7
+ 2 0 6 5
==============
3 4 0 2
Ile pracy wykonaliśmy? W sumie wykonujemy O (1) pracy na cyfrę (czyli stałą ilość pracy), a jest O (max {log n, log m}) wszystkich cyfr, które muszą zostać przetworzone. Daje to w sumie złożoność O (max {log n, log m}), ponieważ musimy odwiedzić każdą cyfrę w dwóch liczbach.
Wiele algorytmów otrzymuje w nich człon O (log n), pracując po jednej cyfrze w jakiejś bazie. Klasycznym przykładem jest sortowanie radix , które sortuje liczby całkowite po jednej cyfrze. Istnieje wiele odmian sortowania radix, ale zwykle działają one w czasie O (n log U), gdzie U jest największą możliwą liczbą całkowitą, która jest sortowana. Powodem tego jest to, że każdy przebieg sortowania zajmuje O (n) czasu, a do przetworzenia każdej z O (log U) cyfr największej sortowanej liczby wymagane jest łącznie O (log U) iteracji. Wiele zaawansowanych algorytmów, takich jak algorytm najkrótszej ścieżki Gabowa lub skalująca wersja algorytmu maksymalnego przepływu Forda-Fulkersona , ma złożoność logarytmiczną, ponieważ działają one po jednej cyfrze na raz.
Jeśli chodzi o drugie pytanie dotyczące sposobu rozwiązania tego problemu, warto przyjrzeć się temu pokrewnemu pytaniu, które dotyczy bardziej zaawansowanej aplikacji. Biorąc pod uwagę ogólną strukturę problemów, które są tutaj opisane, możesz teraz lepiej myśleć o problemach, gdy wiesz, że w wyniku znajduje się termin dziennika, więc radziłbym nie patrzeć na odpowiedź, dopóki jej nie podasz jakaś myśl.
Mam nadzieję że to pomoże!
O(log n)
można postrzegać jako: Jeśli podwoisz rozmiar problemun
, Twój algorytm potrzebuje tylko stałej liczby kroków więcej.