34 votes

La détermination de la complexité compte tenu de codes

Étant donné un snipplet de code, comment allez-vous déterminer la complexité en général. Je me retrouve à se confondre avec Big O questions. Par exemple, une question très simple:

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

Le TA a expliqué cela avec quelque chose comme les combinaisons. Comme c'est n choisir 2 = (n(n-1))/2 = n^2 + 0,5, puis supprimer la constante de sorte qu'il devient n^2. Je peux mettre int valeurs de test et d'essayer mais comment cette combinaison chose venir?

Si theres une instruction if? Quelle est la complexité déterminé?

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++) { ... }
    }
}

Alors que penser de la récursivité ...

int fib(int a, int b, int n) {
    if (n == 3) {
        return a + b;
    } else {
        return fib(b, a+b, n-1);
    }
}

69voto

hugomg Points 29789

En général, il n'y a aucun moyen de déterminer la complexité d'une fonction donnée

Avertissement! Mur de texte entrant!

1. Il y a très simple des algorithmes que l'on ne sait pas si ils même d'arrêter ou pas.

Il n'y a pas d'algorithme qui peut décider si un programme s'arrête ou pas, si certaines d'entrée. Le calcul de la complexité de calcul est encore plus difficile de problème puisque non seulement nous avons besoin de prouver que l'algorithme s'arrête, mais nous avons besoin de prouver à quelle vitesse il fait.

//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. Certains algorithmes ont bizarre et décalé complexité

Un général de "la complexité de la détermination de régime" pourrait facilement devenir trop compliqué à cause de ces gars-là

//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. Certaines fonctions sont très simples mais confondez beaucoup de sortes de l'analyse statique des tentatives

//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));
    }
}

Cela dit, nous avons encore besoin d'un moyen de trouver de la complexité de la chose, pas vrai? Pour les boucles sont un moyen simple et modèle commun. Prenez votre premier exemple:

for(i=0; i<N; i++){
   for(j=0; j<i; j++){
       print something
   }
}

Depuis chaque print something O(1), la complexité temporelle de l'algorithme sera déterminé par combien de fois nous exécuter cette ligne. Eh bien, comme votre TA mentionné, nous en regardant les combinaisons dans ce cas. L'intérieur de la boucle sera exécuté (N + (N-1) + ... + 1) fois, pour un total de (N+1)*N/2.

Comme nous négligeons les constantes de nous obtenir O(N2).

Maintenant, pour la plus délicate des cas, nous pouvons obtenir plus de mathématiques. Essayez de créer une fonction dont la valeur représente combien de temps l'algorithme d'exécution, compte tenu de la taille N de l'entrée. Souvent, nous pouvons construire une version récursive de cette fonction directement à partir de l'algorithme lui-même et pour le calcul de la complexité devient le problème de mettre des bornes sur cette fonction. Nous appelons cette fonction une récidive

Par exemple:

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

il est facile de voir que le temps d'exécution, en termes de N, sera donnée par

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

Eh bien, T(N) est simplement la bonne vieille fonction de Fibonacci. Nous pouvons utiliser l'induction de mettre certaines limites sur ce point.

Pour, par exemple, Permet de prouver, par induction, que T(N) <= 2^n pour tout n (c, T(N) est O(2^n))

  • cas de base: n = 0 ou n = 1
    T(0) = 1 <= 1 = 2^0
    T(1) = 1 <= 2 = 2^1
  • inductive cas (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

(nous pouvons essayer de faire quelque chose de similaire à prouver que la limite inférieure de trop)

Dans la plupart des cas, avoir une bonne estimation sur la dernière exécution de la fonction, vous permettent de résoudre la récurrence des problèmes avec une induction de la preuve. Bien sûr, cela vous oblige à être en mesure de deviner la première seulement beaucoup de pratique peut vous aider ici.

Et comme f note finale, je tiens à souligner à propos de le Maître théorème, la seule règle pour le plus difficile de la récurrence des problèmes que je peux penser maintenant qui est couramment utilisé. L'utiliser lorsque vous avez à traiter avec une délicate diviser et conquérir de l'algorithme.


Aussi, dans votre "cas", par exemple, je voudrais résoudre que par la tricherie et de le diviser en deux boucles séparées qui ne t ont un si à l'intérieur.

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++) { ... }
    }
}

