Cette question précédente traite de certains des facteurs susceptibles de donner à un algorithme une complexité O (log n).
Qu'est-ce qui ferait qu'un algorithme présente une complexité temporelle O (log log n)?
Les termes O (log log n) peuvent apparaître à différents endroits, mais il existe généralement deux routes principales qui arriveront à cette exécution.
Comme mentionné dans la réponse à la question liée, une méthode classique pour qu'un algorithme ait une complexité temporelle O (log n) consiste à réduire de façon répétitive la taille de l'entrée par un facteur constant à chaque itération. Si tel est le cas, l'algorithme doit se terminer après les itérations O (log n), car après avoir fait une division de O (log n) par une constante, l'algorithme doit réduire la taille du problème à 0 ou 1. C'est pourquoi, par exemple , la recherche binaire a une complexité O (log n).
Il est intéressant de noter qu’il existe un moyen similaire de réduire la taille d’un problème qui donne des durées d’exécution de la forme O (log log n). Au lieu de diviser l’entrée en deux à chaque couche, que se passe-t-il si nous prenons la racine carrée de la taille à chaque couche?
Par exemple, prenons le nombre 65 536. Combien de fois devons-nous diviser cela par 2 jusqu'à ce que nous passions à 1? Si nous faisons cela, nous obtenons
Ce processus prend 16 étapes, et il est également le cas que 65 536 = 216.
Mais, si nous prenons la racine carrée à chaque niveau, nous obtenons
Notez qu'il suffit de quatre étapes pour atteindre le niveau 2. Pourquoi? Eh bien, réécrivons cette séquence en termes de puissances de deux:
Notez que nous avons suivi la séquence 216 → 28 → 24 → 22 → 21. A chaque itération, nous réduisons l'exposant de la puissance de deux en deux. C'est intéressant, car cela renvoie à ce que nous savons déjà - vous ne pouvez diviser le nombre k en deux fois et demi (log k) avant qu'il ne tombe à zéro.
Alors prenez n'importe quel nombre n et écrivez-le comme n = 2k. Chaque fois que vous prenez la racine carrée de n, vous divisez l’exposant par deux dans cette équation. Par conséquent, il ne peut y avoir que 0 (log k) racines carrées appliquées avant que k tombe à 1 ou moins (dans ce cas, n tombe à 2 ou moins). Puisque n = 2k, cela signifie que k = log2 n, et donc le nombre de racines carrées prises est O (log k) = O (log log n). Par conséquent, si un algorithme fonctionne en réduisant à plusieurs reprises le problème à un sous-problème de taille représentant la racine carrée de la taille du problème d'origine, cet algorithme se terminera après les étapes O (log log n).
Un exemple concret de ceci est la structure de données de van Emde Boas (vEB-tree). Un vEB-tree est une structure de données spécialisée permettant de stocker des entiers compris dans la plage 0 ... N - 1. Il fonctionne comme suit: le nœud racine de l’arbre contient √N pointeurs, séparant ainsi la plage 0 ... N - 1 dans √N compartiments contenant chacun une plage d'environ √N nombres entiers. Ces compartiments sont ensuite chacun subdivisé de manière interne en √ (√ N), chacun contenant environ √ (√ N) éléments. Pour parcourir l'arborescence, commencez à la racine, déterminez le compartiment auquel vous appartenez, puis continuez de manière récursive dans la sous-arborescence appropriée. En raison de la structure de l’arborescence vEB, vous pouvez déterminer en O(1) le sous-arbre dans lequel descendre, et ainsi, après les étapes O (log log N), vous atteindrez le bas de l’arborescence. . En conséquence, les recherches dans une arborescence de vEB ne prennent que du temps O (log log N).
Un autre exemple est l'algorithme de paire de points le plus proche Hopcroft-Fortune . Cet algorithme tente de trouver les deux points les plus proches dans une collection de points 2D. Cela fonctionne en créant une grille de compartiments et en répartissant les points dans ces compartiments. Si, à un moment quelconque de l'algorithme, un compartiment contenant plus de √N est détecté, l'algorithme traite ce compartiment de manière récursive. La profondeur maximale de la récursivité est donc O (log log n), et en analysant l'arbre de récursion, il est possible de montrer que chaque couche de l'arborescence fonctionne O(n). Par conséquent, le temps d'exécution total de l'algorithme est O (n log log n).
Il existe certains autres algorithmes qui réalisent des exécutions O (log log n) en utilisant des algorithmes tels que la recherche binaire sur des objets de taille O (log n). Par exemple, la structure de données x-fast trie effectue une recherche binaire sur les couches de l'arborescence de hauteur O (journal U), de sorte que le temps d'exécution de certaines de ses opérations est O (journal U) La partie liée y-fast trie obtient une partie de ses exécutions O (log log U) en maintenant des BST équilibrés de nœuds O (log U) chacun, permettant ainsi aux recherches dans ces arbres de s'exécuter dans le temps O ( log log U). L'arbre tango et les arbres liés multisplay se retrouvent avec un terme O (log log n) dans leurs analyses car ils conservent des arbres contenant O (log n) articles chacun.
D'autres algorithmes réalisent l'exécution O (log log n) d'une autre manière. Recherche d'interpolation s'attend à ce que le moteur d'exécution O (log log n) trouve un nombre dans un tableau trié, mais l'analyse est assez complexe. En fin de compte, l’analyse montre que le nombre d’itérations est égal au nombre k tel que n2-k ≤ 2, pour lequel log log n est la solution correcte. Certains algorithmes, comme l'algorithme Algorithme Cheriton-Tarjan MST , aboutissent à une exécution impliquant O (log log n) en résolvant un problème complexe d'optimisation sous contrainte.
J'espère que cela t'aides!
Une façon de voir le facteur O (log log n) dans la complexité temporelle est une division, comme ce qui est expliqué dans l’autre réponse, mais il existe une autre façon de voir ce facteur lorsque nous voulons faire un échange entre le temps et l’espace/le temps. et approximation/temps et dureté/... des algorithmes et nous avons une itération artificielle sur notre algorithme.
Par exemple, SSSP (chemin le plus court d'une source unique) a un algorithme O(n)] sur les graphes planaires, mais avant cet algorithme compliqué, il existait un algorithme beaucoup plus simple (mais toujours assez dur) avec le temps d'exécution O (n log log n), la base de l’algorithme est la suivante (juste une description très approximative, et je proposerais de ne pas comprendre cette partie et de lire l’autre partie de la réponse):
Mais ce que je veux dire, c’est que nous choisissions ici la division de taille O (log n/(log log n)). Si nous choisissons d’autres divisions comme O (log n/(log log n) ^ 2), elles risquent d’être plus rapides et d’apporter un autre résultat. Je veux dire, dans de nombreux cas (comme dans les algorithmes d'approximation ou les algorithmes aléatoires, ou les algorithmes tels que SSSP comme ci-dessus), lorsque nous itérons sur quelque chose (sous-problèmes, solutions possibles, ...), nous choisissons le nombre d'itérations correspondant au commerce de celui-ci. nous avons (temps/espace/complexité de l'algorithme/facteur constant de l'algorithme, ...). Alors peut-être voyons-nous plus de choses compliquées que "log log n" dans de vrais algorithmes.