Niveau : licence, du facile au difficile. Durée estimée : 90 min. 10 exercices avec corrections détaillées.
Rappel des notions clés
Pour trouver la complexité d'un algorithme, compte ses opérations élémentaires (comparaisons, échanges) selon la taille $n$ des données, puis garde le terme dominant. Exemple : deux boucles imbriquées de $n$ tours font $n^2$ comparaisons, donc la complexité est en $\Theta(n^2)$, dite quadratique.
$O(g(n))$ signifie : au plus de l'ordre de $g(n)$, à une constante près, pour $n$ grand ; $\Theta(g(n))$ : exactement de cet ordre.
Formules à retenir :
- $1 + 2 + \dots + (n-1) = \dfrac{n(n-1)}{2}$, en $\Theta(n^2)$.
- $\log_2(2^k) = k$ et $2^{10} = 1024 \approx 10^3$, donc $\log_2(10^6) \approx 20$.
- Dichotomie dans un tableau trié de taille $n$ : au plus $\lfloor \log_2 n \rfloor + 1$ tours.
| Tri | Principe | Pire cas |
|---|---|---|
| Par sélection | amène le minimum restant en tête de la partie non triée | $\Theta(n^2)$ |
| À bulles | échange les voisins mal rangés | $\Theta(n^2)$ |
| Par insertion | insère chaque élément dans la partie triée à sa gauche | $\Theta(n^2)$ |
| Fusion | trie chaque moitié, puis les fusionne | $\Theta(n\log n)$ |
| Rapide | répartit autour d'un pivot (un élément choisi), puis trie chaque côté | $\Theta(n^2)$ |
Piège classique : donner une complexité sans dire le cas, meilleur, pire ou moyen. Le tri rapide est en $\Theta(n\log n)$ en moyenne, mais si le pivot est toujours le premier élément, un tableau déjà trié lui coûte $\Theta(n^2)$.
Exercices : niveau facile
Exercice 1 : Un programme traite $n$ fiches clients. Pour $n = 10\,000$, il met 2 s (données fictives). Estime son temps pour $n = 100\,000$ s'il est en $\Theta(n)$, en $\Theta(n\log n)$, puis en $\Theta(n^2)$, en supposant le temps proportionnel au nombre d'opérations.
Correction :
Quand $n$ est multiplié par 10, le temps est multiplié par $10$ en $\Theta(n)$, par $10^2 = 100$ en $\Theta(n^2)$, et par $\dfrac{10^5 \log 10^5}{10^4 \log 10^4} = 10 \times \dfrac{5}{4} = 12{,}5$ en $\Theta(n\log n)$.
Résultat : 20 s en $\Theta(n)$, 25 s en $\Theta(n\log n)$, 200 s en $\Theta(n^2)$.
Exercice 2 : Applique le tri par sélection au tableau [64, 25, 12, 22, 11]. Donne l'état après chaque passage, puis le nombre de comparaisons. Généralise à $n$ éléments.
Correction :
Chaque passage échange le minimum de la partie non triée avec sa première case.
passage 1 : [11, 25, 12, 22, 64]
passage 2 : [11, 12, 25, 22, 64]
passage 3 : [11, 12, 22, 25, 64]
passage 4 : [11, 12, 22, 25, 64]
Trouver le minimum de $k$ éléments coûte $k - 1$ comparaisons : $4 + 3 + 2 + 1 = 10$.
Résultat : 10 comparaisons, et $\dfrac{n(n-1)}{2}$ pour $n$ éléments, quel que soit l'ordre de départ.
Exercice 3 : Applique le tri à bulles au tableau [5, 1, 4, 2, 8]. Donne l'état après chaque passage. Combien de passages fait la version qui s'arrête après un passage sans échange ?
Correction :
Chaque passage pousse le plus grand élément restant vers la fin.
passage 1 : [1, 4, 2, 5, 8] (3 échanges)
passage 2 : [1, 2, 4, 5, 8] (1 échange)
passage 3 : aucun échange, tableau trié, arrêt
Résultat : 3 passages, contre $n - 1 = 4$ pour la version sans arrêt.
Exercice 4 : Tu fais une recherche linéaire dans [15, 8, 23, 42, 10, 5, 17]. a) Combien de comparaisons pour trouver 42 ? pour conclure que 99 est absent ? b) Si la valeur est présente, chaque position ayant la même probabilité, combien de comparaisons en moyenne pour $n$ éléments ? et ici ?
Correction :
a) Les cases sont testées dans l'ordre : 4 comparaisons pour 42, la quatrième case ; 7 pour 99, absent.
b) Une valeur en position $k$ coûte $k$ comparaisons. En moyenne : $\dfrac{1 + 2 + \dots + n}{n} = \dfrac{n+1}{2}$, soit 4 pour $n = 7$.
Résultat : 4 et 7 comparaisons ; $\dfrac{n+1}{2}$ en moyenne, soit 4 ici. Le cas moyen reste en $\Theta(n)$.
Exercices : niveau moyen
Exercice 5 : Cherche 70, puis 25, par dichotomie dans le tableau trié [10, 20, 30, 40, 50, 60, 70, 80, 90], d'indices 0 à 8, en partant de $\text{debut} = 0$ et $\text{fin} = 8$. À chaque tour, $\text{milieu} = \lfloor (\text{debut} + \text{fin})/2 \rfloor$ ; si la case du milieu est trop petite, $\text{debut} = \text{milieu} + 1$ ; trop grande, $\text{fin} = \text{milieu} - 1$. On s'arrête quand on trouve, ou quand $\text{debut} > \text{fin}$. Donne debut, fin et milieu à chaque tour.
Correction :
À chaque tour, on garde la moitié qui peut encore contenir la valeur.
recherche de 70
debut=0 fin=8 milieu=4 50 trop petit
debut=5 fin=8 milieu=6 trouvé
recherche de 25
debut=0 fin=8 milieu=4 50 trop grand
debut=0 fin=3 milieu=1 20 trop petit
debut=2 fin=3 milieu=2 30 trop grand
debut=2 fin=1 arrêt, absent
Résultat : 70 trouvé à l'indice 6 en 2 tours ; 25 absent, constaté en 3 tours. Erreur à éviter : s'arrêter quand $\text{debut} = \text{fin}$, alors qu'il reste une case à tester.
Exercice 6 : Pour chaque fragment Python ($n \geq 1$), compte exactement les tours de la boucle la plus intérieure, puis donne la complexité.
# a
for i in range(n):
for j in range(n):
print(i, j)
# b
for i in range(n):
for j in range(i + 1):
print(i, j)
# c
i = 1
while i < n:
i = 2 * i
Correction :
Compter les tours, puis garder le terme dominant.
a) $n \times n = n^2$ tours : $\Theta(n^2)$.
b) Pour chaque $i$, la boucle sur $j$ fait $i + 1$ tours : $1 + 2 + \dots + n = \dfrac{n(n+1)}{2}$. Diviser par 2 ne change pas l'ordre de grandeur : $\Theta(n^2)$.
c) Après $k$ tours, $i = 2^k$ ; la boucle s'arrête dès que $2^k \geq n$, soit après $\lceil \log_2 n \rceil$ tours : $\Theta(\log n)$.
Résultat : $\Theta(n^2)$, $\Theta(n^2)$ et $\Theta(\log n)$. Ce sont les tours qui comptent, pas les boucles écrites.
Exercice 7 : Un tableau contient $n = 10^6$ identifiants (une comparaison par tour de dichotomie). a) Dans le pire cas, combien de comparaisons pour une recherche linéaire ? pour une dichotomie, si le tableau est trié ? b) Il n'est pas trié, et le trier coûte environ $n\log_2 n$ comparaisons. À partir de combien de recherches vaut-il mieux le trier d'abord ?
Correction :
Pour $k$ recherches, on compare les deux stratégies.
a) Linéaire : $10^6$. Dichotomie : $2^{19} = 524\,288 \leq 10^6 < 2^{20}$, donc $\lfloor \log_2 10^6 \rfloor + 1 = 20$.
b) Sans tri : $k \times 10^6$. Avec tri : $10^6 \log_2 10^6 + 20k \approx 2 \times 10^7 + 20k$. Trier gagne quand $10^6 k > 2 \times 10^7 + 20k$, soit $k > \dfrac{2 \times 10^7}{10^6 - 20} \approx 20$.
Résultat : $10^6$ contre 20 comparaisons ; trier d'abord devient rentable vers 20 recherches. Pour une seule recherche, c'est une perte : $n\log n$ dépasse $n$.
Exercice 8 : Applique le tri par insertion à [12, 11, 13, 5, 6]. Donne l'état après chaque insertion et le nombre total de comparaisons, puis compare-le au meilleur et au pire cas pour $n = 5$.
Correction :
L'élément remonte tant que son voisin de gauche est plus grand ; chaque test est une comparaison.
insérer 11 : 1 comparaison [11, 12, 13, 5, 6]
insérer 13 : 1 comparaison [11, 12, 13, 5, 6]
insérer 5 : 3 comparaisons [5, 11, 12, 13, 6]
insérer 6 : 4 comparaisons [5, 6, 11, 12, 13]
Total : $1 + 1 + 3 + 4 = 9$. Meilleur cas (déjà trié) : $n - 1 = 4$ ; pire cas (trié à l'envers) : $\dfrac{n(n-1)}{2} = 10$.
Résultat : 9 comparaisons, entre 4 et 10. Erreur à éviter : comparer aussi 13 à 11, alors que $13 > 12$ arrête déjà la boucle.
Exercices : niveau difficile
Exercice 9 : Soit la fonction Python suivante, pour un entier $n \geq 0$ (une multiplication coûte une opération, un nombre occupe une case mémoire).
def factorielle(n):
if n == 0:
return 1
return n * factorielle(n - 1)
a) Écris la récurrence vérifiée par $M(n)$, le nombre de multiplications de factorielle(n), et résous-la. b) Combien d'appels sont en cours au plus profond de la récursion ? Compare la mémoire utilisée à celle d'une boucle.
Correction :
On traduit le code en récurrence et on la déroule.
a) $M(0) = 0$ et, pour $n \geq 1$, $M(n) = M(n-1) + 1$ : une multiplication, plus celles de l'appel sur $n - 1$. Donc $M(n) = n$, et le temps est en $\Theta(n)$.
b) Au plus profond, les $n + 1$ appels, de factorielle(n) à factorielle(0), attendent tous sur la pile d'appels (la mémoire réservée aux appels en cours). La mémoire est en $\Theta(n)$, contre $\Theta(1)$ pour une boucle.
Résultat : $M(n) = n$ ; temps en $\Theta(n)$ ; mémoire en $\Theta(n)$ en récursif, $\Theta(1)$ avec une boucle.
Exercice 10 : a) Pour $n = 2^k$, le nombre $T(n)$ de comparaisons du tri fusion vérifie $T(1) = 0$ et $T(n) = 2T(n/2) + n$, où $n$ majore le coût de la fusion. Montre par récurrence sur $k$ que $T(2^k) = k\,2^k$, puis calcule $T(1024)$. b) Un camarade affirme que le tri rapide est lui aussi en $n\log n$. Teste-le sur le tableau trié [1, 2, ..., n], avec le premier élément comme pivot : chaque partage compare le pivot aux autres éléments de sa partie. Compte les comparaisons, calcule-les pour $n = 1024$, et dis s'il a raison.
Correction :
a) Initialisation, $k = 0$ : $T(1) = 0 = 0 \times 2^0$. Hérédité : si $T(2^{k-1}) = (k-1)\,2^{k-1}$, alors $T(2^k) = 2(k-1)\,2^{k-1} + 2^k = (k-1)\,2^k + 2^k = k\,2^k$. Avec $1024 = 2^{10}$ : $T(1024) = 10 \times 1024 = 10\,240$.
b) Sur un tableau trié, le pivot est le plus petit élément : une partie est vide, l'autre garde les $n - 1$ éléments suivants, encore triés. Le scénario se répète : $(n-1) + (n-2) + \dots + 1 = \dfrac{n(n-1)}{2}$ comparaisons, en $\Theta(n^2)$. Pour $n = 1024$ : $\dfrac{1024 \times 1023}{2} = 523\,776$.
Résultat : $10\,240$ comparaisons au plus pour le tri fusion, $523\,776$ pour ce tri rapide. Le camarade a tort : le tri rapide est en $\Theta(n\log n)$ en moyenne, pas dans le pire cas. Un pivot tiré au hasard rend ce cas très improbable.
Bilan et conseils
Ce qu'il faut retenir : compte exactement, puis garde le terme dominant ; dis toujours de quel cas tu parles.
En Python, le module bisect fait la dichotomie pour toi dans une liste triée (documentation de Python). En Java ou en C, calcule le milieu par debut + (fin - debut) / 2 : debut + fin peut dépasser la capacité d'un int (bug décrit par Joshua Bloch en 2006).
Sources
Documentation officielle de Python, module bisect. Joshua Bloch, billet sur la recherche dichotomique, blog de Google Research, 2 juin 2006.