A la même exécution que

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++) { ... }
}

Et chacune des deux parties peut être facilement vu à O(N^2), pour un total qui est également en O(N^2).

Notez que j'ai utilisé un bon truc astuce pour se débarrasser de la "si" ici. Il n'y a pas de règle générale pour le faire, comme le montre l'algorithme de Collatz exemple

14voto

Antti Huima Points 15465

En général, décider de la complexité de l'algorithme est théoriquement impossible.

Cependant, une froide et centrée sur le code de la méthode pour le faire est de penser en termes de programmes directement. Prenez votre exemple:

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

Maintenant, nous voulons analyser sa complexité, nous allons donc ajouter un simple compteur qui compte le nombre d'exécutions de l'intérieur de la ligne:

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

Parce que le Système.out.println ligne n'a pas vraiment d'importance, nous allons la supprimer:

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

Maintenant que nous avons seulement le compteur de gauche, on peut évidemment simplifier l'intérieure de la boucle:

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

... parce que nous savons que l'incrément est exécuté exactement n fois. Et maintenant, nous voyons que le compteur est incrémenté par n exactement n fois, afin de nous simplifier ainsi:

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

Et nous en sommes sortis avec le (bon) O(n2) complexité :) C'est là, dans le code :)

Voyons comment cela fonctionne pour récursive de Fibonacci de la calculatrice:

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

Modification de la routine de sorte qu'elle retourne le nombre d'itérations passé à l'intérieur à la place de la suite de Fibonacci:

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

C'est encore de Fibonacci! :) Donc, nous savons maintenant que le récursive de Fibonacci de la calculatrice est de complexité O(F(n)) où F est le nombre de Fibonacci lui-même.

Ok, regardons quelque chose de plus intéressant, de dire simple (et inefficace) mergesort:

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];
}

Parce que nous ne sommes pas intéressés par le résultat, mais la complexité, nous changer de la routine de sorte qu'il retourne en fait le nombre d'unités de travail effectué:

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;
}

Puis nous supprimer ces lignes qui ne sont pas réellement l'impact des comtes et simplifier:

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;
}

Toujours en simplifiant un peu:

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;
}

Nous pouvons maintenant passer de la matrice:

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;
}

Nous pouvons maintenant voir que en fait les valeurs absolues de partir et de ne pas faire plus d'importance, mais seulement de leur distance, de sorte que l'on modifie ainsi:

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;
}

Et puis, nous arrivons à:

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

