Określanie złożoności podanych kodów

Biorąc pod uwagę fragment kodu, jak określisz złożoność w ogóle. Jestem bardzo zmieszany z dużymi pytaniami O. Na przykład bardzo proste pytanie:

for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        System.out.println("*");
    }
}

TA wyjaśnił to czymś w rodzaju kombinacji. Jak to jest N Wybierz 2 = (n (n-1))/2 = N^2 + 0,5, następnie usuń stałą, aby stała stała się n^2. Mogę wprowadzić wartości testowe int i spróbować, ale w jaki sposób wchodzi ta kombinacja?

Co jeśli jest deklaracja if? Jak wygląda złożoność zdeterminowany?

for (int i = 0; i < n; i++) {
    if (i % 2 ==0) {
        for (int j = i; j < n; j++) { ... }
    } else {
        for (int j = 0; j < i; j++) { ... }
    }
}

Więc co z rekurencją ...

int fib(int a, int b, int n) {
    if (n == 3) {
        return a + b;
    } else {
        return fib(b, a+b, n-1);
    }
}
Author: prapin, 2011-10-25

6 answers

ogólnie nie ma sposobu na określenie złożoności danej funkcji

Uwaga! Ściana tekstu nadchodzi!

1. Istnieją bardzo proste algorytmy, których nikt nie wie, czy w ogóle się zatrzymują, czy nie.

Nie istnieje żaden algorytm , który może zdecydować, czy dany program zatrzyma się, czy nie, jeśli podano określone dane wejściowe. Obliczanie złożoności obliczeniowej jest jeszcze trudniejszym problemem, ponieważ nie tylko musimy udowodnić, że algorytm zatrzymuje się, ale musimy udowodnić Jak szybko to robi.

//The Collatz conjecture states that the sequence generated by the following
// algorithm always reaches 1, for any initial positive integer. It has been
// an open problem for 70+ years now.
function col(n){
    if (n == 1){
        return 0;
    }else if (n % 2 == 0){ //even
        return 1 + col(n/2);
    }else{ //odd
        return 1 + col(3*n + 1);
    }
}

2. niektóre algorytmy mają dziwne i nietypowe złożoności

Ogólny "schemat określania złożoności" łatwo stałby się zbyt skomplikowany z powodu tych facetów.]}
//The Ackermann function. One of the first examples of a non-primitive-recursive algorithm.
function ack(m, n){
    if(m == 0){
        return n + 1;
    }else if( n == 0 ){
        return ack(m-1, 1);
    }else{
        return ack(m-1, ack(m, n-1));
    }
}

function f(n){ return ack(n, n); }

//f(1) = 3
//f(2) = 7
//f(3) = 61
//f(4) takes longer then your wildest dreams to terminate.

3. niektóre funkcje są bardzo proste, ale mylą wiele rodzajów prób analizy statycznej

//Mc'Carthy's 91 function. Try guessing what it does without
// running it or reading the Wikipedia page ;)
function f91(n){
    if(n > 100){
        return n - 10;
    }else{
        return f91(f91(n + 11));
    }
}

To powiedziawszy, wciąż potrzebujemy sposobu, aby znaleźć złożoność rzeczy, prawda? Na pętle są prostym i powszechnym wzorem. Weźmy przykład:
for(i=0; i<N; i++){
   for(j=0; j<i; j++){
       print something
   }
}

Ponieważ każda print something jest O(1), złożoność czasowa algorytmu będzie określona przez to, ile razy uruchomimy tę linię. Jak wspomniała pani asystentka, robimy to, patrząc na kombinacje w tym przypadku. Pętla wewnętrzna będzie działać (N + (N-1)+... + 1) razy, w sumie (N+1)*N / 2.

Ponieważ pomijamy stałe otrzymujemy O (N2).

