Séance 2 — Analyse algorithmique

Compter, borner, mesurer~: le coût d’un algorithme

Dr. El Hadji Bassirou TOURÉ

Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar

Objectifs de la séance

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.

  • Établir la fonction de coût \(f(n)\) d’un fragment Java, selon une convention de comptage annoncée.
  • Compter les tours d’une boucle à pas constant, imbriquée ou multiplicative.
  • Énoncer \(\mathcal{O}\), \(\Omega\) et \(\Theta\), et démontrer une borne en exhibant \(c\) et \(n_0\).
  • Classer les fonctions usuelles par croissance : \(\log n\), \(n\), \(n\log n\), \(n^2\), \(2^n\).
  • Distinguer meilleur cas, pire cas et cas moyen : recherche séquentielle et dichotomique.
  • Mesurer des durées par l’expérience du doublement et lire le rapport \(T(2n)/T(n)\).
  • Poser la récurrence d’un algorithme récursif et la résoudre par le théorème maître.

Un même programme, trois durées

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.

Juger un algorithme sans chronomètre

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.

La convention 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.

Compter les opérations d’une boucle, pas à pas

Sortie \(\rightarrow\) 

3

Lecture. Ligne 2 : s reçoit 0 : une affectation. Total : 1.

Lecture. Ligne 3 : initialisation de la boucle, i reç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 : s reçoit \(0+0=0\) : une addition et une affectation, 2 opérations. Total : 5.

Lecture. Ligne 3 : i++ : i passe à 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 : s reçoit \(0+1=1\) : une addition et une affectation, 2 opérations. Total : 10.

Lecture. Ligne 3 : i++ : i passe à 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 : s reçoit \(1+2=3\) : une addition et une affectation, 2 opérations. Total : 15.

Lecture. Ligne 3 : i++ : i passe à 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\).

Un compteur dans le code

public static long somme(int n, Compteur op) {
    long s = 0;
    op.ajouter(1);      // s = 0
    op.ajouter(1);      // i = 0
    for (int i = 0; i < n; i++) {
        op.ajouter(1);  // i < n, vraie
        s = s + i;
        op.ajouter(2);  // s = s + i
        op.ajouter(2);  // i++
    }
    op.ajouter(1);      // i < n, fausse
    return s;
}
for (int n = 0; n <= 10; n++) {
    Compteur op = new Compteur();
    Fragments.somme(n, op);
    System.out.print(op.total() + " ");
}
System.out.println();

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.

La boucle triangulaire, tour par tour

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.

De la somme des tours à \(f(n)\)

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\).

for (int n : new int[] {5, 10, 1000}) {
    Compteur op = new Compteur();
    long tours = Fragments.triangle(n, op);
    System.out.println("n = " + n + " : " + tours + " tours, f(n) = " + op.total());
}

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.

Une boucle qui multiplie, pas à pas

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\).

Additionner ou multiplier la variable de boucle

static int toursTripler(int n) {
    int tours = 0;
    for (int i = 1; i < n; i = i * 3) {
        tours++;
    }
    return tours;
}
for (int n = 100; n <= 1_000_000; n *= 10) {
    int a = toursTripler(n);
    System.out.println(n + " : " + a + " tours, contre "
        + (n - 1) + " pour i++");
}

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.

Exercice : la boucle carrée

Le fragment

int c = 0;
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
c = c + 1;

  1. Les exécutions de chaque instruction.
  2. Leurs opérations (convention du cours).
  3. \(f(n)\) et sa classe \(\Theta\).
  4. \(f(4)\) ; comparer au triangle.
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

  1. \(n+1\) tests internes par tour externe.
  2. 2 pour i++, j++, c = c + 1.
  3. \(\Theta(n^2)\) : le terme \(5n^2\) domine.
  4. 103 contre 73 (triangle) : même classe, \(f\) différente.

Le coût qu’on ne voit pas dans le code

public static void remplir(List<Integer> liste, int n) {
    for (int i = 0; i < n; i++) {
        liste.add(0, i);  // insertion en tete
    }
}
public static double mediane(Runnable tache) {
    double[] d = new double[5];
    for (int r = 0; r < d.length; r++) {
        long debut = System.nanoTime();
        tache.run();
        d[r] = (System.nanoTime() - debut) / 1e6;  // millisecondes
    }
    Arrays.sort(d);
    return d[2];                                   // valeur du milieu
}

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.

Deux listes chronométrées

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.

L’hypothèse du doublement

public static boolean contient(int[] t) {
    for (int i = 0; i < t.length; i++) {
        for (int j = i + 1; j < t.length; j++) {
            if (t[i] == t[j]) return true;
        }
    }
    return false;
}
public static double mesurer(int n) {
    // n valeurs distinctes : pire cas
    int[] t = new int[n];
    for (int i = 0; i < n; i++) t[i] = 3 * i;
    return Chrono.mediane(() -> vu = contient(t));
}

