Większość osób z dyplomem CS z pewnością wie, co stoi na Big O . Pomaga nam zmierzyć, jak dobrze skaluje się algorytm.
Ale jestem ciekaw, w jaki sposób możesz obliczyć lub zbliżenie złożoności algorytmów?
Większość osób z dyplomem CS z pewnością wie, co stoi na Big O . Pomaga nam zmierzyć, jak dobrze skaluje się algorytm.
Ale jestem ciekaw, w jaki sposób możesz obliczyć lub zbliżenie złożoności algorytmów?
Odpowiedzi:
Zrobię co w mojej mocy, aby wyjaśnić to tutaj na prostych zasadach, ale ostrzegam, że ten temat zajmuje moim studentom kilka miesięcy, aby w końcu zrozumieć. Więcej informacji można znaleźć w rozdziale 2 książki Struktury i algorytmy danych w języku Java .
Nie ma mechanicznej procedury, która mogłaby zostać wykorzystana do uzyskania BigOh.
Jako „książka kucharska”, aby uzyskać BigOh z fragmentu kodu, musisz najpierw zdać sobie sprawę, że tworzysz formułę matematyczną, aby policzyć, ile kroków obliczeń zostanie wykonanych przy danych wejściowych o pewnym rozmiarze.
Cel jest prosty: porównać algorytmy z teoretycznego punktu widzenia, bez konieczności wykonywania kodu. Im mniejsza liczba kroków, tym szybszy algorytm.
Załóżmy na przykład, że masz ten fragment kodu:
Ta funkcja zwraca sumę wszystkich elementów tablicy, a my chcemy stworzyć formułę liczącą złożoność obliczeniową tej funkcji:
Mamy więc
f(N)
funkcję zliczającą liczbę kroków obliczeniowych. Dane wejściowe funkcji to rozmiar struktury do przetworzenia. Oznacza to, że ta funkcja jest wywoływana, na przykład:Parametr
N
przyjmujedata.length
wartość. Teraz potrzebujemy faktycznej definicji funkcjif()
. Odbywa się to z kodu źródłowego, w którym każdy interesujący wiersz jest ponumerowany od 1 do 4.Istnieje wiele sposobów obliczania BigOh. Od tego momentu zakładamy, że każde zdanie, które nie zależy od wielkości danych wejściowych, wymaga
C
obliczeń o stałej liczbie.Dodamy indywidualną liczbę kroków funkcji i ani deklaracja zmiennej lokalnej, ani instrukcja return nie zależy od wielkości
data
tablicy.Oznacza to, że każdy wiersz 1 i 4 zajmuje C liczby kroków, a funkcja wygląda mniej więcej tak:
Następną częścią jest zdefiniowanie wartości
for
instrukcji. Pamiętaj, że liczymy liczbę kroków obliczeniowych, co oznacza, że treśćfor
instrukcji jest wykonywanaN
razy. To tak samo jak dodawanieC
,N
czasy:Nie ma mechanicznej reguły liczącej, ile razy
for
wykonywana jest treść operacji , musisz ją policzyć, sprawdzając, co robi kod. Aby uprościć obliczenia, ignorujemy części inicjalizacji zmiennej, warunek i części przyrostowefor
instrukcji.Aby uzyskać rzeczywistą wartość BigOh, potrzebujemy analizy asymptotycznej funkcji. Robi się to mniej więcej tak:
C
.f()
uzyskać polynomium w swojejstandard form
.N
zbliżainfinity
.Nasz
f()
ma dwa terminy:Usuwanie wszystkich
C
stałych i zbędnych części:Ponieważ ostatni termin jest tym, który rośnie, gdy
f()
zbliża się do nieskończoności (pomyśl o limitach ), jest to argument BigOh, asum()
funkcja ma BigOh o wartości:Istnieje kilka sztuczek, aby rozwiązać niektóre trudne: użyj podsumowań, kiedy tylko możesz.
Na przykład ten kod można łatwo rozwiązać za pomocą podsumowań:
Pierwszą rzeczą, o którą musisz zapytać, jest kolejność wykonania
foo()
. Choć zwykle tak jestO(1)
, musisz zapytać o to swoich profesorów.O(1)
oznacza (prawie głównie) stałąC
, niezależną od wielkościN
.for
Oświadczenie o jeden numer zdanie jest trudne. Podczas gdy indeks kończy się na2 * N
, przyrost jest wykonywany przez dwa. Oznacza to, że pierwszyfor
wykonywany jest tylkoN
krok i musimy podzielić liczbę przez dwa.Zdanie numer dwa jest jeszcze trudniejsze, ponieważ zależy od wartości
i
. Spójrz: indeks i przyjmuje wartości: 0, 2, 4, 6, 8, ..., 2 * N, a drugifor
wykonuje się: N razy pierwszy, N - 2 drugi, N - 4 trzeci ... aż do etapu N / 2, na którym drugifor
nigdy nie zostanie wykonany.W formule oznacza to:
Ponownie liczymy liczbę kroków . I z definicji każde sumowanie powinno zawsze zaczynać się od jednego i kończyć liczbą większą lub równą niż jeden.
(Zakładamy, że tak
foo()
jestO(1)
i podejmujeC
kroki.)Mamy tutaj problem: kiedy
i
wznosi się wartość wN / 2 + 1
górę, wewnętrzne Podsumowanie kończy się liczbą ujemną! To niemożliwe i złe. Musimy podzielić podsumowanie na dwie części, ponieważ jest to najważniejszy moment, jakii
zajmuje ta chwilaN / 2 + 1
.Od momentu kluczowego momentu
i > N / 2
wnętrzefor
nie zostanie wykonane, a my zakładamy stałą złożoność wykonania C na jego ciele.Teraz podsumowania można uprościć, stosując pewne reguły tożsamości:
w
)Stosowanie algebry:
A BigOh to:
źródło
O(n)
gdzien
jest liczba elementów lubO(x*y)
gdziex
i gdziey
są wymiary tablicy. Big-oh jest „względne w stosunku do danych wejściowych”, więc zależy od tego, jaki jest twój wkład.Duże O określa górną granicę złożoności algorytmu w czasie. Jest zwykle używany w połączeniu z przetwarzaniem zestawów danych (list), ale może być używany w innym miejscu.
Kilka przykładów użycia w kodzie C.
Powiedzmy, że mamy tablicę n elementów
Gdybyśmy chcieli uzyskać dostęp do pierwszego elementu tablicy, byłby to O (1), ponieważ nie ma znaczenia, jak duża jest tablica, zawsze uzyskanie tego samego czasu zajmuje tyle samo czasu.
Jeśli chcielibyśmy znaleźć numer na liście:
To byłoby O (n), ponieważ co najwyżej musielibyśmy przejrzeć całą listę, aby znaleźć nasz numer. Big-O to wciąż O (n), chociaż możemy znaleźć nasz numer przy pierwszej próbie przejścia przez pętlę raz, ponieważ Big-O opisuje górną granicę algorytmu (omega jest dla dolnej granicy, a theta jest dla ścisłej granicy) .
Kiedy dojdziemy do zagnieżdżonych pętli:
Jest to O (n ^ 2), ponieważ dla każdego przejścia zewnętrznej pętli (O (n)) musimy ponownie przejść przez całą listę, aby liczba mnożała się n, pozostawiając nam kwadrat n.
To ledwo zarysowanie powierzchni, ale kiedy dochodzi do analizy bardziej złożonych algorytmów, w grę wchodzi złożona matematyka obejmująca dowody. Mam nadzieję, że przynajmniej zapozna się z podstawami.
źródło
O(1)
działać same. Na przykład w standardowych interfejsach API Cbsearch
jest z naturyO(log n)
,strlen
jestO(n)
iqsort
jestO(n log n)
(technicznie nie ma żadnych gwarancji, a sam quicksort ma najgorszą złożoność przypadkuO(n²)
, ale zakładając, żelibc
autor nie jest kretynem, jego średnia złożoność jestO(n log n)
i używa strategia wyboru osi obrotu, która zmniejsza prawdopodobieństwo trafienia wO(n²)
skrzynkę). I obabsearch
iqsort
może być gorzej, jeśli funkcja komparator jest patologiczny.Chociaż wiedza, jak obliczyć czas Wielkiego O dla konkretnego problemu, jest przydatna, znajomość niektórych ogólnych przypadków może znacznie pomóc w podejmowaniu decyzji dotyczących algorytmu.
Oto niektóre z najczęstszych przypadków, które można znaleźć w http://en.wikipedia.org/wiki/Big_O_notation#Orders_of_common_functions :
O (1) - Ustalanie, czy liczba jest parzysta czy nieparzysta; przy użyciu tabeli przeglądowej lub tabeli mieszającej o stałym rozmiarze
O (logn) - Wyszukiwanie elementu w posortowanej tablicy za pomocą wyszukiwania binarnego
O (n) - Znalezienie elementu na nieposortowanej liście; dodając dwie liczby n-cyfrowe
O (n 2 ) - Mnożenie dwóch liczb n-cyfrowych przez prosty algorytm; dodanie dwóch macierzy n × n; sortowanie bąbelkowe lub wstawianie
O (n 3 ) - Mnożenie dwóch macierzy n × n przez prosty algorytm
O (c n ) - Znalezienie (dokładnego) rozwiązania problemu podróżnego sprzedawcy za pomocą programowania dynamicznego; ustalenie, czy dwa zdania logiczne są równoważne przy użyciu brutalnej siły
O (n!) - Rozwiązanie problemu podróżującego sprzedawcy za pomocą wyszukiwania z użyciem siły
O (n n ) - Często używane zamiast O (n!) W celu uzyskania prostszych wzorów dla asymptotycznej złożoności
źródło
x&1==1
aby sprawdzić dziwność?x & 1
, nie trzeba sprawdzać== 1
; w Cx&1==1
jest oceniany jakox&(1==1)
dzięki pierwszeństwu operatora , więc w rzeczywistości jest taki sam jak testowaniex&1
). Myślę, że źle rozumiesz odpowiedź; jest tam średnik, a nie przecinek. Nie oznacza to, że potrzebujesz tabeli przeglądowej do testowania parzystego / nieparzystego, to znaczy, że zarówno parzyste / nieparzyste testowanie, jak i sprawdzanie tabeli przeglądowej toO(1)
operacje.Małe przypomnienie:
big O
notacja służy do oznaczenia asymptotycznej złożoności (to znaczy, gdy rozmiar problemu rośnie do nieskończoności) i ukrywa stałą.Oznacza to, że między algorytmem w O (n) a jednym w O (n 2 ) najszybszym nie zawsze jest pierwszy (choć zawsze istnieje wartość n taka, że dla problemów z rozmiarem> n pierwszy algorytm jest najszybszy).
Zauważ, że ukryta stała bardzo zależy od implementacji!
Ponadto w niektórych przypadkach środowisko wykonawcze nie jest funkcją deterministyczną wielkości n danych wejściowych. Weźmy na przykład sortowanie za pomocą szybkiego sortowania: czas potrzebny na posortowanie tablicy n elementów nie jest stały, ale zależy od początkowej konfiguracji tablicy.
Istnieją różne zawiłości czasowe:
Przeciętny przypadek (zwykle znacznie trudniejszy do zrozumienia ...)
...
Dobrym wprowadzeniem jest Wprowadzenie do analizy algorytmów autorstwa R. Sedgewicka i P. Flajoleta.
Jak mówisz,
premature optimisation is the root of all evil
i (jeśli to możliwe) profilowanie naprawdę powinno zawsze być używane podczas optymalizacji kodu. Może nawet pomóc w określeniu złożoności algorytmów.źródło
Widząc odpowiedzi tutaj, myślę, że możemy dojść do wniosku, że większość z nas rzeczywiście przybliża porządek algorytmu, patrząc na niego i kierując się zdrowym rozsądkiem zamiast obliczać go na przykład za pomocą metody mistrzowskiej, jak myśleliśmy na uniwersytecie. Powiedziawszy to, muszę dodać, że nawet profesor zachęcił nas (później), abyśmy się nad tym zastanowili , a nie tylko obliczali.
Chciałbym również dodać, jak to się robi dla funkcji rekurencyjnych :
załóżmy, że mamy funkcję typu ( kod schematu ):
która rekurencyjnie oblicza silnię podanej liczby.
Pierwszym krokiem jest próba określenia charakterystyki wydajności dla ciała funkcji tylko w tym przypadku, nic specjalnego nie jest robione w ciele, tylko pomnożenie (lub zwrócenie wartości 1).
Zatem wydajność dla ciała wynosi: O (1) (stała).
Następnie spróbuj ustalić to dla liczby połączeń rekurencyjnych . W tym przypadku mamy n-1 połączeń rekurencyjnych.
Zatem wydajność dla wywołań rekurencyjnych jest następująca: O (n-1) (kolejność wynosi n, gdy wyrzucamy nieistotne części).
Następnie połącz te dwa elementy i uzyskasz wydajność dla całej funkcji rekurencyjnej:
1 * (n-1) = O (n)
Piotr , aby odpowiedzieć na twoje poruszone problemy; metoda, którą tu opisuję, radzi sobie całkiem dobrze. Pamiętaj jednak, że wciąż jest to przybliżenie, a nie pełna matematycznie poprawna odpowiedź. Opisana tutaj metoda jest również jedną z metod, których nauczono nas na uniwersytecie, i jeśli dobrze pamiętam, została użyta w znacznie bardziej zaawansowanych algorytmach niż silnia, którą zastosowałem w tym przykładzie.
Oczywiście wszystko zależy od tego, jak dobrze możesz oszacować czas działania treści funkcji i liczbę wywołań rekurencyjnych, ale tak samo jest w przypadku innych metod.
źródło
Jeśli twój koszt jest wielomianem, po prostu zachowaj termin najwyższego rzędu, bez jego mnożnika. Na przykład:
Pamiętaj, że to nie działa w przypadku nieskończonej serii. Nie ma jednego przepisu na ogólny przypadek, chociaż w niektórych powszechnych przypadkach obowiązują następujące nierówności:
źródło
Myślę o tym w kategoriach informacji. Każdy problem polega na nauce określonej liczby bitów.
Twoim podstawowym narzędziem jest koncepcja punktów decyzyjnych i ich entropia. Entropia punktu decyzyjnego jest średnią informacją, którą ci da. Na przykład, jeśli program zawiera punkt decyzyjny z dwiema gałęziami, jego entropia jest sumą prawdopodobieństwa każdej gałęzi razy log 2 odwrotnego prawdopodobieństwa tej gałęzi. Tak dużo się uczysz, wykonując tę decyzję.
Na przykład
if
instrukcja posiadająca dwie gałęzie, obie jednakowo prawdopodobne, ma entropię 1/2 * log (2/1) + 1/2 * log (2/1) = 1/2 * 1 + 1/2 * 1 = 1. Więc jego entropia wynosi 1 bit.Załóżmy, że przeszukujesz tabelę N elementów, na przykład N = 1024. To jest problem 10-bitowy, ponieważ log (1024) = 10 bitów. Jeśli więc możesz przeszukać go za pomocą instrukcji IF, które mają równie prawdopodobne wyniki, powinien podjąć 10 decyzji.
To właśnie zyskujesz dzięki wyszukiwaniu binarnemu.
Załóżmy, że przeprowadzasz wyszukiwanie liniowe. Patrzysz na pierwszy element i pytasz, czy to ten, którego chcesz. Prawdopodobieństwa to 1/1024, a 1023/1024, że tak nie jest. Entropia tej decyzji to 1/1024 * log (1024/1) + 1023/1024 * log (1024/1023) = 1/1024 * 10 + 1023/1024 * około 0 = około 0,01 bitu. Nauczyłeś się bardzo mało! Druga decyzja nie jest dużo lepsza. Właśnie dlatego wyszukiwanie liniowe jest tak wolne. W rzeczywistości jest to wykładnicza liczba bitów, której musisz się nauczyć.
Załóżmy, że indeksujesz. Załóżmy, że tabela jest wstępnie posortowana na wiele koszy i że używasz niektórych wszystkich bitów w kluczu, aby indeksować bezpośrednio do pozycji tabeli. Jeśli istnieje 1024 pojemników, entropia wynosi 1/1024 * log (1024) + 1/1024 * log (1024) + ... dla wszystkich 1024 możliwych wyników. Jest to 1/1024 * 10 razy 1024 wyników lub 10 bitów entropii dla tej jednej operacji indeksowania. Dlatego wyszukiwanie indeksowania jest szybkie.
Pomyśl teraz o sortowaniu. Masz N przedmiotów i masz listę. Dla każdego elementu należy wyszukać miejsce, w którym element znajduje się na liście, a następnie dodać go do listy. Sortowanie zajmuje więc około N-krotność liczby kroków podstawowego wyszukiwania.
Tak więc sortowanie oparte na decyzjach binarnych, które mają z grubsza jednakowo prawdopodobne wyniki, wymaga wykonania kroków O (N log N). Algorytm sortowania O (N) jest możliwy, jeśli jest oparty na wyszukiwaniu indeksowym.
Przekonałem się, że prawie wszystkie problemy z wydajnością algorytmu można rozpatrywać w ten sposób.
źródło
Zacznijmy od początku.
Przede wszystkim zaakceptuj zasadę, że pewne proste operacje na danych można wykonać w
O(1)
czasie, to znaczy w czasie niezależnym od wielkości danych wejściowych. Te prymitywne operacje w C składają się zUzasadnienie tej zasady wymaga szczegółowego przestudiowania instrukcji maszyny (prymitywnych kroków) typowego komputera. Każdą z opisanych operacji można wykonać za pomocą niewielkiej liczby instrukcji maszyny; często potrzebna jest tylko jedna lub dwie instrukcje. W konsekwencji kilka rodzajów instrukcji w C może być wykonywanych w
O(1)
czasie, to znaczy w pewnym stałym czasie niezależnym od danych wejściowych. Te proste obejmująW C powstaje wiele pętli for, inicjując zmienną indeksu do pewnej wartości i zwiększając tę zmienną o 1 za każdym razem wokół pętli. Pętla for kończy się, gdy indeks osiągnie pewien limit. Na przykład pętla for
używa zmiennej indeksu i. Inkrementuje i o 1 za każdym razem wokół pętli, a iteracje zatrzymują się, gdy osiągnę n - 1.
Jednak na razie skupmy się na prostej formie pętli for, w której różnica między wartościami końcowymi i początkowymi podzielona przez kwotę, o którą zmienna indeksowa jest zwiększana, mówi nam, ile razy ominęliśmy pętlę . Ta liczba jest dokładna, chyba że istnieją sposoby na wyjście z pętli za pomocą instrukcji skoku; w każdym razie jest to górna granica liczby iteracji.
Na przykład iteracja pętli for
((n − 1) − 0)/1 = n − 1 times
, ponieważ 0 jest wartością początkową i, n - 1 jest najwyższą wartością osiągniętą przez i (tj. Gdy i osiągnie n-1, pętla zatrzymuje się i nie występuje iteracja z i = n− 1) i 1 dodaje się do i przy każdej iteracji pętli.W najprostszym przypadku, gdy czas spędzony w ciele pętli jest taki sam dla każdej iteracji, możemy pomnożyć górną granicę dużego oh ciała dla liczby razy wokół pętli . Ściśle mówiąc, musimy dodać czas O (1) do zainicjowania indeksu pętli i czas O (1) do pierwszego porównania indeksu pętli z limitem , ponieważ testujemy jeszcze raz, niż obejdziemy pętlę. Jednakże, chyba że nie można wykonać pętli zero razy, czas zainicjowania pętli i jednorazowego przetestowania limitu jest terminem niskiego rzędu, który można usunąć za pomocą reguły sumowania.
Rozważmy teraz ten przykład:
Wiemy, że linia (1) wymaga
O(1)
czasu. Oczywiście, omijamy pętlę n razy, co możemy ustalić, odejmując dolną granicę od górnej granicy znalezionej na linii (1), a następnie dodając 1. Ponieważ ciało, linia (2) zajmuje czas O (1), możemy pominąć czas do przyrostu j oraz czas do porównania jz, z których oba są również O (1). Zatem czas przebiegu linii (1) i (2) jest iloczynem n i O (1) , co oznaczaO(n)
.Podobnie możemy ograniczyć czas działania zewnętrznej pętli składającej się z linii (2) do (4), czyli
Ustaliliśmy już, że pętla linii (3) i (4) zajmuje czas O (n). Możemy zatem zaniedbać czas O (1), aby zwiększyć i i sprawdzić, czy i <n w każdej iteracji, stwierdzając, że każda iteracja zewnętrznej pętli zajmuje czas O (n).
Inicjalizacja i = 0 zewnętrznej pętli i (n + 1) test warunku i <n podobnie zajmują czas O (1) i można je pominąć. Na koniec obserwujemy, że n razy omijamy zewnętrzną pętlę, zajmując czas O (n) dla każdej iteracji, dając całkowity
O(n^2)
czas działania.Bardziej praktyczny przykład.
źródło
Jeśli chcesz empirycznie oszacować kolejność kodu, nie analizując go, możesz zastosować szereg rosnących wartości n i czasu kodu. Wykreśl swoje czasy na skali dziennika. Jeśli kod to O (x ^ n), wartości powinny spaść na linii nachylenia n.
Ma to kilka zalet w porównaniu z samym studiowaniem kodu. Po pierwsze, możesz sprawdzić, czy jesteś w zakresie, w którym czas działania zbliża się do swojej asymptotycznej kolejności. Może się również okazać, że jakiś kod, który według ciebie był porządkiem O (x), jest tak naprawdę porządkiem O (x ^ 2), na przykład z powodu czasu spędzonego na wywołaniach biblioteki.
źródło
Zasadniczo rzeczą, która pojawia się w 90% przypadków, jest po prostu analiza pętli. Czy masz pojedyncze, podwójne, potrójne zagnieżdżone pętle? Masz czas pracy O (n), O (n ^ 2), O (n ^ 3).
Bardzo rzadko (chyba że piszesz platformę z rozbudowaną biblioteką podstawową (np. .NET BCL lub STL C ++) napotkasz wszystko, co jest trudniejsze niż tylko patrzenie na twoje pętle (dla instrukcji, podczas gdy goto, itp...)
źródło
Notacja Big O jest przydatna, ponieważ jest łatwa w obsłudze i ukrywa niepotrzebne komplikacje i szczegóły (w przypadku niektórych definicji niepotrzebnych). Jednym z fajnych sposobów obliczania złożoności algorytmów dzielenia i zdobywania jest metoda drzewa. Załóżmy, że masz wersję Quicksort z procedurą mediany, więc za każdym razem dzielisz tablicę na idealnie zrównoważone pod-tablice.
Teraz zbuduj drzewo odpowiadające wszystkim tablicom, z którymi pracujesz. W katalogu głównym masz oryginalną tablicę, katalog główny ma dwoje potomków, które są podobrazami. Powtarzaj tę czynność, dopóki na dole nie będzie tablic jednoelementowych.
Ponieważ możemy znaleźć medianę w czasie O (n) i podzielić tablicę na dwie części w czasie O (n), praca wykonana w każdym węźle to O (k), gdzie k jest rozmiarem tablicy. Każdy poziom drzewa zawiera (co najwyżej) całą tablicę, więc praca na poziomie wynosi O (n) (rozmiary podcieni sumują się do n, a ponieważ mamy O (k) na poziom, możemy to dodać) . W drzewie są tylko poziomy log (n), ponieważ za każdym razem zmniejszamy o połowę dane wejściowe.
Dlatego możemy górną granicę ilości pracy o O (n * log (n)).
Jednak Big O ukrywa pewne szczegóły, których czasami nie możemy zignorować. Rozważ obliczenie sekwencji Fibonacciego za pomocą
i załóżmy, że a i b to BigIntegers w Javie lub coś, co może obsłużyć dowolnie duże liczby. Większość ludzi powiedziałaby, że jest to algorytm O (n) bez wzdrygnięcia. Powodem jest to, że masz iteracje w pętli for, a O (1) działa po stronie pętli.
Ale liczby Fibonacciego są duże, n-ta liczba Fibonacciego jest wykładnicza in, więc po prostu przechowywanie przyjmie kolejność n bajtów. Wykonanie dodawania z dużymi liczbami całkowitymi zajmie O (n) ilość pracy. Tak więc łączna ilość pracy wykonanej w tej procedurze wynosi
1 + 2 + 3 + ... + n = n (n-1) / 2 = O (n ^ 2)
Tak więc ten algorytm działa w czasie kwadratowym!
źródło
Myślę, że ogólnie mniej przydatny, ale ze względu na kompletność istnieje również Big Omega Ω , która określa dolną granicę złożoności algorytmu, i Big Theta Θ , która określa zarówno górną, jak i dolną granicę.
źródło
Podziel algorytm na części, dla których znasz dużą notację O, i połącz za pomocą dużych operatorów O. To jedyny znany mi sposób.
Aby uzyskać więcej informacji, sprawdź stronę Wikipedii na ten temat.
źródło
Znajomość używanych algorytmów / struktur danych i / lub szybka analiza zagnieżdżania iteracji. Trudność polega na tym, że wywołujesz funkcję biblioteki, być może wiele razy - często nie masz pewności, czy czasami wywołujesz funkcję niepotrzebnie, czy też jakiej implementacji używają. Być może funkcje biblioteczne powinny mieć miarę złożoności / wydajności, niezależnie od tego, czy jest to Big O, czy inna metryka, która jest dostępna w dokumentacji, a nawet IntelliSense .
źródło
Jeśli chodzi o „jak obliczyć” Big O, jest to część teorii złożoności obliczeniowej . W niektórych (wielu) szczególnych przypadkach możesz uzyskać prostą heurystykę (np. Mnożenie liczby pętli dla zagnieżdżonych pętli), szczególnie. kiedy wszystko, czego chcesz, to jakiekolwiek oszacowanie górnej granicy i nie masz nic przeciwko, jeśli jest to zbyt pesymistyczne - co, jak sądzę, prawdopodobnie jest tym, o co ci chodzi.
Jeśli naprawdę chcesz odpowiedzieć na pytanie dotyczące dowolnego algorytmu, najlepiej możesz zastosować teorię. Oprócz uproszczonej analizy „najgorszego przypadku” analiza amortyzowana jest bardzo przydatna w praktyce.
źródło
W pierwszym przypadku wewnętrzna pętla jest wykonywana
n-i
razy, więc całkowita liczba wykonań jest sumąi
przejścia od0
don-1
(ponieważ niższa, nie mniejsza niż lub równa) zn-i
. Dostajesz w końcun*(n + 1) / 2
, więcO(n²/2) = O(n²)
.Dla drugiej pętli
i
znajduje się pomiędzy0
i jestn
uwzględniony dla zewnętrznej pętli; wtedy pętla wewnętrzna jest wykonywana, gdyj
jest ściśle większa niżn
, co jest wówczas niemożliwe.źródło
Oprócz korzystania z metody master (lub jednej z jej specjalizacji) testuję algorytmy eksperymentalnie. Nie może to udowodnić, że osiągnięto określoną klasę złożoności, ale może zapewnić pewność, że analiza matematyczna jest odpowiednia. Aby pomóc w tym zapewnieniu, używam narzędzi pokrycia kodu w połączeniu z moimi eksperymentami, aby upewnić się, że wykonuję wszystkie przypadki.
Jako bardzo prosty przykład powiedz, że chciałeś przeprowadzić kontrolę poczytalności dotyczącą szybkości sortowania listy .NET Framework. Możesz napisać coś takiego, a następnie przeanalizować wyniki w programie Excel, aby upewnić się, że nie przekraczają krzywej n * log (n).
W tym przykładzie mierzę liczbę porównań, ale rozsądnie jest też zbadać rzeczywisty czas wymagany dla każdej wielkości próbki. Jednak musisz być jeszcze bardziej ostrożny, gdy tylko mierzysz algorytm, nie uwzględniając artefaktów z infrastruktury testowej.
źródło
Nie zapomnij również uwzględnić złożoności przestrzeni, która może być również powodem do niepokoju, jeśli ktoś ma ograniczone zasoby pamięci. Na przykład możesz usłyszeć, że ktoś chce stałego algorytmu przestrzeni, co w zasadzie mówi, że ilość miejsca zajmowanego przez algorytm nie zależy od żadnych czynników wewnątrz kodu.
Czasami złożoność może wynikać z tego, ile razy coś się nazywa, jak często wykonywana jest pętla, jak często przydzielana jest pamięć, i tak dalej jest inna część odpowiedzi na to pytanie.
Wreszcie, duże O może być użyte w najgorszym przypadku, najlepszym przypadku i przypadkach amortyzacji, gdzie ogólnie jest to najgorszy przypadek używany do opisania, jak zły może być algorytm.
źródło
Często pomijane jest oczekiwane zachowanie algorytmów. Nie zmienia Big-O twojego algorytmu , ale odnosi się do stwierdzenia „przedwczesna optymalizacja ...”
Oczekiwane zachowanie twojego algorytmu jest - bardzo głupie - jak szybko możesz oczekiwać, że Twój algorytm będzie działał na danych, które najprawdopodobniej zobaczysz.
Na przykład, jeśli szukasz wartości na liście, jest to O (n), ale jeśli wiesz, że większość list, które widzisz, ma swoją wartość z góry, typowe zachowanie algorytmu jest szybsze.
Aby naprawdę to ustalić, musisz być w stanie opisać rozkład prawdopodobieństwa „przestrzeni wejściowej” (jeśli musisz posortować listę, jak często ta lista będzie już posortowana? Jak często jest całkowicie odwracana? W jaki sposób często jest to najczęściej posortowane?) Nie zawsze jest to możliwe, że o tym wiesz, ale czasami tak jest.
źródło
świetne pytanie!
Zastrzeżenie: ta odpowiedź zawiera fałszywe stwierdzenia, patrz komentarze poniżej.
Jeśli używasz Big O, mówisz o najgorszym przypadku (więcej o tym, co to znaczy później). Dodatkowo istnieje kapitał theta dla przeciętnego przypadku i duża omega dla najlepszego przypadku.
Sprawdź tę stronę, aby uzyskać piękną formalną definicję Big O: https://xlinux.nist.gov/dads/HTML/bigOnotation.html
Ok, więc co teraz rozumiemy przez złożoność „najlepszego przypadku” i „najgorszego przypadku”?
Prawdopodobnie najlepiej to ilustrują przykłady. Na przykład, jeśli używamy wyszukiwania liniowego do znalezienia liczby w posortowanej tablicy, najgorszym przypadkiem jest decyzja o poszukiwaniu ostatniego elementu tablicy, ponieważ zajęłoby to tyle kroków, ile jest elementów w tablicy. Najlepszym wypadku będzie, gdy szukamy w pierwszym elemencie , ponieważ chcemy być wykonane po pierwszym czeku.
Istotą wszystkich tych przymiotników -złożoności jest to, że szukamy sposobu na wykres czasu, przez jaki hipotetyczny program biegnie do końca, pod względem wielkości poszczególnych zmiennych. Jednak w przypadku wielu algorytmów można argumentować, że nie ma czasu dla określonego rozmiaru danych wejściowych. Zauważ, że jest to sprzeczne z podstawowym wymogiem funkcji, każde wejście powinno mieć nie więcej niż jedno wyjście. Dlatego opracowaliśmy wiele funkcji opisujących złożoność algorytmu. Teraz, nawet jeśli wyszukiwanie tablicy o rozmiarze n może zająć różny czas w zależności od tego, czego szukasz w tablicy i proporcjonalnie do n, możemy stworzyć pouczający opis algorytmu przy użyciu najlepszego przypadku, średniej wielkości i najgorsze klasy.
Niestety jest to tak źle napisane i brakuje w nim wielu informacji technicznych. Ale miejmy nadzieję, że ułatwi to zastanowienie się nad klasami złożoności czasu. Kiedy już poczujesz się z nimi komfortowo, staje się prostą kwestią analizowania programu i szukania takich rzeczy, jak pętle for, które zależą od rozmiarów tablic i wnioskowania na podstawie struktur danych, jaki rodzaj danych wejściowych spowodowałby trywialne przypadki i jakie dane wejściowe spowodowałyby w najgorszych przypadkach.
źródło
Nie wiem, jak programowo rozwiązać ten problem, ale pierwszą rzeczą, którą ludzie robią, jest to, że próbkujemy algorytm dla pewnych wzorców w liczbie wykonanych operacji, powiedzmy 4n ^ 2 + 2n + 1 mamy 2 reguły:
Jeśli uprościć f (x), gdzie f (x) jest wzorem na liczbę wykonanych operacji ((4n ^ 2 + 2n + 1 wyjaśnione powyżej), otrzymujemy w tym przypadku dużą wartość O [O (n ^ 2) walizka]. Musiałoby to jednak uwzględniać interpolację Lagrange'a w programie, co może być trudne do wdrożenia. A jeśli prawdziwą dużą wartością O byłoby O (2 ^ n), a moglibyśmy mieć coś takiego jak O (x ^ n), więc ten algorytm prawdopodobnie nie byłby programowalny. Ale jeśli ktoś udowodni, że się mylę, daj mi kod. . . .
źródło
W przypadku kodu A zewnętrzna pętla będzie wykonywana
n+1
razy, czas „1” oznacza proces, który sprawdza, czy nadal spełniam wymaganie. A wewnętrzna pętla działan
razy,n-2
razy ... Tak więc0+2+..+(n-2)+n= (0+n)(n+1)/2= O(n²)
.W przypadku kodu B, chociaż pętla wewnętrzna nie wkroczyłaby i nie wykona foo (), pętla wewnętrzna zostanie wykonana, ponieważ n razy zależy od czasu wykonania pętli zewnętrznej, czyli O (n)
źródło
Chciałbym wyjaśnić Big-O w nieco innym aspekcie.
Big-O ma po prostu porównać złożoność programów, co oznacza, jak szybko rosną one, gdy rosną nakłady, a nie dokładny czas, jaki poświęca się na działanie.
IMHO we wzorach big-O lepiej nie używać bardziej złożonych równań (możesz po prostu trzymać się tych na poniższym wykresie). Jednak nadal możesz użyć innej, bardziej precyzyjnej formuły (np. 3 ^ n, n ^ 3, .. .), ale więcej niż to może czasem wprowadzać w błąd! Więc lepiej, aby było to tak proste, jak to możliwe.
Chciałbym jeszcze raz podkreślić, że tutaj nie chcemy uzyskać dokładnej formuły dla naszego algorytmu. Chcemy tylko pokazać, jak rośnie, gdy rosną nakłady, i porównać z innymi algorytmami w tym sensie. W przeciwnym razie lepiej skorzystaj z różnych metod, takich jak benchmarking.
źródło