Teraz w bardziej skomplikowanych przypadkach możemy uzyskać więcej matematyczne. Spróbuj utworzyć funkcję, której wartość odpowiada długości działania algorytmu, biorąc pod uwagę rozmiar N wejścia. często możemy skonstruować rekurencyjną wersję tej funkcji bezpośrednio z samego algorytmu i tak obliczanie złożoności staje się problemem nałożenia ograniczeń na tę funkcję. nazywamy tę funkcję a powtarzaniem

Na przykład:

function fib_like(n){
    if(n <= 1){
        return 17;
    }else{
        return 42 + fib_like(n-1) + fib_like(n-2);
    }
 }

Łatwo zauważyć, że czas trwania, w kategoriach N, będzie podany by

T(N) = 1 if (N <= 1)
T(N) = T(N-1) + T(N-2) otherwise

Cóż, T (N) jest tylko starą, dobrą funkcją Fibonacciego. Możemy użyć indukcji, żeby ustalić pewne granice.

Dla przykładu Udowodnijmy indukcyjnie, że T (N)

  • przypadek podstawowy: n = 0 lub n = 1
    T(0) = 1 <= 1 = 2^0
    T(1) = 1 <= 2 = 2^1
  • przypadek indukcyjny (n > 1):
    T(N) = T(n-1) + T(n-2)
    aplying the inductive hypothesis in T(n-1) and T(n-2)...
    T(N) <= 2^(n-1) + 2^(n-2)
    so..
    T(N) <= 2^(n-1) + 2^(n-1)
         <= 2^n

(możemy spróbować zrobić coś podobnego, aby udowodnić dolną granicę też)

W większości przypadków dobre odgadnięcie finału runtime funkcji pozwoli Ci łatwo rozwiązać problemy z powtarzaniem za pomocą dowodu indukcyjnego. oczywiście, to wymaga, aby być w stanie odgadnąć pierwszy - tylko dużo praktyki może pomóc tutaj.

I jako ostatnią notkę chciałbym zwrócić uwagę na twierdzenie mistrza , jedyna reguła dla trudniejszych problemów nawrotowych, o której teraz myślę, jest powszechnie używana. użyj go, gdy musisz poradzić sobie z trudnym podziałem i zwycięstwem algorytm.


Również, w twoim przykładzie" jeśli przypadek", rozwiązałbym to poprzez oszustwo i podzielenie go na dwie oddzielne pętle, które nie mają if W Środku.

for (int i = 0; i < n; i++) {
    if (i % 2 ==0) {
        for (int j = i; j < n; j++) { ... }
    } else {
        for (int j = 0; j < i; j++) { ... }
    }
}

Ma ten sam czas działania co

for (int i = 0; i < n; i += 2) {
    for (int j = i; j < n; j++) { ... }
}

for (int i = 1; i < n; i+=2) {
    for (int j = 0; j < i; j++) { ... }
}

I każda z dwóch części może być łatwo postrzegana jako O (N^2) dla sumy, która jest również O(N^2).

Zauważ, że użyłem dobrej sztuczki, aby pozbyć się "jeśli" tutaj. nie ma na to ogólnej reguły, jak pokazuje algorytm Collatza przykład

 69
Author: hugomg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-20 13:07:33

Ogólnie rzecz biorąc, decydowanie o złożoności algorytmu jest teoretycznie niemożliwe.

[17]}jednak jedną z fajnych i zorientowanych na kod metod robienia tego jest bezpośrednie myślenie w kategoriach programów. Weźmy przykład:
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        System.out.println("*");
    }
}

Teraz chcemy przeanalizować jego złożoność, więc dodajmy prosty licznik, który zlicza liczbę egzekucji wewnętrznej linii:

int counter = 0;
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        System.out.println("*");
        counter++;
    }
}

Ponieważ System.Wynocha.linia println nie ma znaczenia, usuńmy ją:

int counter = 0;
for (int i = 0; i < n; i++) {
    for (int j = 0; j < n; j++) {
        counter++;
    }
}

