Apprendre Algorithmes et complexité depuis zéro
1Ce que mesure la complexité
Un algorithme est une liste finie d’étapes précises qui résout un problème, comme trier une liste ou trouver un nom dans un annuaire.
Deux algorithmes peuvent résoudre le même problème mais prendre des temps très différents. La complexité décrit comment le travail croît quand l’entrée grandit.
La taille de l’entrée se note n : le nombre d’éléments à trier, la longueur d’une liste, le nombre de nœuds d’un graphe.
Imaginez que vous cherchez un nom dans un annuaire de 1000 pages. Lire chaque page demande jusqu’à 1000 étapes. L’ouvrir au milieu et diviser par deux à chaque fois demande environ 10 étapes.
La complexité en temps compte les étapes. La complexité en espace compte la mémoire supplémentaire. Toutes deux s’écrivent comme une fonction de n.
En général, on regarde le pire cas : le plus de travail sur toutes les entrées de taille n. Parfois, c’est le cas moyen ou le meilleur cas qui est demandé.
Auto-évaluation Un algorithme dont le travail croît comme n² prend 1 seconde pour n = 1000. Environ combien de temps prend-il pour n = 2000 ?
- 1 seconde
- 2 secondes
- 4 secondes
- 8 secondes
Réponse: 4 secondes. Doubler n multiplie n² par 2² = 4, donc le temps passe d’environ 1 à environ 4 secondes. 2 secondes serait la réponse pour une croissance linéaire, et 8 pour n³.
2Grand O et classes de croissance
La notation grand O ne garde que ce qui compte pour de grands n. Vous supprimez les facteurs constants et ne gardez que le terme qui croît le plus vite.
3n² + 5n + 2 ⇒ O(n²)
Pour n = 1000, le terme en n² vaut 3 000 000, alors que 5n n’est que 5000. Les termes plus petits changent à peine le total.
Ces classes reviennent sans cesse, de la croissance la plus lente à la plus rapide :
- O(1), constant : lire un élément d’un tableau par son indice.
- O(log n), logarithmique : la recherche dichotomique, qui divise le problème par deux à chaque étape.
- O(n), linéaire : un parcours de tous les éléments, comme trouver la plus grande valeur.
- O(n log n) : les bons algorithmes de tri, comme le tri fusion.
- O(n²), quadratique : deux boucles imbriquées sur les éléments, comme le tri à bulles.
- O(2ⁿ), exponentiel : essayer tous les sous-ensembles. Cela devient vite inutilisable.
Le grand O est une borne supérieure : le travail croît au plus à cette vitesse. Ω (grand Oméga) est une borne inférieure, et Θ (Thêta) est une borne serrée, à la fois supérieure et inférieure.
La base d’un logarithme n’a pas d’importance en grand O, car des logarithmes de bases différentes ne diffèrent que d’un facteur constant.
Auto-évaluation Quel est le grand O de 4n + 2n² + 100 ?
- O(100)
- O(2n²)
- O(n)
- O(n²)
Réponse: O(n²). Pour de grands n, le terme en n² domine, et le facteur constant 2 est supprimé. O(n) garde le mauvais terme, et O(2n²) oublie de supprimer la constante.
3Recherche, tri et récursivité
Quelques algorithmes reviennent dans presque tous les cours. Apprenez leurs complexités comme des faits.
- La recherche linéaire examine les éléments un par un : O(n). La recherche dichotomique demande un tableau trié et divise l’intervalle par deux à chaque étape : O(log n).
- Les tris à bulles, par insertion et par sélection comparent et échangent des éléments dans des boucles imbriquées : O(n²). Le tri par insertion est en O(n) sur un tableau déjà trié.
- Le tri fusion coupe le tableau en deux moitiés, les trie et les fusionne : O(n log n) dans tous les cas, avec O(n) de mémoire supplémentaire.
- Le tri rapide (quicksort) découpe autour d’un pivot : O(n log n) en moyenne, mais O(n²) dans le pire cas, par exemple un tableau trié avec le premier élément comme pivot.
- Aucun tri qui ne fait que comparer des éléments ne peut faire mieux que Ω(n log n) dans le pire cas. Le tri par comptage est plus rapide parce qu’il ne compare pas.
Une fonction récursive s’appelle elle-même sur une entrée plus petite jusqu’à atteindre un cas de base auquel elle répond directement. Son temps d’exécution s’écrit sous forme de récurrence :
- T(n) = T(n/2) + O(1) donne O(log n), comme la recherche dichotomique.
- T(n) = 2T(n/2) + O(n) donne O(n log n), comme le tri fusion.
- T(n) = T(n − 1) + O(n) donne O(n²).
- T(n) = 2T(n − 1) + O(1) donne O(2ⁿ).
Trois idées de conception méritent d’être connues. Diviser pour régner découpe, résout et combine. Le glouton prend à chaque étape le meilleur choix local. La programmation dynamique mémorise les réponses aux sous-problèmes qui se recoupent pour qu’elles soient calculées une seule fois.
Auto-évaluation Quelle est la complexité en temps du tri rapide dans le pire cas ?
- O(log n)
- O(n log n)
- O(n)
- O(n²)
Réponse: O(n²). Avec de mauvais pivots, chaque découpage laisse n − 1 éléments d’un côté, ce qui donne O(n²). O(n log n) est son cas moyen, et le pire cas du tri fusion.
4La méthode pas à pas
Pour trouver la complexité en temps d’un morceau de code :
- Décidez ce qu’est n : la taille de l’entrée.
- Comptez combien de fois chaque boucle s’exécute. Une boucle qui ajoute 1 à chaque fois s’exécute environ n fois. Une boucle qui double ou divise par deux s’exécute environ log₂ n fois.
- Les boucles imbriquées se multiplient. Les boucles qui se suivent s’additionnent.
- Pour une fonction récursive, écrivez la récurrence : combien d’appels, sur quelle taille, plus le travail par appel.
- Gardez le terme qui croît le plus vite et supprimez les constantes.
- Vérifiez avec un petit nombre, par exemple n = 8 ou n = 1024.
Pour une question de théorie, nommez d’abord l’algorithme ou l’idée décrits, puis rappelez sa complexité et ses conditions (entrée triée, poids positifs, sous-problèmes qui se recoupent).
5Un exemple détaillé
Suivez chaque étape. Essayez de deviner la suivante avant de l’ouvrir.
Exemple détailléQuelle est la complexité en temps de ce code ? for i = 1 to n: (j = 1; while j < n: j = j × 2)
1n est le nombre jusqu’auquel les boucles s’exécutent.
2i va de 1 à n par pas de 1, donc la boucle extérieure s’exécute n fois.
3j prend les valeurs 1, 2, 4, 8, … et s’arrête quand il atteint n. Après k tours, j = 2k, donc la boucle s’arrête après environ log₂ n tours.
4La boucle intérieure est à l’intérieur de la boucle extérieure, donc les nombres se multiplient.
n · log₂ n ⇒ O(n log n)
5Pour n = 1024, la boucle intérieure s’exécute log₂ 1024 = 10 fois, donc le corps s’exécute 1024 · 10 = 10 240 fois. C’est bien moins que n² = 1 048 576.
6Erreurs fréquentes
- Multiplier des boucles qui se suivent. n + n = 2n est encore O(n).
- Garder les constantes, comme dans O(2n) ou O(n/2). Les deux sont O(n).
- Compter une boucle qui double comme n tours. Elle s’exécute environ log₂ n fois.
- Utiliser la recherche dichotomique sur des données non triées, ou sur une liste chaînée où le milieu ne peut pas être atteint en une étape.
- Donner le cas moyen du tri rapide quand le pire cas est demandé.
- Penser que grand O signifie « exactement ». O(n²) est une borne supérieure ; Θ(n²) est la croissance exacte.
Auto-évaluation Une fonction exécute une boucle de n itérations puis, séparément, une autre boucle de n itérations. Quelle est sa complexité en temps ?
- O(2n)
- O(log n)
- O(n)
- O(n²)
Réponse: O(n). Les boucles qui se suivent s’additionnent : n + n = 2n, et la constante est supprimée, donc O(n). O(n²) ne serait juste que si une boucle était à l’intérieur de l’autre.
7À quoi ressemblent les questions d’examen
- Facile : termes clés et faits. La complexité de la recherche dichotomique, du tri fusion ou de l’accès à un tableau, le sens de O, Ω et Θ, ce qu’est la récursivité.
- Moyen : application des concepts. La complexité d’un fragment de code, une récurrence à résoudre, comment le temps change quand n double, quel algorithme convient à une tâche.
- Difficile : cas et analyse. Le théorème maître, les pires cas du tri rapide, glouton contre programmation dynamique, et les classes P, NP et NP-complet.
P contient les problèmes qui peuvent être résolus en temps polynomial. NP contient les problèmes dont les solutions peuvent être vérifiées en temps polynomial. Tout problème de P est dans NP ; on ne sait toujours pas si P = NP.
Apprenez pas à pas dans l’applicationApprenez pas à pas dans l’application