web-dev-qa-db-fra.com

Qu'est-ce qui ferait qu'un algorithme soit de complexité O (log n)?

Ma connaissance de big-O est limitée, et lorsque les termes de log apparaissent dans l'équation, cela me jette encore plus.

Quelqu'un peut-il peut-être m'expliquer en termes simples ce qu'est un algorithme O(log n)? D'où vient le logarithme?

Ceci est apparu spécifiquement lorsque j'essayais de résoudre cette question de pratique à mi-parcours:

Soit X(1..n) et Y(1..n) contient deux listes d’entiers, chacune triée dans l’ordre non décroissant. Donnez un O ( log n) - algorithme de temps pour trouver la médiane (ou le nième plus petit entier) de tous les 2n éléments combinés. Par exemple, X = (4, 5, 7, 8, 9) et Y = (3, 5, 8, 9 , 10), alors 7 est la médiane de la liste combinée (3, 4, 5, 5, 7, 8, 8, 9, 9, 10). [Indice: utilisez les concepts de la recherche binaire]

99
user1189352

Je dois admettre que c'est assez étrange la première fois que vous voyez un algorithme O (log n) ... D'où vient ce logarithme? Cependant, il se trouve qu'il existe plusieurs manières différentes d'obtenir un terme de journal en notation Big-O. Voici quelques-uns:

Diviser à plusieurs reprises par une constante

Prenez n'importe quel nombre n; disons 16. Combien de fois pouvez-vous diviser n par deux avant d'obtenir un nombre inférieur ou égal à un? Pour 16, nous avons que

16 / 2 = 8
 8 / 2 = 4
 4 / 2 = 2
 2 / 2 = 1

Notez que cela finit par prendre quatre étapes. Fait intéressant, nous avons également ce journal2 16 = 4. Hmmm ... qu'en est-il de 128?

128 / 2 = 64
 64 / 2 = 32
 32 / 2 = 16
 16 / 2 = 8
  8 / 2 = 4
  4 / 2 = 2
  2 / 2 = 1

Cela a pris sept étapes et connectez-vous2 128 = 7. Est-ce une coïncidence? Nan! Il y a une bonne raison à cela. Supposons que nous divisons un nombre n par 2 fois. Ensuite, nous obtenons le numéro n/2je. Si nous voulons résoudre pour la valeur de i où cette valeur est au plus 1, nous obtenons

n/2je ≤ 1

n ≤ 2je

bûche2 n ≤ i

En d’autres termes, si on choisit un entier i tel que i ≥ log2 n, puis après avoir divisé n en deux fois la valeur de n, nous aurons une valeur d'au plus 1. Le plus petit i pour lequel cela est garanti est approximativement le journal2 n, donc si nous avons un algorithme qui divise par 2 jusqu'à ce que le nombre soit suffisamment petit, alors nous pouvons dire qu'il se termine par des étapes O (log n).

