Compter, borner, mesurer~: le coût d’un algorithme
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
L’outil qui juge un algorithme sans dépendre de la machine : compter ses opérations élémentaires pour obtenir une fonction de coût \(f(n)\), la classer par sa croissance (\(\mathcal{O}\), \(\Omega\), \(\Theta\)), distinguer les cas, résoudre la récurrence d’un algorithme récursif, puis confronter la prévision à des durées mesurées. Chaque comptage est déroulé sur le code Java du cours, pas à pas.
D’où l’on vient, où l’on va
Amont. Les listes, piles, files et deques de la Séance 1, sur tableau ou nœuds chaînés, et leurs coûts comptés : décalages, références suivies, coût amorti du doublement.
Aval. La Séance 3 compare la table de hachage, en \(\mathcal{O}(1)\) en moyenne, à la recherche séquentielle en \(\mathcal{O}(n)\) et à la dichotomie en \(\mathcal{O}(\log n)\) ; la Séance 4 borne la hauteur des arbres ; la Séance 6 résout la récurrence du tri fusion par le théorème maître.
int n = 20_000_000;
for (int essai = 1; essai <= 3; essai++) {
long debut = System.nanoTime();
long s = 0;
for (int i = 0; i < n; i++) {
s = s + i;
}
long fin = System.nanoTime();
System.out.println("essai " + essai + " : " + (fin - debut) / 1000
+ " us, s = " + s + ", " + (5L * n + 3) + " operations");
}Résultat \(\rightarrow\)
essai 1 : 12246 us, s = 199999990000000, 100000003 operations
essai 2 : 25955 us, s = 199999990000000, 100000003 operations
essai 3 : 12876 us, s = 199999990000000, 100000003 operations
Lecture. Le même calcul, exécuté trois fois de suite : 12,2 ms, 26,0 ms puis 12,9 ms, pour le même nombre d’opérations, \(5n+3=100\,000\,003\). La durée dépend de la machine, du compilateur à la volée de la JVM et de la charge du système ; le nombre d’opérations, non.
La Séance 1 a déclaré mauvaise une file servie par supprimer(0) sur la liste sur tableau (comme remove(0) sur une ArrayList) : elle est correcte, ses tests passent, et pourtant elle décale tous les éléments restants à chaque client servi. Ce jugement ne repose ni sur les tests, qui passent, ni sur le chronomètre, qui varie. Il faut une grandeur qui ne dépende ni du matériel ni du langage, et qui dise comment le coût évolue quand la taille \(n\) des données augmente.
La démarche de la séance
1. Compter les opérations élémentaires, selon une convention annoncée : une fonction \(f(n)\).
2. Classer \(f(n)\) dans une famille de croissance : \(\mathcal{O}\), \(\Omega\), \(\Theta\).
3. Confronter la prévision à des durées mesurées, par l’expérience du doublement.
Lecture. Le premier temps est arithmétique, le deuxième mathématique, le troisième expérimental ; ensemble, ils servent jusqu’à la fin du cours.
| instruction | opérations comptées |
|---|---|
s = s + i |
2 : une addition, une affectation |
i++ |
2 : une addition, une affectation |
tab[i] |
1 : un accès mémoire |
i < n |
1 : une comparaison |
s = 0 |
1 : une affectation |
int i = 0 |
1 : une affectation |
return x |
1 : un retour |
Une convention explicite plutôt qu’une vérité
D’autres présentations comptent s = s + i pour une seule opération. La classe de complexité ne change pas, car les constantes disparaissent à l’étape suivante ; mais tant qu’on manipule \(f(n)\), un résultat ne se discute que si la règle est écrite. Ici, on compte comme dans le tableau, et on l’annonce.
Modèle des opérations primitives : Goodrich, Tamassia et Goldwasser, Data Structures and Algorithms in Java, 6e éd., Wiley, 2014, ch. 4.
Lecture. Le fragment compté est la boucle elle-même : la déclaration de \(n\) et l’affichage du résultat restent hors du compte. Quand la boucle s’arrête par son test (sans break ni return), le test est évalué une fois de plus qu’il n’y a de tours : la dernière évaluation, fausse, fait sortir.
Sortie \(\rightarrow\)
3
Résultat \(\rightarrow\)
3 8 13 18 23 28 33 38 43 48 53
Un objet Compteur reçoit op.ajouter(k) à chaque instruction comptée : le compteur suit la convention, instruction par instruction, pendant une exécution réelle.
Lecture. Pour \(n=0,\dots,10\), le compteur donne \(3, 8, 13, \dots, 53\) : exactement \(5n+3\). Sans l’appel op.ajouter(1) placé après la boucle (le test qui fait sortir), il donnerait \(5n+2\) : c’est l’oubli le plus courant, une comparaison par tour au lieu d’une de plus que de tours.
Sortie \(\rightarrow\)
10
Lecture. Lignes 2–3 : c = 0, i = 0, test \(0<4\) : 3 opérations. Les cases grises (\(j<i\)) ne sont jamais visitées.
Lecture. Lignes 4–5 : j = i vaut 0, test \(j<n\) : tour \((0, 0)\), c passe à 1. Total : 7 opérations.
Lecture. Lignes 4–5 : j++, test \(j<n\) : tour \((0, 1)\), c passe à 2. Total : 12 opérations.
Lecture. Lignes 4–5 : j++, test \(j<n\) : tour \((0, 2)\), c passe à 3. Total : 17 opérations.
Lecture. Lignes 4–5 : j++, test \(j<n\) : tour \((0, 3)\), c passe à 4. Total : 22 opérations.
Lecture. Lignes 3–5 : j++, test \(j<n\) faux (fin de la ligne \(i=0\)), i++, test \(1<4\), j = i, test \(j<n\) : tour \((1, 1)\), c passe à 5. Total : 32 opérations.
Lecture. Lignes 4–5 : j++, test \(j<n\) : tour \((1, 2)\), c passe à 6. Total : 37 opérations.
Lecture. Lignes 4–5 : j++, test \(j<n\) : tour \((1, 3)\), c passe à 7. Total : 42 opérations.
Lecture. Lignes 3–5 : j++, test \(j<n\) faux (fin de la ligne \(i=1\)), i++, test \(2<4\), j = i, test \(j<n\) : tour \((2, 2)\), c passe à 8. Total : 52 opérations.
Lecture. Lignes 4–5 : j++, test \(j<n\) : tour \((2, 3)\), c passe à 9. Total : 57 opérations.
Lecture. Lignes 3–5 : j++, test \(j<n\) faux (fin de la ligne \(i=2\)), i++, test \(3<4\), j = i, test \(j<n\) : tour \((3, 3)\), c passe à 10. Total : 67 opérations.
Lecture. Ligne 8 : la ligne \(i\) fait \(n-i\) tours : \(4+3+2+1=10=n(n+1)/2\). Avec les sorties de boucle, 73 opérations.
La boucle triangulaire
La boucle interne fait \(n-i\) tours pour chaque \(i\) : \(\sum_{i=0}^{n-1}(n-i)=n(n+1)/2\) tours. Hors des tours internes, \(3+5n\) opérations (c = 0, i = 0, \(n+1\) tests externes, i++, j = i, le test interne qui sort) ; chaque tour interne en coûte 5 (un test, c = c + 1, j++) : \(f(n)=3+5n+5\cdot\frac{n(n+1)}{2}=2{,}5\,n^2+7{,}5\,n+3\).
Résultat \(\rightarrow\)
n = 5 : 15 tours, f(n) = 103
n = 10 : 55 tours, f(n) = 328
n = 1000 : 500500 tours, f(n) = 2507503
Lecture. \(f(5)=103\), comme le compteur. Le terme \(2{,}5n^2\) pèse \(250/328\approx76{,}2\) % de \(f(10)\) et \(2\,500\,000/2\,507\,503\approx99{,}7\) % de \(f(1\,000)\) : plus \(n\) grandit, plus le terme de plus haut degré écrase les autres.
Sortie \(\rightarrow\)
11 31 3 91 3 9 271 3 9 27 811 3 9 27 81 | 5 tours
Lecture. Lignes 2–3 : tours reçoit 0, i reçoit 1, et \(1<100\) : le premier tour commence.
Lecture. Lignes 4–5 : tour 1 : tours \(=1\), la valeur 1 \(=3^{0}\) s’affiche.
Lecture. Lignes 3–5 : i = i * 3 donne 3, et \(3<100\) ; tour 2 : tours \(=2\), la valeur 3 \(=3^{1}\) s’affiche.
Lecture. Lignes 3–5 : i = i * 3 donne 9, et \(9<100\) ; tour 3 : tours \(=3\), la valeur 9 \(=3^{2}\) s’affiche.
Lecture. Lignes 3–5 : i = i * 3 donne 27, et \(27<100\) ; tour 4 : tours \(=4\), la valeur 27 \(=3^{3}\) s’affiche.
Lecture. Lignes 3–5 : i = i * 3 donne 81, et \(81<100\) ; tour 5 : tours \(=5\), la valeur 81 \(=3^{4}\) s’affiche.
Lecture. Ligne 3 : i = i * 3 donne 243, et \(243<100\) est faux : la boucle s’arrête après 5 tours.
Lecture. Ligne 7 : 5 tours, car \(3^4=81<100\leq3^5\) : le nombre de tours est le plus petit \(k\) tel que \(3^k\geq n\), soit \(\lceil\log_3 n\rceil\).
Résultat \(\rightarrow\)
100 : 5 tours, contre 99 pour i++
1000 : 7 tours, contre 999 pour i++
10000 : 9 tours, contre 9999 pour i++
100000 : 11 tours, contre 99999 pour i++
1000000 : 13 tours, contre 999999 pour i++
L’erreur la plus fréquente de la séance
Lire i = i * 3 comme une boucle linéaire : multiplier \(n\) par 10 n’ajoute que 2 ou 3 tours (\(\log_3 10\approx2{,}1\)), contre \(9n\) en i++.
Lecture. La variable est-elle additionnée ou multipliée ? i vaut \(3^k\) après \(k\) tours : \(\lceil\log_3 n\rceil\) tours.
Le fragment
int c = 0;for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
c = c + 1;
| instruction | exécutions | opérations |
|---|---|---|
c = 0 |
1 | 1 |
i = 0 |
1 | 1 |
i < n |
\(n+1\) | \(n+1\) |
i++ |
\(n\) | \(2n\) |
j = 0 |
\(n\) | \(n\) |
j < n |
\(n(n+1)\) | \(n^2+n\) |
c = c + 1 |
\(n^2\) | \(2n^2\) |
j++ |
\(n^2\) | \(2n^2\) |
| total \(f(n)\) | \(5n^2+5n+3\) | |
| \(n=4\) | 16 tours | \(f(4)=103\) |
Correction
i++, j++, c = c + 1.Lecture. liste.add(0, i) : aucune boucle visible, mais sur une ArrayList les \(i\) éléments présents sont décalés (Séance 1) : \(n(n-1)/2\) décalages, \(1\,999\,000\) pour \(n=2\,000\). mediane chronomètre cinq exécutions et garde la valeur du milieu, moins sensible à une exécution perturbée.
for (int r = 0; r < 5; r++) { // chauffe
Chrono.mediane(() -> remplir(new ArrayList<>(), 4000));
Chrono.mediane(() -> remplir(new LinkedList<>(), 4000));
}
for (int n = 4000; n <= 64000; n *= 2) {
final int m = n;
double ta = Chrono.mediane(() -> remplir(new ArrayList<>(), m));
double tl = Chrono.mediane(() -> remplir(new LinkedList<>(), m));
System.out.printf(Locale.ROOT, "%5d ArrayList %7.2f ms LinkedList %5.2f ms%n",
n, ta, tl);
}Résultat \(\rightarrow\)
4000 ArrayList 0.32 ms LinkedList 0.09 ms
8000 ArrayList 1.15 ms LinkedList 0.17 ms
16000 ArrayList 6.92 ms LinkedList 0.33 ms
32000 ArrayList 37.39 ms LinkedList 0.65 ms
64000 ArrayList 161.10 ms LinkedList 0.44 ms
Lecture. ArrayList : rapports 3,6, 6,0, 5,4 puis 4,3, au-dessus de 4 entre 8 000 et 32 000 ; une seule série ne fixe pas l’exposant. LinkedList : sous la milliseconde, dans le bruit.
\(T(n)\approx a,n^b\) donne \(T(2n)/T(n)\approx2^b\)
Le rapport vaut environ 2 pour un coût linéaire (\(b=1\)), 4 pour un coût quadratique (\(b=2\)), 8 pour un coût cubique (\(b=3\)) : \(b\approx\log_2\bigl(T(2n)/T(n)\bigr)\), et la constante \(a\) disparaît.
Lecture. Sans doublon, \(n(n-1)/2\) comparaisons : rapport attendu 4. Le champ vu empêche la JVM de supprimer l’appel.
Sortie \(\rightarrow\)
2000 0.40 ms 3.92
4000 1.53 ms 3.85
8000 6.43 ms 4.20
16000 24.07 ms 3.74
32000 96.43 ms 4.01
Lecture. Ligne 3 : chauffe : cinq mesures à \(n=4\,000\), dont le résultat est jeté. La JVM interprète d’abord le code, puis le compile : les mesures suivantes portent sur le code compilé.
Lecture. Ligne 4 : \(T(1\,000)=0{,}101\) ms, médiane de cinq durées ; rien n’est affiché, la valeur sert de référence.
Lecture. Lignes 5–9 : \(n=2\,000\) : \(T(n)=0{,}40\) ms, rapport \(3{,}92\) au doublement précédent ; prec reçoit \(t\).
Lecture. Lignes 5–9 : \(n=4\,000\) : \(T(n)=1{,}53\) ms, rapport \(3{,}85\) au doublement précédent ; prec reçoit \(t\).
Lecture. Lignes 5–9 : \(n=8\,000\) : \(T(n)=6{,}43\) ms, rapport \(4{,}20\) au doublement précédent ; prec reçoit \(t\).
Lecture. Lignes 5–9 : \(n=16\,000\) : \(T(n)=24{,}07\) ms, rapport \(3{,}74\) au doublement précédent ; prec reçoit \(t\).
Lecture. Lignes 5–9 : \(n=32\,000\) : \(T(n)=96{,}43\) ms, rapport \(4{,}01\) au doublement précédent ; prec reçoit \(t\).
Lecture. Ligne 5 : \(64\,000>32\,000\) : fin ; rapports voisins de \(4=2^2\), comme \(n(n-1)/2\) le prévoit.
Lecture. Une autre exécution : contient donne 92,5 ms à \(n=32\,000\) au lieu de 96,4 ms, et des rapports de 3,86 à 4,07, compatibles avec \(b=2\). Les insertions en tête dans l’ArrayList sont plus dispersées (2,2 à 5,2) : on lit une tendance sur plusieurs doublements et plusieurs séries, pas un rapport isolé.
Quoi
Mesurer \(T(n)\) pour \(n, 2n, 4n, \dots\) et lire \(\log_2\) du rapport \(T(2n)/T(n)\) : l’exposant \(b\) du modèle \(T(n)\approx a\,n^b\).
Pourquoi on l’utilise
Il confirme ou réfute une analyse sans connaître la constante \(a\) ; il prévoit la durée à \(2n\) à partir de la dernière mesure : environ \(4\times96{,}4\approx386\) ms pour contient à \(n=64\,000\).
Quand ça échoue
Des durées de moins d’une milliseconde, dominées par le bruit ; une mesure sans chauffe, sur du code encore interprété ; un calcul dont le résultat n’est pas utilisé, que la JVM peut supprimer ; la charge de la machine (les deux séries) ; un coût par opération qui change avec \(n\) : pour l’ArrayList, environ 0,04 ns par décalage à \(n=8\,000\) et 0,07 ns à \(32\,000\), peut-être parce que le tableau dépasse une mémoire cache ; la mesure ne le démontre pas.
Notation \(\mathcal{O}\)
Soient \(f, g:\mathbb{N}\to\mathbb{R}^+\). On écrit \(f(n)\in\mathcal{O}(g(n))\) s’il existe deux constantes \(c>0\) et \(n_0\in\mathbb{N}\) telles que \(f(n)\leq c\,g(n)\) pour tout \(n\geq n_0\).
Lecture. \(c\) fait oublier les facteurs, \(n_0\) les petites tailles ; c’est une majoration : \(f\in\mathcal{O}(n^2)\) n’interdit pas \(f\) linéaire.
Résultat \(\rightarrow\)
c = 73 : n0 = 1
c = 5 : n0 = 13
c = 4 : n0 = 23
c = 3 : aucun n0
Preuve : \(3n^2+20n+50\in\mathcal{O}(n^2)\)
Pour tout \(n\geq1\), \(n\leq n^2\) et \(1\leq n^2\), donc \(3n^2+20n+50\leq3n^2+20n^2+50n^2=73\,n^2\) : le couple \((73, 1)\) satisfait la définition.
Lecture. \((73, 1)\), \((5, 13)\), \((4, 23)\) conviennent, aucun \(n_0\) pour \(c=3\) : prouver, c’est exhiber un couple.
Notations \(\Omega\) et \(\Theta\)
\(f(n)\in\Omega(g(n))\) s’il existe \(c>0\) et \(n_0\) tels que \(f(n)\geq c\,g(n)\) pour tout \(n\geq n_0\). \(f(n)\in\Theta(g(n))\) si \(f(n)\in\mathcal{O}(g(n))\) et \(f(n)\in\Omega(g(n))\).
Pour \(f, g, h\) à valeurs positives
Transitivité : \(f\in\mathcal{O}(g)\) et \(g\in\mathcal{O}(h)\) donnent \(f\in\mathcal{O}(h)\). (ii) Somme : \(\mathcal{O}(f)+\mathcal{O}(g)=\mathcal{O}(\max(f, g))\).
Produit : \(f_1\in\mathcal{O}(g_1)\) et \(f_2\in\mathcal{O}(g_2)\) donnent \(f_1f_2\in\mathcal{O}(g_1g_2)\). (iv) Constantes : \(\mathcal{O}(c\,f)=\mathcal{O}(f)\) pour \(c>0\).
Lecture. \(\mathcal{O}\) majore, \(\Omega\) minore, \(\Theta\) encadre : quand le coût est connu, on écrit \(\Theta\). La propriété (ii) dit que, dans une suite d’instructions, la plus coûteuse l’emporte ; (iii) justifie la multiplication des coûts des boucles imbriquées.
Quoi
Une classe de croissance, à une constante multiplicative près et pour \(n\) assez grand : \(2{,}5n^2+7{,}5n+3\in\Theta(n^2)\).
Pourquoi on l’utilise
Elle ne dépend ni de la machine, ni du langage, ni de la convention de comptage ; elle compare deux algorithmes sur les grandes tailles, celles où le choix pèse.
Quand ça échoue
Sur les petites tailles, la constante cachée décide : \(100n\) dépasse \(n^2\) tant que \(n<100\). Une borne lâche est vraie mais n’informe pas : \(n\in\mathcal{O}(n^3)\). Et \(\mathcal{O}\) ne dit rien du cas étudié : une borne porte sur une fonction, le cas sur les entrées.
System.out.println(" n log2 n n log2 n n^2 2^n");
for (long n : new long[] {10, 100, 1_000, 1_000_000}) {
double lg = Math.log(n) / Math.log(2);
String deux = n < 63 ? "" + (1L << n)
: "~10^" + Math.round(n * Math.log10(2));
System.out.printf(Locale.ROOT, "%7d %8.1f %11.0f %15d %s%n",
n, lg, n * lg, n * n, deux);
}Résultat \(\rightarrow\)
n log2 n n log2 n n^2 2^n
10 3.3 33 100 1024
100 6.6 664 10000 ~10^30
1000 10.0 9966 1000000 ~10^301
1000000 19.9 19931569 1000000000000 ~10^301030
Lecture. À \(n=10^6\), \(\log_2 n\approx20\), \(n\log_2 n\approx2\cdot10^7\) et \(n^2=10^{12}\) : à \(10^9\) opérations par seconde, un algorithme en \(n^2\) demande environ 17 minutes, un algorithme en \(n\log n\) 20 millisecondes. À la même vitesse, \(2^{100}\approx1{,}27\cdot10^{30}\) opérations demandent environ \(4\cdot10^{13}\) années.
Lecture. En échelle linéaire, \(2^n\) quitte le graphique avant \(n=10\) ; en échelle logarithmique, les cinq familles restent lisibles ensemble, et \(2^n\) devient une droite.
Trois gestes
Compter, garder le terme dominant, oublier les constantes :
\[ 2{,}5n^2+7{,}5n+3\;\rightarrow\;2{,}5n^2\;\rightarrow\;\Theta(n^2) \]
Lire une boucle sans tout compter
Successives : on additionne, le maximum l’emporte.
Imbriquées : on multiplie les nombres de tours.
Variable multipliée à chaque tour : \(\Theta(\log n)\) tours.
| fragment de la séance | classe |
|---|---|
| somme, \(5n+3\) | \(\Theta(n)\) |
| boucle triangulaire | \(\Theta(n^2)\) |
| boucle carrée, \(5n^2+5n+3\) | \(\Theta(n^2)\) |
i = i * 3 |
\(\Theta(\log n)\) |
| deux boucles successives sur \(n\) | \(\Theta(n)\) |
contient, sans doublon |
\(\Theta(n^2)\) |
\(n\) fois ArrayList.add(0, i) |
\(\Theta(n^2)\) |
Lecture. Deux boucles successives sur \(n\) font \(n+n=2n\) tours : \(\Theta(n)\), et non \(\Theta(n^2)\).
Cinq affirmations
Pour chacune, dire si elle est vraie ou fausse, et donner la raison en une ligne.
| affirmation | V / F | raison |
|---|---|---|
| \(5n^2+3n\in\mathcal{O}(n^3)\) | vrai | majoration, pas forcément serrée |
| \(5n^2+3n\in\Theta(n^3)\) | faux | pas de minoration par \(n^3\) |
| \(n^2\in\Omega(n\log n)\) | vrai | \(n^2/(n\log n)\to\infty\) |
| \(2^{n+1}\in\mathcal{O}(2^n)\) | vrai | \(2^{n+1}=2\cdot2^n\) |
| \(\mathcal{O}\) veut dire « pire cas » | faux | fonctions contre entrées |
Correction
\(f(n)=7n+40\)
| \(c\) | plus petit \(n_0\) | car |
|---|---|---|
| 47 | 1 | \(40\leq40n\) dès \(n\geq1\) |
| 8 | 40 | \(40\leq n\) |
| 11 | 10 | \(40\leq4n\) |
| 7 | aucun | \(7n+40>7n\) toujours |
Correction
Pour une même taille \(n\), le coût dépend du contenu : examinees compte les cases comparées à x.
| cas | situation | cases examinées |
|---|---|---|
| meilleur | x en case 0 |
1, \(\Theta(1)\) |
| pire | x en dernière case ou absent |
\(n\), \(\Theta(n)\) |
| moyen | x présent, position uniforme |
\((n+1)/2\), \(\Theta(n)\) |
Lecture. Au cas moyen, chaque position \(i\) (de 0 à \(n-1\)) a la probabilité \(1/n\) et coûte \(i+1\) cases : \(\frac1n\sum_{i=0}^{n-1}(i+1)=\frac{n+1}{2}\).
Sortie \(\rightarrow\)
-1
Lecture. Ligne 9 : chercher(t, 1200) : 1 200 FCFA n’est pas dans le tableau, la boucle ira jusqu’au bout.
Lecture. Lignes 2–3 : \(i=0\) : t[0] \(=125\neq1\,200\) ; case 1 examinée sur 10.
Lecture. Lignes 2–3 : \(i=1\) : t[1] \(=175\neq1\,200\) ; case 2 examinée sur 10.
Lecture. Lignes 2–3 : \(i=2\) : t[2] \(=225\neq1\,200\) ; case 3 examinée sur 10.
Lecture. Lignes 2–3 : \(i=3\) : t[3] \(=275\neq1\,200\) ; case 4 examinée sur 10.
Lecture. Lignes 2–3 : \(i=4\) : t[4] \(=350\neq1\,200\) ; case 5 examinée sur 10.
Lecture. Lignes 2–3 : \(i=5\) : t[5] \(=425\neq1\,200\) ; case 6 examinée sur 10.
Lecture. Lignes 2–3 : \(i=6\) : t[6] \(=550\neq1\,200\) ; case 7 examinée sur 10.
Lecture. Lignes 2–3 : \(i=7\) : t[7] \(=625\neq1\,200\) ; case 8 examinée sur 10.
Lecture. Lignes 2–3 : \(i=8\) : t[8] \(=750\neq1\,200\) ; case 9 examinée sur 10.
Lecture. Lignes 2–3 : \(i=9\) : t[9] \(=975\neq1\,200\) ; case 10 examinée sur 10.
Lecture. Lignes 5, 9 : la boucle s’arrête après les 10 cases : -1, cible absente. Au pire cas, \(n\) cases examinées ; le tableau trié n’a servi à rien.
Pourquoi le pire cas
Il fournit une garantie : jamais plus que cela, ce qui permet de dimensionner un système. Il ne suppose rien sur la distribution des données, alors que le cas moyen exige une hypothèse probabiliste (ici : une cible présente, à une position uniforme), souvent invérifiable.
Cas et notation sont deux questions distinctes
« Pire cas » désigne une famille d’entrées ; \(\mathcal{O}\) désigne une famille de fonctions. On parle du \(\Theta\) du meilleur cas comme du \(\mathcal{O}\) du pire cas : écrire « \(\mathcal{O}\) signifie pire cas » est une confusion fréquente, et fausse.
Lecture. Le cas moyen garde son intérêt quand l’hypothèse est contrôlée : la Séance 3 obtient un coût moyen constant pour la table de hachage sous l’hypothèse d’un hachage uniforme.
Deux algorithmes
contient(t) s’arrête au premier doublon. Insertion directe : chaque clé entre dans un tableau trié, en décalant les plus grandes.
| entrée | situation | coût |
|---|---|---|
4 8 15 16 23 42 |
aucun doublon : pire | 15 comp. |
4 4 15 16 23 42 |
doublon en tête : meilleur | 1 comp. |
4 8 15 16 23 4 |
aux deux bouts | 5 comp. |
64 52 39 27 13 |
ordre décroissant : pire | 10 déc. |
13 27 39 52 64 |
ordre croissant : meilleur | 0 déc. |
Correction
Poser la récurrence
Sur un tableau trié, chaque appel examine une case et garde au plus la moitié de l’intervalle : \(T(n)=T(n/2)+\Theta(1)\), \(T(1)=\Theta(1)\). Après \(k\) appels, l’intervalle a pour taille \(n/2^k\) ; la récursion s’arrête quand \(n/2^k<1\), soit après \(\lfloor\log_2 n\rfloor+1\) appels qui examinent une case : \(T(n)\in\Theta(\log n)\).
Lecture. Le coût d’une méthode récursive s’exprime en fonction de lui-même, sur une entrée plus petite : c’est une récurrence.
Résultat \(\rightarrow\)
1.0 1.0
500.5 8.987
1000.0 10.0
Lecture. Séquentielle (première colonne) : 1 case, \(500{,}5=(n+1)/2\) en moyenne, \(1\,000\) pour 1 999 absente. Dichotomie : 1 case pour 998, au milieu, 8,987 en moyenne, 10 au pire, \(\lfloor\log_2 1\,000\rfloor+1\).
Sortie \(\rightarrow\)
-1
Lecture. Ligne 10 : premier appel sur tout le tableau : \(lo=0\), \(hi=9\) ; la même cible absente, 1 200.
Lecture. Lignes 2–3 : \(0\leq9\) : mid \(=0+(9-0)/2=4\) ; la case 4 est examinée.
Lecture. Lignes 4–5 : t[4] \(=350<1\,200\) : la cible ne peut être qu’à droite ; appel 2 sur \([5, 9]\), 5 cases.
Lecture. Lignes 2–3 : \(5\leq9\) : mid \(=5+(9-5)/2=7\) ; la case 7 est examinée.
Lecture. Lignes 4–5 : t[7] \(=625<1\,200\) : la cible ne peut être qu’à droite ; appel 3 sur \([8, 9]\), 2 cases.
Lecture. Lignes 2–3 : \(8\leq9\) : mid \(=8+(9-8)/2=8\) ; la case 8 est examinée.
Lecture. Lignes 4–5 : t[8] \(=750<1\,200\) : la cible ne peut être qu’à droite ; appel 4 sur \([9, 9]\), 1 case.
Lecture. Lignes 2–3 : \(9\leq9\) : mid \(=9+(9-9)/2=9\) ; la case 9 est examinée.
Lecture. Lignes 4–5 : t[9] \(=975<1\,200\) : la cible ne peut être qu’à droite ; appel 5 sur \([10, 9]\), intervalle vide.
Lecture. Ligne 2 : \(lo=10>hi=9\) : plus aucune case, -1.
Lecture. Ligne 10 : 4 cases examinées au lieu de 10 : chaque appel garde au plus la moitié de l’intervalle, d’où \(\lfloor\log_2 10\rfloor+1=4\) au pire.
Résultat \(\rightarrow\)
(lo + hi) / 2 = -397483648
lo + (hi - lo) / 2 = 1750000000
(lo + hi) / 2 déborde
En int, la somme lo + hi peut déborder dès qu’un indice dépasse \(2^{30}\) (\(2^{31}-1\) est le plus grand int) : le milieu devient négatif, et t[mid] lève ArrayIndexOutOfBoundsException. lo + (hi - lo) / 2 donne le même milieu sans jamais dépasser hi. Le code d’OpenJDK (Arrays.binarySearch) écrit (low + high) >>> 1, qui corrige aussi le débordement.
Lecture. \(1\,500\,000\,000+2\,000\,000\,000=3{,}5\cdot10^9\) dépasse \(2^{31}-1\approx2{,}15\cdot10^9\) : Java calcule modulo \(2^{32}\) et obtient \(-794\,967\,296\), puis \(-397\,483\,648\).
for (int n = 1_000; n <= 1_000_000; n *= 10) {
int[] t = new int[n];
for (int i = 0; i < n; i++) t[i] = 2 * i;
Recherche.examinees = 0;
Recherche.sequentielle(t, 2 * n); // absente : pire cas
long seq = Recherche.examinees;
Recherche.examinees = 0;
Recherche.dicho(t, 2 * n, 0, n - 1); // absente
System.out.println(n + " : " + seq + " et " + Recherche.examinees);
}Résultat \(\rightarrow\)
1000 : 1000 et 10
10000 : 10000 et 14
100000 : 100000 et 17
1000000 : 1000000 et 20
Lecture. Au pire cas (valeur absente), \(n\) cases contre \(\lfloor\log_2 n\rfloor+1\) : 20 au lieu d’un million. Multiplier \(n\) par 10 ajoute 3 ou 4 cases à la dichotomie, et multiplie par 10 le travail de la recherche séquentielle. En contrepartie, le tableau doit rester trié : une insertion y décale en moyenne la moitié des cases.
Théorème maître (CLRS, § 4.5)
Soit \(T(n)=a\,T(n/b)+f(n)\), avec \(a\geq1\), \(b>1\), et \(\alpha=\log_b a\).
Cas 1. Si \(f(n)\in\mathcal{O}(n^{\alpha-\varepsilon})\) pour un \(\varepsilon>0\), alors \(T(n)\in\Theta(n^{\alpha})\).
Cas 2. Si \(f(n)\in\Theta(n^{\alpha})\), alors \(T(n)\in\Theta(n^{\alpha}\log n)\).
Cas 3. Si \(f(n)\in\Omega(n^{\alpha+\varepsilon})\) pour un \(\varepsilon>0\), et si \(a\,f(n/b)\leq c\,f(n)\) pour un \(c<1\) et \(n\) assez grand, alors \(T(n)\in\Theta(f(n))\).
Le tri fusion
Couper en deux, trier chaque moitié, fusionner en temps linéaire : \(T(n)=2T(n/2)+\Theta(n)\), \(a=b=2\), \(\alpha=1\), \(f(n)\in\Theta(n^1)\) : cas 2, \(T(n)\in\Theta(n\log n)\) (Séance 6).
Lecture. Deux forces s’affrontent : le travail des feuilles, mesuré par \(n^{\alpha}\), et le travail à chaque niveau, mesuré par \(f(n)\). Les feuilles l’emportent (cas 1), l’équilibre ajoute un facteur \(\log n\) (cas 2), la racine l’emporte (cas 3).
Lecture. 11 niveaux, \(2^j\) appels au niveau \(j\). À gauche, le dernier niveau coûte 1 024 sur 2 047 ; au centre, chaque niveau 1 024 ; à droite, la racine \(1\,048\,576\) sur \(2\,096\,128\).
Résultat \(\rightarrow\)
f(n) = n^0 : 2047 puis 4095, rapport 2.00
f(n) = n^1 : 11264 puis 24576, rapport 2.18
f(n) = n^2 : 2096128 puis 8386560, rapport 4.00
Lecture. Doubler \(n\) multiplie \(T\) par 2,00 (cas 1), \(2{,}18=2\cdot12/11\) (cas 2) et 4,00 (cas 3).
Quoi
La classe de \(T(n)=a\,T(n/b)+f(n)\), obtenue en comparant \(f(n)\) à \(n^{\log_b a}\).
Pourquoi on l’utilise
Il donne la classe d’un algorithme « diviser pour régner » sans dérouler la récurrence : dichotomie, tri fusion, et les algorithmes récursifs des séances suivantes.
Quand ça échoue
Des sous-problèmes qui ne sont pas des fractions de \(n\) : \(T(n)=T(n-1)+n\) (hors du théorème, \(\Theta(n^2)\) en déroulant). Une fonction \(f\) entre deux cas : \(T(n)=2T(n/2)+n/\log n\). Le cas 3 sans la condition de régularité.
Quatre récurrences
Pour chacune : \(a\), \(b\), \(\alpha=\log_b a\), le cas, la classe de \(T(n)\).
| récurrence | \(a\) | \(b\) | \(\alpha=\log_b a\) | cas | \(T(n)\) |
|---|---|---|---|---|---|
| \(2T(n/2)+\Theta(1)\) | 2 | 2 | 1 | 1 | \(\Theta(n)\) |
| \(T(n/2)+\Theta(1)\) | 1 | 2 | 0 | 2 | \(\Theta(\log n)\) |
| \(4T(n/2)+\Theta(n)\) | 4 | 2 | 2 | 1 | \(\Theta(n^2)\) |
| \(2T(n/2)+\Theta(n)\) | 2 | 2 | 1 | 2 | \(\Theta(n\log n)\) |
Correction
\(n\) clients servis
La file servie par supprimer(0) sur la liste sur tableau de la Séance 1 (comme remove(0) sur une ArrayList), et la file sur tableau circulaire, qui avance un indice par dequeue.
dequeue par supprimer(0), et sa classe.| \(n\) | supprimer(0) |
tableau circulaire | rapport |
|---|---|---|---|
| 2 000 | 1 999 000 déc. | 2 000 indices | 999,5 |
| 20 000 | 199 990 000 déc. | 20 000 indices | 9 999,5 |
Correction
supprimer(0) décale les \(n-k\) éléments restants : \(\sum_{k=1}^{n}(n-k)=n(n-1)/2\), \(\Theta(n^2)\).dequeue, aucun décalage : \(\Theta(n)\).| outil | question | résultat de la séance |
|---|---|---|
| comptage | combien d’opérations, exactement ? | somme \(5n+3\) ; triangle \(2{,}5n^2+7{,}5n+3\), \(f(4)=73\) |
| tours de boucle | additionnée ou multipliée ? | i = i * 3 : \(\lceil\log_3 n\rceil\), 13 tours pour \(10^6\) |
| \(\mathcal{O}\), \(\Omega\), \(\Theta\) | quelle croissance, à une constante près ? | \(3n^2+20n+50\in\mathcal{O}(n^2)\) avec \((5, 13)\), et \(\Theta(n^2)\) |
| cas | quelle entrée ? | séquentielle : 1, \((n+1)/2\), \(n\) ; dichotomie : au plus \(\lfloor\log_2 n\rfloor+1\) |
| doublement | la mesure confirme-t-elle ? | contient : rapports de 3,74 à 4,20, compatibles avec \(\Theta(n^2)\) |
| théorème maître | quelle classe pour \(aT(n/b)+f(n)\) ? | dichotomie \(\Theta(\log n)\), tri fusion \(\Theta(n\log n)\) |
Lecture. Le comptage donne \(f(n)\), la notation la classe, la mesure la confirmation : trois regards sur un même coût.
À retenir
s = s + i vaut 2, et le test d’une boucle qui s’arrête par lui s’évalue une fois de plus qu’il n’y a de tours.Séance 3
Dans un tableau, l’accès par rang est en \(\Theta(1)\), mais la recherche par valeur est en \(\Theta(n)\) ; la dichotomie la ramène à \(\Theta(\log n)\) au prix d’un tableau trié, où l’insertion coûte \(\Theta(n)\). Or la plupart des programmes cherchent par clé (un matricule, un numéro de téléphone), non par position. La Séance 3 calcule la case d’une clé à partir de la clé elle-même : fonctions de hachage, collisions, chaînage et sondage linéaire, facteur de charge, pour un coût moyen constant.
(low + high) / 2).java.util.Arrays.binarySearch, java.util.ArrayList, System.nanoTime (code source et documentation).Structures de données et algorithmes avancés — Séance 2 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture. Ligne 2 :
sreçoit 0 : une affectation. Total : 1.Lecture. Ligne 3 : initialisation de la boucle,
ireçoit 0 : une affectation. Total : 2.Lecture. Ligne 3 : test \(i<n\), \(0<3\) : vrai, un tour de plus. Une comparaison. Total : 3.
Lecture. Ligne 4 :
sreçoit \(0+0=0\) : une addition et une affectation, 2 opérations. Total : 5.Lecture. Ligne 3 :
i++:ipasse à 1, une addition et une affectation. Total : 7.Lecture. Ligne 3 : test \(i<n\), \(1<3\) : vrai, un tour de plus. Une comparaison. Total : 8.
Lecture. Ligne 4 :
sreçoit \(0+1=1\) : une addition et une affectation, 2 opérations. Total : 10.Lecture. Ligne 3 :
i++:ipasse à 2, une addition et une affectation. Total : 12.Lecture. Ligne 3 : test \(i<n\), \(2<3\) : vrai, un tour de plus. Une comparaison. Total : 13.
Lecture. Ligne 4 :
sreçoit \(1+2=3\) : une addition et une affectation, 2 opérations. Total : 15.Lecture. Ligne 3 :
i++:ipasse à 3, une addition et une affectation. Total : 17.Lecture. Ligne 3 : test \(i<n\), \(3<3\) : faux, la boucle s’arrête. Une comparaison. Total : 18.
Lecture. Ligne 6 : l’affichage n’appartient pas au fragment compté. Pour \(n=3\) : \(1+1+4+6+6=18\) opérations, soit \(f(n)=5n+3\).