Aller au contenu
Blockchain Association of Cameroon
BAC-L0-FOUND-1023 h

Structures de données et coût des opérations

Tableaux, tables de hachage, arbres : ce que chaque structure rend rapide et ce qu'elle rend cher. Le socle de tout raisonnement ultérieur sur le stockage d'une chaîne.

  • Comparer le coût des opérations de recherche, d'insertion et de parcours
  • Choisir une structure à partir du profil d'accès attendu
  • Mesurer un coût réel plutôt que de l'estimer

Un programme passe son temps à chercher, à ajouter et à parcourir des données, et la façon de les ranger décide du temps que cela prend : retrouver un client parmi un million peut demander un million de comparaisons ou une vingtaine. Cet atelier vous apprend à compter ce que coûte chaque opération sur un tableau, une table de hachage et un arbre, à choisir la structure qui convient au profil d'accès réel, et surtout à vérifier ses prévisions par une mesure plutôt que de s'y fier aveuglément. C'est le socle des raisonnements ultérieurs : une blockchain est d'abord un système qui doit retrouver, ajouter et vérifier énormément de données à bon coût.

Leçons 1/4 · 25 min

Compter les étapes : parcours et recherche dichotomique

Un cahier de clients

Awa tient une boutique à Bonamoussadi et note ses clients dans un cahier : un nom, un numéro de téléphone par ligne, dans l'ordre où ils sont venus. Pour retrouver un numéro, elle lit ligne après ligne. Avec 50 clients, c'est supportable. Avec 10 000, il lui faudra en moyenne 5 000 lignes et, au pire, les 10 000. Si elle avait tenu son cahier par ordre alphabétique, elle l'ouvrirait au milieu, verrait que le nom cherché est avant ou après, et ne garderait que la bonne moitié — comme on cherche un mot dans un dictionnaire. Même cahier, même contenu : la façon de le ranger change tout.

Le vocabulaire du coût

On mesure le coût d'une opération en nombre d'étapes élémentaires (une comparaison, une lecture) en fonction du nombre n d'éléments. Trois opérations reviennent partout :

  • la recherche (lookup) : retrouver un élément donné ;
  • l'insertion : ajouter un élément ;
  • le parcours (traversal) : visiter tous les éléments, par exemple pour tout afficher. Il coûte au minimum n étapes, quelle que soit la structure : on ne peut pas visiter n éléments en moins de n visites.

On note l'ordre de grandeur avec la notation « grand O » : O(n) veut dire « le coût croît proportionnellement à n », O(log n) « le coût croît comme le nombre de fois qu'on peut diviser n par 2 », O(1) « le coût ne dépend pas de n ». Cette notation ignore volontairement les constantes (« 3n » et « n » sont tous deux O(n)) ; nous y reviendrons dans la dernière leçon.

Le parcours séquentiel

Parcourir dans l'ordre, jusqu'à trouver ou jusqu'à épuiser la liste. Au pire (l'élément est absent ou en dernière position), il faut n comparaisons.

La recherche dichotomique

Elle exige un tableau trié. On compare la cible à l'élément du milieu ; selon le résultat on garde la moitié gauche ou droite, et on recommence.