Un détail important est que la constante divisée par n importe peu (tant qu'elle est supérieure à un); si vous divisez par la constante k, il faudra vous connecterk n étapes pour atteindre 1. Ainsi, tout algorithme qui divise de manière répétée la taille en entrée par une fraction aura besoin de 0 (log n) itérations pour se terminer. Ces itérations peuvent prendre beaucoup de temps et le temps d'exécution net ne doit pas nécessairement être O (log n), mais le nombre d'étapes sera logarithmique.

Alors, où cela se produit-il? Un exemple classique est recherche binaire, un algorithme rapide permettant de rechercher une valeur dans un tableau trié. L'algorithme fonctionne comme ceci:

  • Si le tableau est vide, indiquez que l'élément n'est pas présent dans le tableau.
  • Autrement:
    • Regardez l'élément du milieu du tableau.
    • Si c'est égal à l'élément que nous recherchons, renvoyez le succès.
    • S'il est supérieur à l'élément recherché:
      • Jeter la seconde moitié du tableau.
      • Répéter
    • Si c'est moins que l'élément recherché, nous recherchons:
      • Jeter la première moitié du tableau.
      • Répéter

Par exemple, pour rechercher 5 dans le tableau

1   3   5   7   9   11   13

Nous allons d'abord regarder l'élément du milieu:

1   3   5   7   9   11   13
            ^

Depuis 7> 5, et puisque le tableau est trié, nous savons que le nombre 5 ne peut pas être dans la moitié arrière du tableau, nous pouvons donc simplement le supprimer. Cela laisse

1   3   5

Alors maintenant, regardons l'élément central ici:

1   3   5
    ^

Depuis 3 <5, nous savons que 5 ne peut pas apparaître dans la première moitié du tableau, nous pouvons donc lancer le tableau de la première moitié pour quitter

        5

Encore une fois, nous regardons au milieu de ce tableau:

        5
        ^

Puisque c'est exactement le nombre que nous recherchons, nous pouvons signaler que 5 se trouve effectivement dans le tableau.

Alors, comment est-ce efficace? Eh bien, à chaque itération, nous jetons au moins la moitié des éléments de tableau restants. L'algorithme s'arrête dès que le tableau est vide ou que nous trouvons la valeur souhaitée. Dans le pire des cas, l’élément n’est pas là; nous continuons donc à diviser par deux la taille du tableau jusqu’à épuisement des éléments. Combien de temps cela prend-il? Eh bien, puisque nous continuons à couper le tableau en deux, encore et encore, nous aurons terminé au plus O (log n) itérations, car nous ne pouvons pas couper le tableau de moitié plus que O (log n) avant d'exécuter. sur des éléments de tableau.

Algorithmes suivant la technique générale de diviser pour régner (découper le problème en morceaux, résoudre ces problèmes, puis mettre le problème ensemble) ont tendance à contenir des termes logarithmiques pour la même raison - vous ne pouvez pas couper un objet de moitié plus que O (log n) fois. Vous voudrez peut-être regarder fusionner le tri comme un excellent exemple.

Traitement des valeurs un chiffre à la fois

Combien de chiffres y a-t-il dans le nombre base 10? Eh bien, s'il y a k chiffres dans le nombre, alors nous aurions que le plus grand chiffre est un multiple de 10k. Le plus grand nombre de k chiffres est 999 ... 9, k fois, et cela équivaut à 10k + 1 - 1. Par conséquent, si nous savons que n contient k chiffres, nous savons que la valeur de n est au plus égale à 10k + 1 - 1. Si nous voulons résoudre pour k en termes de n, nous obtenons

n ≤ 10k + 1 - 1

n + 1 ≤ 10k + 1

bûchedix (n + 1) ≤ k + 1

(bûchedix (n + 1)) - 1 ≤ k

On obtient que k est approximativement le logarithme en base 10 de n. En d'autres termes, le nombre de chiffres dans n est O (log n).

Par exemple, pensons à la complexité d'ajouter deux grands nombres qui sont trop gros pour tenir dans une machine Word. Supposons que ces chiffres soient représentés en base 10 et que nous appelions les nombres m et n. Une méthode pour les ajouter consiste à utiliser la méthode de l'école primaire: écrivez les chiffres un chiffre à la fois, puis travaillez de droite à gauche. Par exemple, pour ajouter 1337 et 2065, nous commencerions par écrire les nombres comme

    1  3  3  7
+   2  0  6  5
==============

Nous ajoutons le dernier chiffre et portons le 1:

          1
    1  3  3  7
+   2  0  6  5
==============
             2

Ensuite, nous ajoutons l'avant-dernier chiffre ("avant-dernier") et portons le 1:

       1  1
    1  3  3  7
+   2  0  6  5
==============
          0  2

Ensuite, nous ajoutons le dernier chiffre ("antepenultimate"):

       1  1
    1  3  3  7
+   2  0  6  5
==============
       4  0  2

Enfin, nous ajoutons l’avant-dernier chiffre ("preantepenultimate" ... I love English):

       1  1
    1  3  3  7
+   2  0  6  5
==============
    3  4  0  2

Maintenant, combien de travail avons-nous fait? Nous faisons un total de O(1) travail par chiffre (c’est-à-dire un travail constant), et il y a O (max {log n, log m}) total de chiffres qui nécessitent Cela donne un total de complexité O (max {log n, log m}), car nous devons visiter chaque chiffre des deux nombres.

De nombreux algorithmes prennent un terme O (log n) en travaillant un chiffre à la fois dans une base donnée. Un exemple classique est sorte de radix, qui trie les entiers chiffre par chiffre. Il existe plusieurs types de tri de base, mais ils s'exécutent généralement dans le temps O (n log U), où U est le plus grand entier possible trié. La raison en est que chaque passe du tri prend O(n)) temps, et qu’il faut un total de répétitions O (log U) pour traiter chacune des opérations O (log U). chiffres du plus grand nombre en cours de tri. De nombreux algorithmes avancés, tels que algorithme des chemins les plus courts de Gabow ou la version mise à l'échelle de algorithme Ford-Fulkerson max-flow , ont un journal terme dans leur complexité, car ils travaillent un chiffre à la fois.


En ce qui concerne votre deuxième question sur la façon de résoudre ce problème, vous voudrez peut-être examiner cette question connexe qui explore une application plus avancée. Compte tenu de la structure générale des problèmes décrits ici, vous pouvez maintenant mieux comprendre comment penser aux problèmes lorsque vous savez que le résultat contient un terme de journal. Je vous déconseille donc de regarder la réponse tant que vous ne l'auriez pas donnée. quelques réflexions.

J'espère que cela t'aides!

278
templatetypedef

Lorsque nous parlons de descriptions big-Oh, nous parlons généralement du temps nécessaire pour résoudre les problèmes d'un ) taille . Et généralement, pour des problèmes simples, cette taille est simplement caractérisée par le nombre d'éléments en entrée, et s'appelle généralement n, ou N. (Évidemment, ce n'est pas toujours vrai - les problèmes de graphes sont souvent caractérisés par le nombre de sommets, V et nombre d'arêtes, E; mais pour l'instant, nous allons parler de listes d'objets, avec N objets dans les listes.)

Nous disons qu'un problème "est gros-Oh de (une fonction de N)" si et seulement si :

Pour tout N> certains N_0 arbitraires, il existe une constante c, telle que le temps d’exécution de l’algorithme est inférieur à cette constante c fois (une fonction de N.)

En d’autres termes, ne pensez pas aux petits problèmes où la "surcharge constante" de la mise en place du problème compte, pensez aux gros problèmes. Et quand on pense à de gros problèmes, gros-Oh de (une fonction de N) signifie que le temps d'exécution est toujours toujours inférieur à des temps constants qui une fonction. Toujours.

En bref, cette fonction est une limite supérieure, jusqu’à un facteur constant.

Ainsi, "big-Oh of log (n)" signifie la même chose que ce que j'ai dit ci-dessus, sauf que "une fonction de N" est remplacée par "log (n)".

Votre problème vous oblige donc à penser à la recherche binaire, alors réfléchissons-y. Supposons que vous ayez, par exemple, une liste de N éléments triés par ordre croissant. Vous voulez savoir si un certain nombre existe dans cette liste. Une façon de faire ce qui est et non une recherche binaire est simplement de scanner chaque élément de la liste et de voir si c'est votre numéro cible. Vous pourriez avoir de la chance et le trouver du premier coup. Mais dans le pire des cas, vous allez vérifier N fois différents. Ce n'est pas une recherche binaire, et ce n'est pas un gros problème de log (N) car il n'y a aucun moyen de le forcer dans les critères que nous avons esquissés ci-dessus.

Vous pouvez choisir cette constante arbitraire pour être c = 10, et si votre liste a N = 32 éléments, tout va bien: 10 * log (32) = 50, ce qui est supérieur au temps d'exécution de 32. Mais si N = 64 , 10 * log (64) = 60, ce qui est inférieur au temps d’exécution de 64. Vous pouvez choisir c = 100, ou 1000, voire un milliard de dollars, et vous pourrez toujours trouver du N qui ne respecte pas cette exigence. En d'autres termes, il n'y a pas de N_0.

Si nous effectuons une recherche binaire, nous sélectionnons l'élément central et faisons une comparaison. Ensuite, nous jetons la moitié des chiffres et recommençons, et ainsi de suite. Si votre N = 32, vous ne pouvez le faire qu’environ 5 fois, ce qui correspond à log (32). Si votre N = 64, vous ne pouvez le faire qu’environ 6 fois, etc. Maintenant, vous pouvez choisir cette constante arbitraire c, de sorte que l'exigence est toujours remplie pour les grandes valeurs de N.

Avec tout ce fond, ce que O(log(N)) signifie généralement que vous avez le moyen de faire quelque chose de simple, qui réduit de moitié la taille de votre problème. Tout comme le fait la recherche binaire ci-dessus. Une fois que vous avez réduit le problème en deux, vous pouvez le réduire en deux, encore et encore, et encore. Mais, ce qui est critique, ce que vous ne pouvez pas est une étape de prétraitement qui prendrait plus de temps que cela O(log(N)) temps. Ainsi, par exemple, vous ne pouvez pas mélanger vos deux listes en une seule grande liste, à moins que vous ne trouviez le moyen de le faire en O(log(N)) heure également.

(REMARQUE: presque toujours, Log (N) signifie log-base-two, ce que je suppose ci-dessus.)

7
Novak

Dans la solution suivante, toutes les lignes avec un appel récursif sont effectuées sur la moitié des tailles données des sous-tableaux de X et Y. Les autres lignes sont effectuées dans un temps constant. La fonction récursive est T (2n) = T (2n/2) + c = T (n) + c = 0 (lg (2n)) = 0 (lgn).

Vous commencez par MEDIAN (X, 1, n, Y, 1, n).

MEDIAN(X, p, r, Y, i, k) 
if X[r]<Y[i]
    return X[r]
if Y[k]<X[p]
    return Y[k]
q=floor((p+r)/2)
j=floor((i+k)/2)
if r-p+1 is even
    if X[q+1]>Y[j] and Y[j+1]>X[q]
        if X[q]>Y[j]
            return X[q]
        else
            return Y[j]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q+1, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j+1, k)