Teraz, gdy my jeśli zostanie tylko licznik, możemy oczywiście uprościć wewnętrzną pętlę out:

int counter = 0;
for (int i = 0; i < n; i++) {
    counter += n;
}

... ponieważ wiemy, że przyrost jest uruchamiany dokładnie n razy. I teraz widzimy, że licznik jest zwiększany o N dokładnie n razy, więc upraszczamy to do:

int counter = 0;
counter += n * n;

I pojawiliśmy się z (poprawnym) O(n2) złożoność:) jest w kodzie:)

Spójrzmy jak to działa dla rekurencyjnego kalkulatora Fibonacciego:

int fib(int n) {
  if (n < 2) return 1;
  return fib(n - 1) + fib(n - 2);
}

Zmień procedura tak, że zwraca liczbę iteracji spędzonych w niej zamiast rzeczywistych liczb Fibonacciego:

int fib_count(int n) {
  if (n < 2) return 1;
  return fib_count(n - 1) + fib_count(n - 2);
}

To wciąż Fibonacci! :) Wiemy więc teraz, że rekurencyjny Kalkulator Fibonacciego ma złożoność O(F (n)), gdzie F jest samą liczbą Fibonacciego.

Ok, spójrzmy na coś ciekawszego, powiedzmy proste (i nieefektywne) połączenia:

void mergesort(Array a, int from, int to) {
  if (from >= to - 1) return;
  int m = (from + to) / 2;
  /* Recursively sort halves */
  mergesort(a, from, m);
  mergesort(m, m,    to);
  /* Then merge */
  Array b = new Array(to - from);
  int i = from;
  int j = m;
  int ptr = 0;
  while (i < m || j < to) {
    if (i == m || a[j] < a[i]) {
      b[ptr] = a[j++];
    } else {
      b[ptr] = a[i++];
    }
    ptr++;
  }
  for (i = from; i < to; i++)
    a[i] = b[i - from];
}

Ponieważ nie interesuje nas rzeczywisty wynik, ale złożoność, zmieniamy rutynę tak, aby w rzeczywistości zwraca liczbę jednostek wykonanych prac:

int mergesort(Array a, int from, int to) {
  if (from >= to - 1) return 1;
  int m = (from + to) / 2;
  /* Recursively sort halves */
  int count = 0;
  count += mergesort(a, from, m);
  count += mergesort(m, m,    to);
  /* Then merge */
  Array b = new Array(to - from);
  int i = from;
  int j = m;
  int ptr = 0;
  while (i < m || j < to) {
    if (i == m || a[j] < a[i]) {
      b[ptr] = a[j++];
    } else {
      b[ptr] = a[i++];
    }
    ptr++;
    count++;
  }
  for (i = from; i < to; i++) {
    count++;
    a[i] = b[i - from];
  }
  return count;
}

Następnie usuwamy te linie, które faktycznie nie wpływają na liczenie i upraszczamy:

int mergesort(Array a, int from, int to) {
  if (from >= to - 1) return 1;
  int m = (from + to) / 2;
  /* Recursively sort halves */
  int count = 0;
  count += mergesort(a, from, m);
  count += mergesort(m, m,    to);
  /* Then merge */
  count += to - from;
  /* Copy the array */
  count += to - from;
  return count;
}

Jeszcze trochę upraszczam:

int mergesort(Array a, int from, int to) {
  if (from >= to - 1) return 1;
  int m = (from + to) / 2;
  int count = 0;
  count += mergesort(a, from, m);
  count += mergesort(m, m,    to);
  count += (to - from) * 2;
  return count;
}

Możemy teraz zrezygnować z tablicy:

int mergesort(int from, int to) {
  if (from >= to - 1) return 1;
  int m = (from + to) / 2;
  int count = 0;
  count += mergesort(from, m);
  count += mergesort(m,    to);
  count += (to - from) * 2;
  return count;
}