\(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.

L’expérience du doublement, pas à pas

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.

Deux séries de mesures

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, pourquoi, quand ça échoue : le doublement

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.

La notation \(\mathcal{O}\) : une borne supérieure

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.

Le seuil calculé, la preuve rédigée

static long f(long n) {
    return 3 * n * n + 20 * n + 50;
}

/** plus petit n0, cherche jusqu'a 10^6 */
static long seuil(int c) {
    long n0 = 1;
    for (long n = 1; n <= 1_000_000; n++) {
        if (f(n) > c * n * n) n0 = n + 1;
    }
    return n0;
}
for (int c : new int[] {73, 5, 4, 3}) {
    long n0 = seuil(c);
    String r = n0 > 1e6 ? "aucun n0" : "n0 = " + n0;
    System.out.println("c = " + c + " : " + r);
}

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.

\(\Omega\) et \(\Theta\), et quatre propriétés

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

  1. 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))\).

  2. 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, pourquoi, quand ça échoue : la notation \(\mathcal{O}\)

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.

La croissance des fonctions

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.

Les classes usuelles

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.

Du comptage à la classe

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)\).

Exercice : vrai ou faux

Cinq affirmations

Pour chacune, dire si elle est vraie ou fausse, et donner la raison en une ligne.

  1. \(5n^2+3n\in\mathcal{O}(n^3)\)
  2. \(5n^2+3n\in\Theta(n^3)\)
  3. \(n^2\in\Omega(n\log n)\)
  4. \(2^{n+1}\in\mathcal{O}(2^n)\)
  5. \(\mathcal{O}\) signifie « pire cas ».
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

  1. Vrai : \(5n^2+3n\leq8n^3\) pour \(n\geq1\).
  2. Faux : \((5n^2+3n)/n^3\to0\), aucune minoration \(c\,n^3\).
  3. Vrai : \(n^2\geq n\log_2 n\) dès \(n\geq1\).
  4. Vrai : une constante multiplicative disparaît.
  5. Faux : on parle du \(\Theta\) du meilleur cas comme du \(\mathcal{O}\) du pire.

Exercice : prouver une borne

\(f(n)=7n+40\)

  1. Montrer que \(f(n)\in\mathcal{O}(n)\) avec \(c=47\).
  2. Le plus petit \(n_0\) pour \(c=8\).
  3. Le plus petit \(n_0\) pour \(c=11\).
  4. Existe-t-il un \(n_0\) pour \(c=7\) ?
\(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

  1. Pour \(n\geq1\), \(40\leq40n\), d’où \(7n+40\leq47n\) : \((47, 1)\).
  2. \(7n+40\leq8n\iff n\geq40\) : \(n_0=40\).
  3. \(7n+40\leq11n\iff n\geq10\) : \(n_0=10\).
  4. Non : \(7n+40>7n\) pour tout \(n\). Tout \(c>7\) convient, avec \(n_0=\lceil40/(c-7)\rceil\).

Trois cas pour une même taille

public static int sequentielle(int[] t, int x) {
    for (int i = 0; i < t.length; i++) {
        examinees++;
        if (t[i] == x) return i;
    }
    return -1;
}

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}\).

La recherche séquentielle, pas à pas

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.

Le pire cas comme référence

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.

Exercice : identifier le pire cas

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.

  1. Comparaisons sans doublon ; le pire cas.
  2. Le meilleur cas, un cas intermédiaire.
  3. Décalages de l’insertion : pire et meilleur cas.
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

  1. Aucune sortie : \(n(n-1)/2=15\), le pire cas.
  2. En tête : 1 ; aux deux bouts : 5, toute la ligne \(i=0\).
  3. Décroissant : \(0+1+2+3+4=10\) ; croissant : 0.

La dichotomie et sa récurrence

public static int dicho(int[] t, int x, int lo, int hi) {
    if (lo > hi) return -1;
    int mid = lo + (hi - lo) / 2;
    examinees++;
    if (t[mid] == x) return mid;
    if (t[mid] < x) return dicho(t, x, mid + 1, hi);
    else return dicho(t, x, lo, mid - 1);
}

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.

Les trois cas mesurés

/** cases examinees par recherche, en moyenne */
static double cout(int[] t, int[] xs, boolean dicho) {
    Recherche.examinees = 0;
    for (int x : xs) {
        if (dicho) Recherche.dicho(t, x, 0, t.length - 1);
        else Recherche.sequentielle(t, x);
    }
    return (double) Recherche.examinees / xs.length;
}
int[] t = new int[1000];
for (int i = 0; i < 1000; i++) {
    t[i] = 2 * i;
}
// meilleur, moyen, pire
int[][] s = {{0}, t, {1999}};
int[][] d = {{998}, t, {1999}};
for (int k = 0; k < 3; k++) {
    double a = cout(t, s[k], false);
    double b = cout(t, d[k], true);
    System.out.println(a + " " + b);
}

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\).

La recherche dichotomique, pas à pas

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.

Le milieu, sans débordement

int lo = 1_500_000_000;
int hi = 2_000_000_000;
System.out.println("(lo + hi) / 2      = " + (lo + hi) / 2);
System.out.println("lo + (hi - lo) / 2 = " + (lo + (hi - lo) / 2));

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\).