else
    if X[q]>Y[j] and Y[j+1]>X[q-1]
        return Y[j]
    if Y[j]>X[q] and X[q+1]>Y[j-1]
        return X[q]
    if X[q+1]<Y[j-1]
        return MEDIAN(X, q, r, Y, i, j)
    else
        return MEDIAN(X, p, q, Y, j, k)
4
Avi Cohen

Le terme Log apparaît très souvent dans l'analyse de la complexité des algorithmes. Voici quelques explications:

1. Comment représentez-vous un nombre?

Prenons le nombre X = 245436. Cette notation de "245436" contient des informations implicites. Rendre cette information explicite:

X = 2 * 10 ^ 5 + 4 * 10 ^ 4 + 5 * 10 ^ 3 + 4 * 10 ^ 2 + 3 * 10 ^ 1 + 6 * 10 ^ 0

Quel est le développement décimal du nombre. Ainsi, le quantité minimale d'informations nous devons représenter ce nombre est 6 chiffres. Ce n'est pas une coïncidence, car tout nombre inférieur à 10 ^ d peut être représenté en d chiffres.

Alors combien de chiffres sont nécessaires pour représenter X? C'est égal au plus grand exposant de 10 dans X plus 1.

==> 10 ^ d> X
==> log (10 ^ d)> log (X)
==> d * log (10)> log (X)
==> d> log (X) // Et log apparaît à nouveau ...
==> d = étage (log (x)) + 1