Teraz widzimy, że w rzeczywistości wartości bezwzględne from I to nie mają już znaczenia, ale tylko ich odległość, więc modyfikujemy to do: {18]}

int mergesort(int d) {
  if (d <= 1) return 1;
  int count = 0;
  count += mergesort(d / 2);
  count += mergesort(d / 2);
  count += d * 2;
  return count;
}

A potem dochodzimy do:

int mergesort(int d) {
  if (d <= 1) return 1;
  return 2 * mergesort(d / 2) + d * 2;
}

Tutaj oczywiście d na pierwszym wywołaniu jest rozmiar tablicy do posortowania, więc masz powtarzalność dla złożoności M (x) (to jest na widoku w drugiej linii:)

M(x) = 2(M(x/2) + x)

I to trzeba rozwiązać, aby dostać się do rozwiązania formularza zamkniętego. Najłatwiej to zrobić zgadując rozwiązanie M (x) = X log x i sprawdzając po prawej stronie:

2 (x/2 log x/2 + x)
        = x log x/2 + 2x
        = x (log x - log 2 + 2)
        = x (log x - C)

I sprawdzić, czy jest asymptotycznie równoważna lewej stronie:

x log x - Cx
------------ = 1 - [Cx / (x log x)] = 1 - [C / log x] --> 1 - 0 = 1.
x log x
 14
Author: Antti Huima,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-03 19:14:01

Mimo, że jest to zbyt uogólnienie, lubię myśleć o Big-O w kategoriach list, gdzie długość listy wynosi N pozycji.

Tak więc, jeśli masz pętlę for, która iteruje nad wszystkim na liście, jest to O (N). W kodzie masz jedną linijkę, która (w izolacji sama w sobie) wynosi 0 (N).