Ici, évidemment, d sur le premier appel est la taille du tableau à trier, vous avez donc la récurrence de la complexité M(x) (c'est à la vue de tous sur la deuxième ligne :)

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

et ce que vous devrez résoudre pour arriver à une solution de la forme finie. Ce que vous faites le plus facile par deviner la solution de M(x) = x log x, et de vérifier pour le côté droit:

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

et vérifiez qu'il est asymptotiquement équivalent à la gauche:

x log x - Cx
------------ = 1 - [Cx / (x log x)] = 1 - [C / log x] --> 1 - 0 = 1.
x log x

2voto

Aurora Points 2456

Même si c'est une généralisation, j'aime à penser que Big-O en termes de listes, d'où la longueur de la liste est N éléments.

Ainsi, si vous avez une boucle for qui parcourt toute la liste, il est O(N). Dans votre code, vous avez une ligne (dans l'isolement par lui-même) est 0(N).

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

Si vous avez une boucle for imbriquée à l'intérieur d'une autre boucle for, et que vous effectuez une opération sur chaque élément de la liste qui vous oblige à regarder chaque élément de la liste, alors vous faites une opération N fois pour chacun des N éléments, donc O(N^2). Dans votre exemple ci-dessus, vous pouvez, en fait, ont une autre boucle for imbriquée à l'intérieur de votre boucle. Ainsi, vous pouvez penser à ce sujet, comme si chaque boucle for est 0(N), et ensuite parce qu'ils sont imbriqués, les multiplier, ensemble, pour une valeur totale de 0(N^2).

A l'inverse, si vous êtes juste faire une opération rapide sur un seul élément, puis que serait O(1). Il n'existe pas de liste de longueur n pour aller plus, juste une seule opération d'une fois.Pour mettre cela en contexte, dans l'exemple ci-dessus, l'opération:

if (i % 2 ==0)

est 0(1). Ce qui est important n'est pas de la "si", mais le fait que la vérification pour voir si un seul élément est égal à un autre élément est une opération rapide sur un seul élément. Comme avant, si l'instruction est imbriquée à l'intérieur de votre externe pour la boucle. Cependant, parce que c'est 0(1), alors vous êtes en multipliant le tout par '1', et donc il n'est pas "visible" affecter dans votre calcul final pour le temps d'exécution de l'ensemble de la fonction.

Pour les logs, et faire face à des situations plus complexes (comme cette activité de comptage jusqu'à j ou i, et pas seulement en n de nouveau), je tiens à vous orienter vers une plus élégant explication ici.

2voto

dbeer Points 2468

J'aime utiliser deux choses pour Big-O de notation: standard du Big-O, qui est le scénario du pire cas et en moyenne Big-O, ce qui est normalement finit par arriver. Il permet aussi de me rappeler que Big-je la notation est d'essayer de rapprocher au moment de l'exécution en fonction de N, le nombre d'entrées.

Le TA a expliqué cela avec quelque chose comme les combinaisons. Comme c'est n choisir 2 = (n(n-1))/2 = n^2 + 0,5, puis supprimer la constante de sorte qu'il devient n^2. Je peux mettre int valeurs de test et d'essayer mais comment cette combinaison chose venir?

Comme je l'ai dit, normal big-O est le scénario du pire cas. Vous pouvez essayer de compter le nombre de fois que chaque ligne est exécuté, mais il est plus simple de regarder le premier exemple et dire qu'il y a deux boucles sur toute la longueur de n, incorporés dans l'autre, de sorte qu'il est n * n. Si ils ont été l'un après l'autre, il serait n + n, égal à 2n. Depuis sa une approximation, tu viens de dire n ou linéaire.

Si theres une instruction if? Quelle est la complexité déterminé?

C'est là que pour m'avoir moyen de cas et dans le meilleur des cas aide beaucoup pour l'organisation de mes pensées. Dans le pire des cas, vous ignorez le si, et de dire n^2. Dans la moyenne des cas, par votre exemple, vous avez une boucle de n, avec une autre boucle sur une partie de n qui passe la moitié du temps. Cela vous donne le n * n/x/2 (le x est quelle que soit la fraction de n obtient en boucle sur la dans votre les boucles magnétiques. Cela vous donne le n^2/(2x), de sorte que vous obtenez n^2 de la même façon. C'est parce que c'est une approximation.

Je sais que ce n'est pas une réponse complète à votre question, mais j'espère qu'il jette une certaine lumière sur l'approximation des complexités dans le code.

Comme il a été dit dans les réponses au-dessus de la mienne, c'est clairement pas possible de déterminer ce pour tous les extraits de code; je voulais juste ajouter l'idée d'utiliser la moyenne des cas Big-O à la discussion.

0voto

bdares Points 10615

Pour le premier extrait, c'est juste n^2 parce que vous effectuez n des opérations de n fois. Si j a été initialisée pour i, ou est allé jusqu'à i, l'explication que vous avez posté, serait plus approprié, mais en l'état actuel il n'est pas.

Pour le deuxième extrait, vous peut facilement voir que la moitié du temps, la première sera exécuté, et le second sera exécuté, l'autre moitié du temps. En fonction de ce qui est là (j'espère que c'est dépendante n), vous pouvez réécrire l'équation récursive de l'un.

Les équations récursives (y compris le troisième extrait) peut être écrite comme suit: le troisième semblerait que

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

Qui nous pouvons facilement voir est O(n).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X