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 s'avère qu'il existe plusieurs façons d'obtenir un terme de journal pour apparaître dans la notation big-O. Voici quelques-uns:
Diviser à plusieurs reprises par une constante
Prenez n'importe quel nombre n; dis 16. Combien de fois pouvez-vous diviser n par deux avant d'obtenir un nombre inférieur ou égal à un? Pour 16, nous avons ça
16 / 2 = 8
8 / 2 = 4
4 / 2 = 2
2 / 2 = 1
Notez que cela prend quatre étapes pour terminer. Fait intéressant, nous avons aussi ce log 2 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 log 2 128 = 7. Est-ce une coïncidence? Nan! Il y a une bonne raison à cela. Supposons que nous divisions un nombre n par 2 i fois. Ensuite, nous obtenons le nombre n / 2 i . Si nous voulons résoudre la valeur de i où cette valeur est au plus 1, nous obtenons
n / 2 i ≤ 1
n ≤ 2 i
log 2 n ≤ i
En d'autres termes, si nous choisissons un entier i tel que i ≥ log 2 n, alors après avoir divisé n en deux fois i, nous aurons une valeur au plus égale à 1. Le plus petit i pour lequel cela est garanti est à peu près log 2 n, donc si nous avons un algorithme qui divise par 2 jusqu'à ce que le nombre devienne suffisamment petit, alors nous pouvons dire qu'il se termine par des étapes O (log n).
Un détail important est que peu importe la constante par laquelle vous divisez n (tant qu'elle est supérieure à un); si vous divisez par la constante k, il faudra log k n pas pour atteindre 1. Ainsi, tout algorithme qui divise à plusieurs reprises la taille d'entrée par une fraction aura besoin de O (log n) itérations pour se terminer. Ces itérations peuvent prendre beaucoup de temps et donc le runtime net n'a pas besoin d'être O (log n), mais le nombre d'étapes sera logarithmique.
Alors, d'où vient ce problème? Un exemple classique est la recherche binaire , un algorithme rapide pour rechercher une valeur dans un tableau trié. L'algorithme fonctionne comme ceci:
- Si le tableau est vide, retournez que l'élément n'est pas présent dans le tableau.
- Autrement:
- Regardez l'élément du milieu du tableau.
- S'il est égal à l'élément recherché, renvoie succès.
- S'il est supérieur à l'élément recherché:
- Jetez la seconde moitié du tableau.
- Répéter
- S'il est inférieur à l'élément recherché:
- Jetez 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 examinerions d'abord l'élément du milieu:
1 3 5 7 9 11 13
^
Puisque 7> 5 et que le tableau est trié, nous savons pertinemment que le nombre 5 ne peut pas être dans la moitié arrière du tableau, nous pouvons donc simplement le rejeter. Cela laisse
1 3 5
Alors maintenant, nous regardons l'élément du milieu 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 premier demi-tableau pour quitter
5
Encore une fois, nous regardons le milieu de ce tableau:
5
^
Puisque c'est exactement le nombre que nous recherchons, nous pouvons signaler que 5 est bien dans le tableau.
Alors, à quel point 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à, donc nous continuons à réduire de moitié la taille du tableau jusqu'à ce que nous manquions d'éléments. Combien de temps cela prend-il? Eh bien, puisque nous continuons à couper le tableau en deux encore et encore, nous le ferons dans au plus O (log n) itérations, car nous ne pouvons pas couper le tableau en deux fois plus de O (log n) fois avant de démarrer hors des éléments du tableau.
Les algorithmes qui suivent la technique générale de diviser-pour-conquérir (couper le problème en morceaux, résoudre ces morceaux, puis remettre le problème ensemble) ont tendance à contenir des termes logarithmiques pour cette même raison - vous ne pouvez pas continuer à couper un objet en moitié plus de O (log n) fois. Vous voudrez peut-être regarder le tri par fusion comme un excellent exemple de cela.
Traitement des valeurs un chiffre à la fois
Combien de chiffres le nombre n en base 10 contient-il? Eh bien, s'il y a k chiffres dans le nombre, alors nous aurions que le plus grand chiffre est un multiple de 10 k . Le plus grand nombre à k chiffres est 999 ... 9, k fois, et ceci est égal à 10 k + 1 - 1. Par conséquent, si nous savons que n contient k chiffres, alors nous savons que la valeur de n est au plus 10 k + 1 - 1. Si nous voulons résoudre k en fonction de n, nous obtenons
n ≤ 10 k + 1 - 1
n + 1 ≤ 10 k + 1
log 10 (n + 1) ≤ k + 1
(log 10 (n + 1)) - 1 ≤ k
D'où nous obtenons 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, réfléchissons à la complexité de l'ajout de deux grands nombres trop gros pour tenir dans un mot machine. Supposons que nous ayons ces nombres représentés en base 10, et nous appellerons les nombres m et n. Une façon de les ajouter est d'utiliser la méthode de l'école primaire - écrivez les nombres 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 troisième au dernier chiffre ("antépénultième"):
1 1
1 3 3 7
+ 2 0 6 5
==============
4 0 2
Enfin, nous ajoutons le chiffre du quatrième au dernier ("préantepenultimate" ... j'aime l'anglais):
1 1
1 3 3 7
+ 2 0 6 5
==============
3 4 0 2
Maintenant, combien de travail avons-nous fait? Nous effectuons un total de O (1) travail par chiffre (c'est-à-dire une quantité de travail constante), et il y a O (max {log n, log m}) chiffres totaux qui doivent être traités. Cela donne un total de complexité O (max {log n, log m}), car nous devons visiter chaque chiffre des deux nombres.
De nombreux algorithmes obtiennent un terme O (log n) en travaillant un chiffre à la fois dans une base. Un exemple classique est le tri par base , qui trie les entiers un chiffre à la fois. Il existe de nombreuses variantes de tri par base, mais elles s'exécutent généralement au temps O (n log U), où U est le plus grand entier possible qui est trié. La raison en est que chaque passage du tri prend O (n) temps, et il y a un total de O itérations (log U) nécessaires pour traiter chacun des chiffres O (log U) du plus grand nombre trié. De nombreux algorithmes avancés, tels que l'algorithme des chemins les plus courts de Gabow ou la version de mise à l'échelle de l' algorithme de débit maximal de Ford-Fulkerson , ont un terme logique dans leur complexité car ils fonctionnent un chiffre à la fois.
Quant à votre deuxième question sur la façon dont vous résolvez ce problème, vous voudrez peut-être vous pencher sur 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 avoir une meilleure idée de la façon de penser aux problèmes lorsque vous savez qu'il y a un terme logique dans le résultat, donc je vous déconseille de regarder la réponse tant que vous ne l'avez pas donnée. certains ont pensé.
J'espère que cela t'aides!
O(log n)
peut être vu comme: Si vous doublez la taille du problèmen
, votre algorithme n'a besoin que d'un nombre constant d'étapes de plus.