def binary_search(items, target):
    low, high = 0, len(items) - 1
    while low <= high:
        mid = (low + high) // 2
        if items[mid] == target:
            return mid
        if items[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return None

Remarque : Python n'a pas de comparaison à trois issues, et ce code en fait jusqu'à deux par étape (== puis <). Dans ce cours on compte une comparaison à trois issues par étape (comme le comparateur d'un tri en C, qui rend un nombre négatif, nul ou positif) : c'est la convention du tableau ci-dessous.

Exemple suivi. Tableau trié [3, 8, 14, 21, 29, 35, 42], cible 35. Premier milieu : indice 3, valeur 21 ; 21 < 35, on garde la droite (indices 4 à 6). Nouveau milieu : indice 5, valeur 35 : trouvé en 2 étapes, au lieu de 6 pour un parcours.

Pourquoi si peu ? À chaque étape il reste au plus la moitié des candidats : après k étapes, au plus n ÷ 2^k. On a fini quand il n'en reste qu'un, donc après environ log₂ n étapes. Dans la convention de comptage de ce cours, on compte une comparaison à trois issues (égal, plus petit, plus grand) par étape, soit au pire ⌊log₂ n⌋ + 1 étapes :

nParcours (pire cas)Dichotomie (pire cas)
1001007
1 0001 00010
5 0005 00013
100 000100 00017
1 000 0001 000 00020

Multiplier n par 1 000 multiplie le parcours par 1 000, mais n'ajoute qu'une dizaine d'étapes à la dichotomie. Voilà ce qu'on appelle « passer à l'échelle ».

Ce que cela ne garantit pas

  • La dichotomie ne marche que sur des données triées. Trier coûte lui-même environ n × log₂ n comparaisons. Pour une seule recherche dans 50 000 éléments en désordre, trier d'abord coûte bien plus cher qu'un parcours : ce n'est rentable que si l'on cherche souvent.
  • Garder un tableau trié a un prix à chaque insertion (leçon 2).
  • Le « pire cas » n'est pas le cas moyen : un parcours trouve en moyenne vers n ÷ 2 si l'élément est présent. Et les constantes cachées comptent pour de petits n : sur dix éléments, le plus simple est souvent le plus rapide.

À retenir

Compter les étapes en fonction de n : parcours = n, dichotomie ≈ log₂ n (sur du trié). Un parcours complet coûte toujours au moins n. Le « pire cas » donne une garantie, pas une prédiction.

Leçons 2/4 · 30 min

Tableaux et tables de hachage

Retrouver un client sans lire le cahier

Dans la leçon précédente, même avec la dichotomie, Awa doit encore trier son cahier et le garder trié. Et si elle pouvait aller directement à la bonne page, comme dans un casier numéroté où chaque client a son tiroir ? C'est l'idée de la table de hachage. Pour la comprendre, partons de la structure plus simple dont elle dérive : le tableau.

Le tableau (liste Python)

Un tableau range ses éléments côte à côte en mémoire. L'élément n° i se trouve par un simple calcul d'adresse : accès par indice en O(1). En revanche :

Opération sur un tableau de n élémentsCoût
Lire l'élément n° iO(1)
Chercher une valeur (tableau non trié)O(n)
Chercher une valeur (tableau trié, dichotomie)O(log n)
Ajouter à la finO(1) en moyenne (parfois plus, quand le tableau doit être agrandi)
Insérer au début ou au milieuO(n) : il faut décaler tous les éléments suivants
Garder le tableau trié en insérantO(n) à chaque insertion

En Python, x in ma_liste est un parcours. ma_liste.insert(0, x) décale tout. Pour une file où l'on retire à gauche, la structure collections.deque est faite pour cela.

La table de hachage (dict, set)

Une table de hachage possède un certain nombre de cases (buckets). Une fonction de hachage transforme une clé (un nom, un numéro) en nombre ; on prend ce nombre modulo le nombre de cases pour obtenir la case où ranger la clé. Retrouver la clé, c'est refaire le même calcul et aller directement à la case — sans parcourir les autres.

Exemple avec une fonction JOUET. Table de 8 cases ; la « fonction » additionne les codes des lettres, puis on prend le reste de la division par 8. (Les vraies fonctions sont bien meilleures ; celle-ci sert à voir les mécanismes.)

CléSomme des codesReste mod 8 = case
Awa65 + 119 + 97 = 2811
Ngu78 + 103 + 117 = 2982
Mba77 + 98 + 97 = 2720
Aaw65 + 97 + 119 = 2811

« Aaw » tombe dans la même case qu'« Awa » : c'est une collision. Deux clés différentes, une seule case. La solution classique, le chaînage, consiste à garder dans chaque case une petite liste : on y ajoute les nouvelles clés à la fin, et pour chercher on compare, dans l'ordre, les clés de la case. Chercher « Aaw » : on calcule la case 1, on compare avec « Awa » (non), puis « Aaw » (oui) : 2 comparaisons, au lieu de lire tout le cahier.

Tant que la fonction répartit bien les clés et que la table reste peu remplie, chaque case contient très peu de clés : recherche, insertion et suppression coûtent O(1) en moyenne. Quand la table se remplit trop, elle est agrandie et toutes les clés sont replacées : une insertion exceptionnelle coûte O(n), mais répartie sur l'ensemble elle reste O(1) « amorti ».

En Python : set(ma_liste) construit une table ; "Mba" in mon_set hache « Mba » et regarde une case. Sur un million d'éléments, la différence avec in sur une liste est celle d'un million de comparaisons contre quelques-unes.

Ce que cela ne garantit pas

  • Le pire cas est O(n). Si toutes les clés tombent dans la même case, la table devient une simple liste. C'est un cas rare avec une bonne fonction de hachage… sauf si quelqu'un le provoque exprès : voir l'exercice de sécurité.
  • Pas d'ordre trié. Une table de hachage ne sait pas répondre vite à « les clients entre D et K » ni « le plus petit ». (Un dict Python se souvient de l'ordre d'insertion depuis Python 3.7, mais ce n'est pas l'ordre des clés.)
  • Elle coûte de la mémoire : des cases vides sont gardées en réserve.
  • Le chaînage est notre modèle d'étude, pas le détail de Python. Les dict et set de CPython utilisent l'adressage ouvert (on cherche une autre case libre en cas de collision) et non une liste par case ; l'histoire du coût (O(1) en moyenne, O(n) au pire) reste la même, mais le schéma « une petite liste dans chaque case » n'est pas ce que fait Python en interne.
  • Les clés doivent être immuables : si une clé change après coup, sa case n'est plus la bonne.
  • Cette fonction de hachage n'a rien de cryptographique : elle ne cherche qu'à répartir. Les fonctions qui protègent des données sont l'objet de l'atelier 104.

À retenir

Tableau : accès par indice en O(1), recherche en O(n) (ou O(log n) si trié, mais insertion en O(n)). Table de hachage : recherche et insertion en O(1) en moyenne, mais sans ordre, avec un pire cas O(n) quand les clés se bousculent.

Leçons 3/4 · 30 min

Arbres de recherche et ordre

Un annuaire qui accepte les nouveaux clients

Le tableau trié cherche vite mais insère mal ; la table de hachage cherche et insère vite mais ignore l'ordre. Existe-t-il une structure qui fasse les deux — chercher en O(log n), insérer en O(log n), et rester triée ? Oui : l'arbre binaire de recherche (ABR).

Le principe

Chaque nœud contient une clé et jusqu'à deux enfants : à gauche des clés plus petites, à droite des clés plus grandes. Pour chercher, on part de la racine et, à chaque nœud, on compare : on descend à gauche ou à droite, jamais les deux.

Insérons 50, 30, 70, 20, 40, 60, 80 dans cet ordre dans un arbre vide :

50
  gauche : 30
    gauche : 20
    droite : 40
  droite : 70
    gauche : 60
    droite : 80

Chercher 60 : on compare avec 50 (60 est plus grand, on va à droite), avec 70 (plus petit, à gauche), avec 60 (trouvé) : 3 comparaisons, pour 7 clés. Un arbre de hauteur h (nombre de niveaux) se parcourt de la racine à une feuille en au plus h comparaisons. Si l'arbre est bien équilibré, h ≈ log₂ n : pour un million de clés, environ 20 niveaux.

Le parcours en ordre

Le grand avantage sur la table de hachage : visiter l'arbre « gauche, nœud, droite » (parcours en ordre) donne les clés triées : 20, 30, 40, 50, 60, 70, 80. Le coût est O(n), le minimum possible. Et une requête d'intervalle (« toutes les clés entre 35 et 65 ») descend d'abord vers 35 en O(log n), puis marche dans l'ordre jusqu'à 65 : on ne visite que ce qu'il faut. Une table de hachage devrait examiner les n clés.

Le piège : l'arbre déséquilibré

Insérons maintenant 10, 20, 30, 40, 50 — des clés déjà triées — dans un arbre vide. Chaque nouvelle clé est plus grande que toutes les précédentes et va donc toujours à droite :

10
  droite : 20
    droite : 30
      droite : 40
        droite : 50

L'arbre est devenu une liste chaînée : trouver 50 demande 5 comparaisons, et avec n clés triées, n comparaisons. La garantie en O(log n) dépend de l'équilibre. Les arbres auto-équilibrés (arbres rouge-noir, arbres AVL…) se réorganisent à l'insertion pour garder la hauteur proche de log₂ n. Les bases de données utilisent des variantes à beaucoup d'enfants par nœud, les arbres B : SQLite range ses tables et ses index dans des arbres B. Vous les retrouverez à l'atelier 103.

Comparaison des quatre structures

StructureChercherInsérerParcours triéIntervalle
Tableau non triéO(n)O(1) à la fintrier d'abord : ≈ n log nO(n)
Tableau triéO(log n)O(n)O(n)O(log n + k)
Table de hachageO(1) en moyenne, O(n) au pireO(1) en moyennetrier d'abordO(n)
Arbre équilibréO(log n)O(log n)O(n)O(log n + k)

(k = nombre de résultats rendus.)

Ce que cela ne garantit pas

  • Un ABR simple ne s'équilibre pas tout seul : tout dépend de l'ordre d'arrivée des clés.
  • L'arbre est plus lent que la table de hachage pour une recherche exacte pure (log n contre ≈ 1), plus lourd à écrire, et ses nœuds éparpillés en mémoire sont moins favorables au processeur qu'un tableau contigu.
  • Les « O » ne prédisent pas qui sera le plus rapide sur 100 éléments. Seule une mesure le dit (leçon 4).

À retenir

L'arbre équilibré offre recherche et insertion en O(log n) et un parcours trié : c'est ce qu'on achète en renonçant au O(1) de la table. Sans équilibrage, l'ordre d'arrivée peut le dégrader en liste.

Leçons 4/4 · 30 min

Choisir selon le profil d'accès, puis mesurer

Quelle structure pour quel travail ?

Il n'y a pas de « meilleure » structure, seulement des compromis. Pour choisir, on commence par décrire le profil d'accès : ce que le programme fait vraiment, et dans quelles proportions.

  1. Quelle taille ? Combien d'éléments aujourd'hui, et dans un an ?
  2. Quelles opérations dominent ? Recherche exacte, insertion, suppression, parcours complet, requête d'intervalle, « le plus petit » ?
  3. L'ordre compte-t-il ? A-t-on besoin des données triées ?
  4. Quelle fréquence ? Une fois par jour ou mille fois par seconde ?

Quelques cas :

ProfilChoix raisonnablePourquoi
Savoir si un numéro appartient à une liste de 5 millions de numéros bloqués, des milliers de fois par seconde, sans jamais les listertable de hachage (set)recherche exacte en O(1) en moyenne ; l'ordre est inutile
Retrouver toutes les transactions entre deux dates, parmi un million, souventstructure triée (arbre équilibré, ou tableau trié + dichotomie si les ajouts sont rares)intervalle en O(log n + k)
File d'attente : ajouter à la fin, retirer au débutdequeles deux extrémités en O(1)
10 éléments, consultés rarementn'importe laquelle, le plus simpleles constantes dominent ; la complexité d'écriture coûte plus que le gain

Dans une blockchain, on retrouvera ces profils : retrouver un solde par adresse (recherche exacte, table de hachage), parcourir les blocs dans l'ordre (liste ordonnée), vérifier qu'une donnée appartient à un ensemble (ateliers suivants).

Mesurer au lieu d'estimer

La notation O dit comment le coût croît, pas combien de millisecondes il vaut. Deux programmes O(n) peuvent différer de dix fois. Un O(log n) peut perdre contre un O(n) sur de petits tableaux. La seule façon de savoir est de mesurer, avec méthode.

import timeit

for n in (1_000, 2_000, 4_000, 8_000):
    items = list(range(n))
    # cas défavorable : l'élément cherché est le dernier
    t = min(timeit.repeat(lambda: (n - 1) in items, number=200, repeat=5)) / 200
    print(n, round(t * 1e6, 1), "µs par appel")

Les bonnes pratiques de cette mesure :

  • Chronométrer seulement l'opération, pas la construction des données.
  • Répéter (ici 5 séries de 200 appels, le temps d'une série étant divisé par 200 pour obtenir celui d'un appel) et garder le minimum ou la médiane : le bruit (autres programmes, mise en cache) ne peut qu'ajouter du temps.
  • Faire varier n et regarder comment le temps évolue, plutôt que de se fier à un seul point.
  • Ne pas conclure à partir de très petites durées : au-dessous de quelques microsecondes, on mesure surtout le coût de l'appel.

Lire un doublement

Doublez n et regardez le rapport des temps :

Rapport du temps quand n doubleCroissance probable
≈ 1 (presque pas de changement)O(1)
un peu plus que 1 (+ une petite constante)O(log n)
≈ 2O(n)
un peu plus de 2O(n log n)
≈ 4O(n²)

Si votre prédiction (« la table de hachage ne doit pas varier ») n'est pas confirmée par la mesure, c'est la mesure qui a raison — il faut alors chercher pourquoi (collisions ? redimensionnement ? mémoire saturée ?). Si au contraire elle l'est, vous avez une preuve, pas une opinion.

Ce que cela ne garantit pas

Une mesure vaut pour ce matériel, ce jeu de données et cette version du langage. Un jeu de données aléatoire peut cacher un cas défavorable (clés triées dans un arbre non équilibré) ; un jeu de données trop petit peut cacher une croissance. Il faut donc dire sur quoi on a mesuré, et ne pas généraliser au-delà.

À retenir

Décrire le profil d'accès avant de choisir ; ne pas confondre croissance (O) et durée ; mesurer en répétant, en isolant l'opération et en faisant varier n ; lire un doublement pour deviner la croissance ; énoncer les limites de sa mesure.

Laboratoire guidé

Le navigateur n'exécute pas de Python : ce laboratoire vous fait compter exactement des comparaisons, des cases et des niveaux, et lire des mesures (fictives) comme on le ferait sur de vraies. Vous écrirez et chronométrerez réellement du code dans le laboratoire autonome. Chaque épreuve générée change à chaque tentative ; les questions fixes vérifient que vous savez raisonner, pas recopier.

Le navigateur ne remplace pas un vrai terminal : ce laboratoire entraîne la lecture et le raisonnement ; la pratique réelle est dans le laboratoire autonome.

Un compte certifié est nécessaire

Les activités notées enregistrent des résultats à votre nom et servent de base à un titre. Elles exigent donc un dossier certifiant complet et un consentement en vigueur. Remplir le dossier ne vérifie pas votre identité ; le Secrétariat peut ensuite la vérifier (badge « Vérifié »).

  1. 1.Compter les comparaisons au pire

    L'épreuve ci-dessous donne une taille n. Donnez le nombre maximal de comparaisons pour retrouver un élément (ou conclure à son absence) par un parcours séquentiel, puis par une recherche dichotomique sur un tableau trié, en comptant une comparaison par étape comme dans la leçon 1. Pour la dichotomie, divisez n par 2 jusqu'à tomber à 1, ou utilisez ⌊log₂ n⌋ + 1 ; vérifiez avec le tableau de la leçon pour les valeurs connues.

  2. 2.Suivre une collision dans une table à chaînage

    Reprenez la table JOUET de la leçon 2 : 8 cases, case = (somme des codes des lettres) modulo 8, nouvelles clés ajoutées à la fin de la chaîne de leur case. On y a inséré dans l'ordre : Awa, Ngu, Mba, Aaw. On insère maintenant « Kofi » (codes : K = 75, o = 111, f = 102, i = 105). Calculez sa case et dites combien de comparaisons de clés coûtera ensuite la recherche de « Kofi » (on compare les clés de la chaîne dans l'ordre, jusqu'à la trouver). Écrivez le calcul sur papier avant de répondre.

  3. 3.Construire un arbre et mesurer une recherche

    Insérez, dans cet ordre exact, les clés 40, 20, 60, 10, 30, 50, 70 dans un arbre binaire de recherche vide (plus petit à gauche, plus grand à droite). Dessinez-le sur papier. Puis insérez, dans un NOUVEL arbre vide, les clés 10, 20, 30, 40, 50, 60, 70 dans cet ordre. Comparez ensuite le coût de la recherche de la clé 70 dans les deux arbres, et répondez à la question posée sur le second.

  4. 4.Lire une mesure

    Un apprenti a chronométré la recherche d'une valeur absente dans une structure inconnue et obtenu (mesures FICTIVES, médiane de 5 séries) : n = 50 000 → 1,8 ms ; n = 100 000 → 3,6 ms ; n = 200 000 → 7,3 ms ; n = 400 000 → 14,5 ms. Appliquez la méthode « lire un doublement » de la leçon 4 et répondez à la question posée : quelle croissance ces mesures indiquent-elles ?

  5. 5.Choisir selon le profil

    Décrivez à l'écrit, en deux lignes, le profil d'accès du service décrit dans la question ci-dessous (taille, opérations dominantes, ordre nécessaire ou non, fréquence), puis choisissez la structure. Justifiez-la par la ligne du tableau de comparaison de la leçon 3 qui la favorise, et par la colonne qui la pénalise.

Exercice de sécurité — Collision volontaire : un attaquant qui choisit les clés

Le service qui range les paramètres dans une table

Un petit service web range les paramètres de chaque requête reçue dans une table de hachage à chaînage (celle de la leçon 2). Pour ajouter un paramètre, il commence par parcourir toute la chaîne de la case visée, pour savoir si la clé existe déjà : une insertion coûte donc autant de comparaisons qu'il y a déjà de clés dans la chaîne.

Pour simplifier, on suppose que le service utilise notre fonction JOUET de la leçon 2 (somme des codes des lettres, modulo 8), et que l'attaquant la connaît. Il connaît donc aussi cette propriété : tout mot formé des mêmes lettres, dans un autre ordre, tombe dans la même case. Avec les six lettres a, b, c, d, e, f, il existe 720 ordres différents, donc 720 clés différentes qui visent toutes la même case.

Journal fictif du service, une requête normale puis une requête hostile :

requête 1   paramètres : ville, quartier, page             3 clés, 0 comparaison en trop
requête 2   paramètres : abcdef, abcdfe, abcedf, abcefd, ... (200 anagrammes)
            temps de traitement : environ 60 fois le temps habituel (chiffre illustratif)

Chaque anagramme est une clé distincte, et toutes visent la même case. La i-ième clé insérée doit être comparée aux i - 1 clés déjà présentes dans cette chaîne. Avec 200 clés, l'attaquant obtient un travail de l'ordre de n au carré, alors qu'une table bien répartie ferait l'ordre de n. En envoyant des milliers de requêtes de ce genre, il occupe le processeur du serveur au point de le saturer : c'est une attaque par complexité algorithmique (ou « hash flooding »), sans une seule faille de mémoire ni un mot de passe volé.

À faire

Répondez à la question de contrôle : combien de comparaisons de clés l'insertion des 200 anagrammes provoque-t-elle en tout ? (Aidez-vous de la somme des entiers de 0 à 199.)

Défenses (à connaître, non notées)

  • Utiliser une fonction de hachage à clé secrète et aléatoire par processus : l'attaquant ne peut plus prévoir les collisions. Python randomise ainsi le hachage de ses chaînes de caractères à chaque lancement (pas celui des entiers, qui se hachent eux-mêmes).
  • Limiter ce qu'une requête peut apporter : nombre maximal de paramètres, taille maximale du corps.
  • Utiliser une structure dont le pire cas est borné (arbre équilibré par case, ou table avec cases en arbre).
  • Limiter le débit par client, et surveiller les temps de traitement anormaux.

Ne testez jamais ce type d'attaque contre un service qui ne vous appartient pas : refaites l'expérience dans votre propre programme, comme dans le laboratoire autonome.

Questionnaire de l'atelier

Huit questions tirées d'une réserve plus large, seuil de réussite 70 %. Il s'ouvre quand tous les points de contrôle du laboratoire sont réussis. Vous pouvez le repasser : le tirage change.

Un compte certifié est nécessaire

Les activités notées enregistrent des résultats à votre nom et servent de base à un titre. Elles exigent donc un dossier certifiant complet et un consentement en vigueur. Remplir le dossier ne vérifie pas votre identité ; le Secrétariat peut ensuite la vérifier (badge « Vérifié »).

Laboratoire autonome (hors du site)

Non noté en ligne : à réaliser sur votre machine et à publier sur votre dépôt Git.

Laboratoire autonome : mesurer sur un jeu de données cent fois plus grand

À faire sur votre machine, avec Python 3 (aucune bibliothèque externe).

Partie 1 : la mesure de base. Écrivez un script qui, pour n valant 1 000, 10 000 et 100 000, construit les entiers de 0 à n - 1 sous quatre formes : une liste, une liste triée (même contenu) interrogée avec le module bisect, un set, et un dict. Pour chaque structure, mesurez avec timeit.repeat (au moins 5 séries) le coût de la recherche d'un élément absent (le pire cas pour la liste). Gardez le minimum de chaque série. Affichez un tableau n × structure.

Partie 2 : cent fois plus grand. Refaites la mesure avec n = 10 000 000, une structure à la fois (supprimez-la avec del avant de construire la suivante) ; si votre machine manque de mémoire, prenez n = 1 000 000 pour toutes les structures et dites-le. Calculez, pour chaque structure, le rapport entre le temps à n et le temps à n ÷ 100. Comparez à ce que prédit la notation O (≈ 100 pour la liste, ≈ 1 pour le set, un peu plus de 1 pour bisect) et expliquez les écarts : cache du processeur, mémoire, allocation, bruit. Si un écart vous surprend, dites quelle hypothèse vous testeriez ensuite.

Partie 3 : insertion. Mesurez 10 000 insertions au début d'une liste (insert(0, x)) puis dans une collections.deque (appendleft). Répétez avec 100 000. Expliquez le rapport observé par le décalage des éléments.

Partie 4 : collision, dans votre propre programme. Implémentez la table à chaînage JOUET de la leçon 2 (8 cases, somme des codes modulo 8). Insérez 200 anagrammes de abcdef (utilisez itertools.permutations), comptez les comparaisons, retrouvez le total de l'exercice de sécurité, puis comparez avec 200 mots variés. N'attaquez jamais un programme qui n'est pas le vôtre.

Partie 5 : l'arbre. Implémentez un arbre binaire de recherche (insertion, recherche, calcul de la hauteur ; la version itérative évite la limite de récursion de Python). Chargez n = 100 000 clés tirées au hasard, puis les mêmes clés triées. Dans les deux cas, relevez la hauteur et mesurez le temps de recherche d'une clé absente, puis comparez à un dict et à un set sur le même jeu de données. Expliquez l'écart entre les deux ordres d'insertion avec les mots de la leçon 3.

À rendre. Un rapport d'une à deux pages : votre tableau de mesures (parties 1 à 5), la machine utilisée (processeur, mémoire, version de Python), vos rapports de croissance, vos explications des écarts, et une recommandation justifiée de structure pour deux profils au choix parmi ceux de la leçon 4. Une mesure sans la description de la machine et de la méthode n'est pas recevable.

À retenir

  • On mesure un coût en nombre d'étapes en fonction de n : un parcours coûte n, une recherche dichotomique sur du trié environ log₂ n (20 pour un million).
  • Tableau : accès par indice en O(1) mais insertion au milieu en O(n). Table de hachage : recherche et insertion en O(1) en moyenne, sans ordre, pire cas O(n). Arbre équilibré : O(log n) partout et parcours trié.
  • On choisit une structure à partir du profil d'accès (taille, opérations dominantes, ordre, fréquence), pas par habitude.
  • Un arbre non équilibré ou une table dont les clés se bousculent retombe à O(n) : l'ordre d'arrivée ou un attaquant peut le provoquer.
  • La notation O dit comment le coût croît, pas combien il vaut : on mesure en répétant, en isolant l'opération et en doublant n.

Idées fausses courantes

  • Idée reçue : Une table de hachage est toujours en O(1), donc toujours le meilleur choix.

    En réalité : O(1) vaut en moyenne et si les clés sont bien réparties. Le pire cas est O(n), la table ne donne aucun ordre trié et elle consomme de la mémoire en plus.

  • Idée reçue : Un algorithme O(log n) est toujours plus rapide qu'un algorithme O(n).

    En réalité : La notation O ignore les constantes et décrit la croissance pour n grand. Sur de petites données, le plus simple peut gagner. Seule une mesure tranche.

  • Idée reçue : Pour chercher plus vite, il suffit de trier la liste d'abord.

    En réalité : Trier coûte environ n × log₂ n comparaisons. Pour une seule recherche, un parcours est moins cher ; trier ne se justifie que si l'on cherche souvent et insère rarement.

  • Idée reçue : Un arbre binaire de recherche a toujours une hauteur d'environ log₂ n.

    En réalité : Seulement s'il est équilibré. Insérer des clés déjà triées dans un arbre simple produit une chaîne de hauteur n, où la recherche coûte n.

Glossaire

Coût (complexité)
Nombre d'étapes élémentaires qu'une opération demande, exprimé en fonction du nombre n d'éléments.
Notation O (grand O)
Écriture de l'ordre de grandeur du coût : O(1), O(log n), O(n), O(n log n), O(n²). Elle ignore les constantes.
Recherche dichotomique
Recherche dans un tableau trié qui élimine la moitié des candidats à chaque comparaison : environ log₂ n étapes.
Table de hachage
Structure qui place chaque clé dans une case calculée par une fonction de hachage ; recherche et insertion en O(1) en moyenne.
Collision
Situation où deux clés différentes donnent la même case. Le chaînage la résout en gardant une liste par case.
Arbre binaire de recherche
Arbre où chaque nœud a des clés plus petites à gauche et plus grandes à droite ; sa hauteur décide du coût d'une recherche.
Arbre équilibré
Arbre qui se réorganise pour garder une hauteur proche de log₂ n, garantissant des recherches en O(log n).
Profil d'accès
Description de ce que fait réellement un programme avec ses données : taille, opérations dominantes, besoin d'ordre, fréquence.