for (int i = 0; i < n; i++) {

Jeśli masz pętlę for zagnieżdżoną w innej pętli for i wykonujesz operację na każdym elemencie na liście, która wymaga spojrzenia na każdy element na liście, to jesteś wykonywanie operacji N razy dla każdego z N elementów, a więc O (N^2). W powyższym przykładzie masz w rzeczywistości zagnieżdżoną inną pętlę for wewnątrz pętli for. Można więc myśleć o tym tak, jakby każda pętla for miała wartość 0(N), A następnie, ponieważ są zagnieżdżone, pomnożyć je razem dla całkowitej wartości 0(N^2).

Odwrotnie, jeśli robisz tylko szybką operację na pojedynczym elemencie, to będzie to O(1). Nie ma "listy długości n" do przejrzenia, tylko jeden raz operation.To umieścić to w kontekście, w twój przykład powyżej, operacja:

if (i % 2 ==0)

Jest 0(1). Ważne nie jest "Jeśli", ale fakt, że sprawdzenie, czy pojedynczy element jest równy innemu elementowi, jest szybką operacją na pojedynczym elemencie. Podobnie jak poprzednio, Instrukcja if jest zagnieżdżona wewnątrz zewnętrznej pętli for. Jednakże, ponieważ jest to 0 (1), wtedy mnożymy wszystko przez '1', a więc nie ma 'zauważalnego' wpływu w ostatecznym obliczeniu czasu działania całej funkcji.

Do logów, oraz do czynienia z bardziej złożone sytuacje (jak ta sprawa liczenia do j lub i, a nie tylko znowu n), chciałbym wskazać Ci bardziej eleganckie Wyjaśnienie tutaj.

 2
Author: Aurora,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-25 19:07:51

Lubię używać dwóch rzeczy do notacji Big-O: standardowego Big-O, co jest najgorszym scenariuszem, i średniego Big-O, co zwykle się dzieje. Pomaga mi to również pamiętać, że notacja Big-O stara się przybliżać czas pracy jako funkcję N, liczbę wejść.

TA wyjaśnił to czymś w rodzaju kombinacji. Jak to jest N Wybierz 2 = (n (n-1))/2 = N^2 + 0,5, następnie usuń stałą, aby stała stała się n^2. Mogę umieścić wartości testowe int i spróbować, ale jak czy ta kombinacja wchodzi w grę?

Jak już mówiłem, normalny big-O to najgorszy scenariusz. Możesz spróbować policzyć ile razy każda linia jest wykonywana, ale prostsze jest po prostu spojrzeć na pierwszy przykład i powiedzieć, że istnieją dwie pętle na długości n, jedna osadzona w drugiej, więc jest to n * N. gdyby były one jedna po drugiej, to byłoby n + n, równe 2N. ponieważ jest to przybliżenie, po prostu mówisz n lub liniowe.

Co jeśli jest deklaracja if? Jak jest złożoność ustalona?

To jest miejsce, gdzie dla mnie średnia sprawa i najlepszy przypadek bardzo pomaga uporządkować moje myśli. W najgorszym przypadku ignorujesz if I mówisz n^2. W przeciętnym przypadku, na przykład, masz pętlę nad n, z inną pętlą nad częścią n, która dzieje się w połowie czasu. Daje to n * n/x / 2 (x jest tym, co ułamek n zostaje zapętlony w osadzonych pętlach. To daje n^2 / (2x), więc dostaniesz n^2 tak samo. To dlatego, że jego / align = "left" /

Wiem, że to nie jest kompletna odpowiedź na twoje pytanie, ale mam nadzieję, że rzuci jakieś światło na przybliżanie złożoności kodu.

Jak zostało powiedziane w odpowiedziach powyżej moich, nie jest wyraźnie możliwe określenie tego dla wszystkich fragmentów kodu; chciałem tylko dodać pomysł użycia średniej wielkości liter Big-O do dyskusji.

 2
Author: dbeer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-04 16:46:42

Dla pierwszego fragmentu, to tylko n^2, ponieważ wykonujesz n operacji n razy. Jeśli {[1] } została zainicjowana na i, lub poszła do i, Wyjaśnienie, które zamieściłeś byłoby bardziej odpowiednie, ale w obecnym stanie nie jest.

Dla drugiego fragmentu, można łatwo zobaczyć, że w połowie czasu pierwszy będzie wykonywany, a drugi będzie wykonywany w drugiej połowie czasu. W zależności od tego, co tam jest (miejmy nadzieję, że zależy to od n), możesz przepisać równanie jako rekurencyjny.

Równania rekurencyjne (w tym trzeci fragment) można zapisać jako takie: trzeci pojawi się jako

T(n) = T(n-1) + 1

Które możemy łatwo zobaczyć to O (n).

 0
Author: bdares,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-10-25 06:59:18

Big-O jest tylko przybliżeniem, nie mówi, jak długo algorytm zajmuje wykonanie, mówi tylko coś o tym, jak długo trwa, gdy rozmiar jego wejścia rośnie.

Więc jeśli wejście jest wielkości N i algorytm obliczy wyrażenie stałej złożoności: O (1) N razy, złożoność algorytmu jest liniowa: O (N). Jeśli wyrażenie ma złożoność liniową, algorytm ma złożoność kwadratową: O (N*N).

Niektóre wyrażenia mają złożoność wykładniczą: O(N^N) lub złożoność logarytmiczna: o (log N). Dla algorytmu z pętlami i rekurencją, mnożenie złożoności każdego poziomu pętli i / lub rekurencji. Pod względem złożoności pętla i rekurencja są równoważne. Algorytm, który ma różne złożoności na różnych etapach algorytmu, wybierz najwyższą złożoność i Ignoruj resztę. I wreszcie, wszystkie złożoności stałe są uważane za równoważne: O(5) jest taka sama jak O(1), O (5*N) jest taka sama jak O (N).

 -1
Author: Minthos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-11-07 11:45:32