Séquentielle contre dichotomique

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.

Le théorème maître

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).

Le coût par niveau

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\).

Trois récurrences calculées

/** T(n) = 2 T(n/2) + n^k, T(1) = 1, pour n puissance de 2 */
static long T(long n, int k) {
    if (n == 1) return 1;
    long f = 1;  // f = n^k
    for (int e = 0; e < k; e++) f = f * n;
    return 2 * T(n / 2, k) + f;
}
for (int k = 0; k <= 2; k++) {
    long a = T(1024, k);
    long b = T(2048, k);
    System.out.printf(Locale.ROOT, "f(n) = n^%d : %d puis %d, rapport %.2f%n",
        k, a, b, (double) b / a);
}

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, pourquoi, quand ça échoue : le théorème maître

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é.

Exercice : appliquer le théorème maître

Quatre récurrences

Pour chacune : \(a\), \(b\), \(\alpha=\log_b a\), le cas, la classe de \(T(n)\).

  1. \(2T(n/2)+\Theta(1)\)
  2. \(T(n/2)+\Theta(1)\)
  3. \(4T(n/2)+\Theta(n)\)
  4. \(2T(n/2)+\Theta(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

  1. \(\alpha=1\), \(f\in\mathcal{O}(n^{1-\varepsilon})\) : cas 1, \(\Theta(n)\).
  2. \(\alpha=\log_2 1=0\), \(f\in\Theta(n^0)\) : cas 2, \(\Theta(\log n)\), la dichotomie.
  3. \(\alpha=\log_2 4=2\), \(f\in\mathcal{O}(n^{2-\varepsilon})\) : cas 1, \(\Theta(n^2)\).
  4. \(\alpha=1\), \(f\in\Theta(n)\) : cas 2, \(\Theta(n\log n)\), le tri fusion.

Exercice : retour à la file de la Séance 1

\(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.

  1. Le coût total de \(n\) dequeue par supprimer(0), et sa classe.
  2. Le coût sur le tableau circulaire.
  3. Le rapport pour \(n=2\,000\).
  4. Pour \(n=20\,000\).
\(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

  1. Le \(k\)-ième supprimer(0) décale les \(n-k\) éléments restants : \(\sum_{k=1}^{n}(n-k)=n(n-1)/2\), \(\Theta(n^2)\).
  2. Un indice par dequeue, aucun décalage : \(\Theta(n)\).
  3. \(1\,999\,000/2\,000=999{,}5\).
  4. \(9\,999{,}5\) : le rapport vaut \((n-1)/2\), il grandit avec \(n\). L’intuition de la Séance 1 est démontrée.

Les outils côte à côte

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

À retenir

  • Analyser, c’est d’abord compter, selon une convention annoncée : 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.
  • \(f(n)\in\mathcal{O}(g(n))\) : il existe \(c>0\) et \(n_0\) tels que \(f(n)\leq c\,g(n)\) pour tout \(n\geq n_0\). Prouver, c’est exhiber un couple \((c, n_0)\).
  • \(\mathcal{O}\) majore, \(\Omega\) minore, \(\Theta\) encadre ; quand le coût est connu, on écrit \(\Theta\).
  • Boucles successives : on additionne ; imbriquées : on multiplie ; variable multipliée : \(\Theta(\log n)\) tours.
  • Le pire cas donne une garantie sans hypothèse sur les données ; « pire cas » et « \(\mathcal{O}\) » sont deux notions distinctes.
  • Doublement : si \(T(n)\approx a\,n^b\), \(T(2n)/T(n)\approx2^b\) ; on lit une tendance sur plusieurs mesures, après chauffe, au-dessus de la milliseconde.
  • Un algorithme récursif se traduit en \(T(n)=aT(n/b)+f(n)\), que le théorème maître résout en comparant \(f(n)\) à \(n^{\log_b a}\).

Prochaine séance : tables de hachage

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.

Références

  • Sedgewick, Wayne — Algorithms, 4e éd., Addison-Wesley, 2011 (§ 1.4 : analyse des algorithmes, modèle de coût, expérience du doublement, ordres de croissance ; § 1.1 : recherche dichotomique).
  • Cormen, Leiserson, Rivest, Stein — Introduction to Algorithms, 4e éd., MIT Press, 2022 (ch. 2 : analyse au pire et en moyenne ; ch. 3 : notations asymptotiques ; ch. 4 : diviser pour régner, § 4.5 : théorème maître).
  • Skiena — The Algorithm Design Manual, 3e éd., Springer, 2020 (ch. 2 : analyse des algorithmes).
  • Goodrich, Tamassia, Goldwasser — Data Structures and Algorithms in Java, 6e éd., Wiley, 2014 (ch. 4 : modèle des opérations primitives).
  • Bloch — « Extra, Extra — Read All About It: Nearly All Binary Searches and Mergesorts are Broken », Google Research Blog, 2006 (débordement de (low + high) / 2).
  • OpenJDK 17 — java.util.Arrays.binarySearch, java.util.ArrayList, System.nanoTime (code source et documentation).