Notez également que c’est le moyen le plus concis de désigner le nombre dans cette plage. Toute réduction entraînera une perte d'informations, puisqu'un chiffre manquant peut être associé à 10 autres nombres. Par exemple: 12 * peuvent être mappés sur 120, 121, 122,…, 129.

2. Comment chercher un numéro en (0, N - 1)?

En prenant N = 10 ^ d, nous utilisons notre observation la plus importante:

Quantité minimale d'informations permettant d'identifier de manière unique une valeur comprise entre 0 et N - 1 = log (N) chiffres.

Cela implique que, lorsqu'on lui demande de rechercher un nombre sur la ligne entière, allant de 0 à N - 1, nous avons besoin de au moins log (N) essaie de le trouver. Pourquoi? Tout algorithme de recherche devra choisir un chiffre après l’autre dans sa recherche du numéro.

Le nombre minimum de chiffres à choisir est log (N). Par conséquent, le nombre minimal d'opérations effectuées pour rechercher un nombre dans un espace de taille N est log (N).

Pouvez-vous deviner les complexités d'ordre de la recherche binaire, de la recherche ternaire ou de la recherche deca?
Son O (log (N)) !

3. Comment trier un ensemble de nombres?

Lorsqu'on lui demande de trier un ensemble de nombres A dans un tableau B, voici à quoi il ressemble ->

éléments permutés

Chaque élément du tableau d'origine doit être mappé sur son index correspondant dans le tableau trié. Donc, pour le premier élément, nous avons n positions. Pour trouver correctement l'index correspondant dans cette plage de 0 à n - 1, nous avons besoin de… opérations log (n).

L'élément suivant nécessite des opérations de journalisation (n-1), le journal suivant (n-2), etc. Le total vient d'être:

==> log (n) + log (n - 1) + log (n - 2) +… + log (1)

En utilisant log (a) + log (b) = log (a * b),

==> log (n!)

Cela peut être approximé à nlog (n) - n.
Qui est O (n * log (n))!

Nous concluons donc qu’il n’existe aucun algorithme de tri capable de faire mieux que O (n * log (n)). Et certains algorithmes ayant cette complexité sont le populaire Merge Sort et le Heap Sort!

Ce sont certaines des raisons pour lesquelles nous voyons log (n) apparaître si souvent dans l'analyse de complexité des algorithmes. La même chose peut être étendue aux nombres binaires. J'ai fait une vidéo à ce sujet ici.
Pourquoi log (n) apparaît-il si souvent lors de l'analyse de la complexité d'un algorithme?

À votre santé!

3
Gaurav Sen

Nous appelons la complexité temporelle O (log n), lorsque la solution est basée sur des itérations sur n, où le travail effectué à chaque itération est une fraction de l'itération précédente, car l'algorithme vise la solution.

2
Alex Worden

Je ne peux pas encore commenter ... c'est necro! La réponse d'Avi Cohen est incorrecte, essayez:

X = 1 3 4 5 8
Y = 2 5 6 7 9

Aucune des conditions n'est vraie, donc MEDIANE (X, p, q, Y, j, k) coupera les deux. Ce sont des séquences non décroissantes, toutes les valeurs ne sont pas distinctes.

Essayez également cet exemple de longueur paire avec des valeurs distinctes:

X = 1 3 4 7
Y = 2 5 6 8

Maintenant, MEDIANE (X, p, q, Y, j + 1, k) coupera les quatre.

Au lieu d’offrir cet algorithme, appelez-le avec MEDIAN (1, n, 1, n):

MEDIAN(startx, endx, starty, endy){
  if (startx == endx)
    return min(X[startx], y[starty])
  odd = (startx + endx) % 2     //0 if even, 1 if odd
  m = (startx+endx - odd)/2
  n = (starty+endy - odd)/2
  x = X[m]
  y = Y[n]
  if x == y
    //then there are n-2{+1} total elements smaller than or equal to both x and y
    //so this value is the nth smallest
    //we have found the median.
    return x
  if (x < y)
    //if we remove some numbers smaller then the median,
    //and remove the same amount of numbers bigger than the median,
    //the median will not change
    //we know the elements before x are smaller than the median,
    //and the elements after y are bigger than the median,
    //so we discard these and continue the search:
    return MEDIAN(m, endx, starty, n + 1 - odd)
  else  (x > y)
    return MEDIAN(startx, m + 1 - odd, n, endy)
}
1
Wolfzoon