cursus.

Cours 4 · ArbresLeçon 2 sur 2

Arbres de recherche et tas

5 h de lecture8 sections Version PDF

À la fin de cette leçon, vous saurez

ABR : insertion, recherche, suppression, dégénérescence et équilibrage en survol ; tas binaire, file de priorité et tri par tas.

Le chapitre 7 a donné une structure de rangement. Il lui manque ce qui fait l'intérêt d'un arbre : un invariant sur les valeurs. Deux invariants différents produisent deux structures aux usages opposés, et c'est tout ce chapitre.

L'arbre binaire de recherche ordonne de gauche à droite : il répond à « cette valeur est-elle là ? » en O(logn)O(\log n). Le tas ordonne de haut en bas : il répond à « quelle est la plus petite valeur ? » en O(1)O(1), et permet de la retirer en O(logn)O(\log n).

Aucun des deux ne fait le travail de l'autre, et c'est le point à retenir : un tas ne sait pas chercher, un ABR ne sait pas donner son minimum rapidement.

L'arbre binaire de recherche

L'invariant d'ABR porte sur tout nœud, sans exception : toutes les valeurs de son sous-arbre gauche lui sont inférieures, toutes celles de son sous-arbre droit lui sont supérieures.

                 8              ┌──┴──┐              3     10           ┌──┴─┐     └──┐           1    6        14              ┌─┴┐     ┌─┘              4  7    13

L'erreur classique est de croire qu'il suffit de comparer un nœud à ses enfants immédiats. L'invariant porte sur tout le sous-arbre : placer un 9 comme enfant gauche du 10 violerait la propriété, puisque 9 est supérieur à 8 et se trouverait dans son sous-arbre gauche.

La recherche exploite l'invariant pour éliminer la moitié de l'arbre à chaque comparaison : si la valeur cherchée est inférieure au nœud, elle ne peut être qu'à gauche. C'est la dichotomie du chapitre 4, appliquée à une structure chaînée, et son coût est O(h)O(h).

L'insertion suit le même chemin et accroche la nouvelle valeur là où la recherche a échoué, c'est-à-dire toujours comme feuille. C'est ce qui la rend simple : aucune restructuration.

La suppression est la seule opération délicate, avec ses trois cas :

  • Feuille : on la détache, c'est tout.
  • Un seul enfant : on remplace le nœud par cet enfant, qui remonte avec son sous-arbre.
  • Deux enfants : on ne peut détacher ni remplacer directement. On cherche le successeur — la plus petite valeur du sous-arbre droit, obtenue en descendant tout à gauche —, on copie sa valeur dans le nœud à supprimer, et l'on supprime le successeur, qui par construction a au plus un enfant. On se ramène donc toujours à l'un des deux premiers cas.

Le successeur convient parce qu'il est la plus petite valeur supérieure au nœud : le placer là préserve exactement l'invariant, à gauche comme à droite.

La dégénérescence

Toutes ces opérations coûtent O(h)O(h), et le chapitre 7 a montré que hh va de log2n\log_2 n à n1n-1. Il reste à savoir dans quel cas on tombe.

Or le pire cas n'a rien d'exotique. Insérer des données déjà triées produit une chaîne : chaque valeur étant supérieure à toutes les précédentes, elle part systématiquement à droite, et l'arbre devient une liste chaînée avec un pointeur inutilisé par nœud.

insertion de 1, 2, 3, 4, 5 dans cet ordre    1    └─ 2        └─ 3            └─ 4                └─ 5        hauteur 4 pour 5 nœuds : recherche en O(n)

C'est exactement le pire cas du tri rapide du chapitre 3, et pour la même raison de fond : une structure qui repose sur une division en deux moitiés s'effondre quand la division est systématiquement déséquilibrée. Et dans les deux cas, les données triées — le cas le plus fréquent en pratique — sont précisément celles qui déclenchent le désastre.

La parade porte un nom : l'équilibrage automatique. Les arbres AVL et rouge-noir maintiennent une hauteur en O(logn)O(\log n) garantie, en effectuant après chaque insertion ou suppression des rotations — des réarrangements locaux de trois nœuds qui préservent l'invariant tout en réduisant la hauteur.

        3                          2       ╱          rotation        ╱ ╲      2          ─────────►      1   3    1

Le principe suffit à ce niveau : le coût est une constante ajoutée à chaque modification, en échange d'une garantie qui transforme un O(n)O(n) possible en O(logn)O(\log n) certain. C'est ce que font les TreeMap de Java, les map de C++ et les index de bases de données — sous une variante à plus de deux enfants, l'arbre B, conçue pour minimiser les accès disque du chapitre 7 du cours de systèmes.

Quiz · vérifiez votre compréhension Sans réponse

On insère les entiers de 1 à 1000 dans l'ordre croissant dans un arbre binaire de recherche non équilibré, puis on cherche la valeur 1000. Combien de comparaisons ?

Le tas binaire

Le tas (heap) répond à un autre besoin : non pas « où est cette valeur ? » mais « quelle est la plus prioritaire ? ».

Son invariant est vertical. Dans un tas min, tout nœud est inférieur ou égal à ses deux enfants. Rien n'est imposé entre frères, ni entre branches — c'est un ordre beaucoup plus faible que celui de l'ABR, et c'est ce qui le rend bon marché à maintenir.

Deux conséquences immédiates. Le minimum est à la racine, donc accessible en O(1)O(1). Et comme aucun ordre n'est imposé horizontalement, on peut exiger en plus que l'arbre soit complet — ce qui autorise la représentation par tableau du chapitre 7, sans un seul pointeur.

        2                    indice   0   1   2   3   4   5      ┌─┴─┐                          ┌───┬───┬───┬───┬───┬───┐      4   3                          │ 2 │ 4 │ 3 │ 9 │ 7 │ 5 │    ┌─┴┐  └┐                         └───┴───┴───┴───┴───┴───┘    9  7   5                enfants de i : 2i+1 et 2i+2

Deux opérations suffisent, et elles sont symétriques.

Insérer : on place la valeur à la première case libre — la fin du tableau —, ce qui garde l'arbre complet mais casse peut-être l'invariant. On la fait alors remonter tant qu'elle est inférieure à son parent. Au plus h=log2nh = \log_2 n échanges.

Extraire le minimum : on prend la racine, on met le dernier élément à sa place — pour garder l'arbre complet —, puis on le fait descendre en l'échangeant à chaque étape avec le plus petit de ses enfants, tant qu'il est plus grand. Au plus log2n\log_2 n échanges là aussi.

C'est la file de priorité : une file où l'on ne sort pas le plus ancien mais le plus prioritaire. Le cours de systèmes en avait besoin sans la nommer — l'ordonnancement par priorités du chapitre 4 est exactement cela — et le chapitre 9 s'en servira pour l'algorithme de Dijkstra.

Le tri par tas

De là découle un troisième tri en nlognn \log n, et il complète le tableau du chapitre 3.

On construit un tas avec les nn valeurs, puis on extrait le minimum nn fois : les valeurs sortent triées. Chaque extraction coûte O(logn)O(\log n), d'où O(nlogn)O(n \log n).

Sa singularité est d'être sur place. On construit le tas dans le tableau lui-même, et chaque valeur extraite se range à la place libérée à la fin — d'où l'usage d'un tas max pour obtenir un ordre croissant.

FusionRapidePar tas
Pire casnlognn \log nn2n^2nlognn \log n
MémoireO(n)O(n)O(logn)O(\log n)O(1)O(1)
Stableouinonnon

Le tri par tas est donc le seul à être à la fois garanti en nlognn \log n et sans mémoire supplémentaire. Pourquoi n'est-il pas le tri par défaut ? Parce qu'il est plus lent en pratique que le tri rapide : ses accès sautent d'un indice ii à 2i+12i+1, donc à travers tout le tableau, ce qui ruine la localité spatiale du chapitre 7 d'architecture. Le tri rapide, lui, avance séquentiellement.

D'où sa place réelle, annoncée au chapitre 3 : c'est le filet de sécurité d'introsort. On trie rapide, et si la profondeur de récursion dérape — signe d'un mauvais pivot — on bascule sur le tri par tas, qui n'a pas de pire cas. On obtient la vitesse de l'un et la garantie de l'autre.

Quiz · vérifiez votre compréhension Sans réponse

Vous devez maintenir en permanence l'élément le plus prioritaire d'un ensemble où l'on insère et retire sans cesse. Un ABR équilibré ou un tas ?

À vous

L'exercice construit les deux structures, et l'essentiel est dans la mesure.

Pour l'ABR : insérer mille valeurs aléatoires, mesurer la hauteur, puis insérer les mêmes valeurs triées et mesurer à nouveau. L'écart entre une dizaine et mille est l'argument le plus convaincant du chapitre en faveur de l'équilibrage.

Pour le tas : écrire la remontée et la descente, vérifier l'invariant après chaque opération — une fonction de vérification est fournie, servez-vous-en, c'est ainsi qu'on débogue une structure — puis en tirer un tri par tas et compter ses comparaisons.

Exercice · JavaScript · à vous de jouer

Mesurez la dégénérescence d'un ABR, puis écrivez la remontée d'un tas et le tri par tas.

En attente
// ── Arbre binaire de recherche ────────────────────────────────────────────
const noeud = (v) => ({ v, g: null, d: null });

function inserer(A, v) {
  if (A === null) return noeud(v);
  if (v < A.v) A.g = inserer(A.g, v);
  else if (v > A.v) A.d = inserer(A.d, v);
  return A;   // les doublons sont ignorés
}

function chercher(A, v, comparaisons = { n: 0 }) {
  while (A !== null) {
    comparaisons.n++;
    if (v === A.v) return comparaisons.n;
    A = v < A.v ? A.g : A.d;
  }
  return -comparaisons.n;   // négatif : absent, après n comparaisons
}

function hauteur(A) { return A === null ? -1 : 1 + Math.max(hauteur(A.g), hauteur(A.d)); }

// L'invariant porte sur TOUT le sous-arbre, pas sur les enfants immédiats :
// on transmet donc un intervalle autorisé, qui se resserre à la descente.
function estUnAbr(A, min = -Infinity, max = Infinity) {
  if (A === null) return true;
  if (A.v <= min || A.v >= max) return false;
  return estUnAbr(A.g, min, A.v) && estUnAbr(A.d, A.v, max);
}

function infixe(A, sortie = []) {
  if (A === null) return sortie;
  infixe(A.g, sortie); sortie.push(A.v); infixe(A.d, sortie);
  return sortie;
}

// ── Tas min, par tableau ──────────────────────────────────────────────────
function creerTas() {
  const T = [];
  const parent = (i) => Math.floor((i - 1) / 2);

  function remonter(i) {
    // ← à écrire : tant que T[i] est inférieur à son parent, échanger
  }

  function descendre(i) {
    while (true) {
      const g = 2 * i + 1, d = 2 * i + 2;
      let plusPetit = i;
      if (g < T.length && T[g] < T[plusPetit]) plusPetit = g;
      if (d < T.length && T[d] < T[plusPetit]) plusPetit = d;
      if (plusPetit === i) return;
      [T[i], T[plusPetit]] = [T[plusPetit], T[i]];
      i = plusPetit;
    }
  }

  return {
    inserer(v) { T.push(v); remonter(T.length - 1); },
    extraireMin() {
      if (T.length === 0) return undefined;
      const min = T[0];
      const dernier = T.pop();
      if (T.length > 0) { T[0] = dernier; descendre(0); }
      return min;
    },
    taille: () => T.length,
    contenu: () => [...T],
    // L'outil de débogage d'une structure : vérifier l'invariant.
    invariantOk() {
      for (let i = 1; i < T.length; i++) if (T[parent(i)] > T[i]) return false;
      return true;
    },
  };
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Écrivez remonter().
// 2. Mesurez la hauteur d'un ABR de 1000 valeurs aléatoires, puis triées.
// 3. Écrivez triParTas(tableau) : tout insérer, tout extraire.

let A = null;
for (const v of [8, 3, 10, 1, 6, 14, 4, 7, 13]) A = inserer(A, v);
console.log("infixe :", infixe(A).join(" "), "| est un ABR :", estUnAbr(A));
console.log("hauteur :", hauteur(A));

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 8 · sur machine

L'arbre qui dégénère, et le tas qui n'en a pas le droit

Constater qu'un arbre de recherche peut perdre tout son intérêt sur une entrée ordinaire, puis implémenter le tas, qui garantit sa forme par construction.

3 h
Avant de commencer
  • Le TP 7 : arbres et parcours
  • Le TP 3 : mesures comparatives
  1. 1. L'arbre de recherche

    Implémentez insertion, recherche et parcours infixe. Insérez mille valeurs aléatoires et vérifiez que le parcours infixe donne une suite triée.

  2. 2. Mesurer la hauteur

    Mesurez la hauteur après insertion de 1000, 10 000 et 100 000 valeurs aléatoires. Comparez au logarithme en base deux de n.

  3. 3. Le faire dégénérer

    Insérez les mêmes valeurs, mais TRIÉES. Mesurez à nouveau la hauteur et le temps de recherche. Dessinez l'arbre obtenu pour cinq valeurs.

  4. 4. La suppression

    Écrivez la suppression, en traitant les trois cas : feuille, un enfant, deux enfants. Le troisième est le seul difficile.

  5. 5. Le tas

    Implémentez un tas binaire dans un TABLEAU, avec insertion et extraction du minimum. Vérifiez l'invariant après chaque opération.

  6. 6. Pourquoi un tableau

    Écrivez les formules donnant père, fils gauche et fils droit à partir d'un indice. Expliquez pourquoi elles ne fonctionnent que sur un arbre complet.

  7. 7. Le tri par tas

    Écrivez le tri par tas et comparez-le au tri rapide du TP 3, sur entrée aléatoire ET sur entrée triée.

  8. 8. La file de priorité

    Emballez votre tas dans une file de priorité avec une valeur et une priorité. Vous l'utiliserez telle quelle au TP 9.

C'est réussi quand
  • Votre parcours infixe donne une suite triée sur mille insertions aléatoires
  • Vous exhibez l'entrée qui rend votre arbre de hauteur n − 1
  • Votre tas maintient son invariant, vérifié automatiquement après chaque opération
  • Le tri par tas ne dégénère PAS sur l'entrée triée, contrairement au tri rapide

Ce que la suite en fait

Le bloc V généralise une dernière fois. Un arbre est un graphe sans cycle et avec une racine ; retirer ces deux contraintes donne l'objet du chapitre 9, où les parcours du chapitre 7 reviennent — mais avec une difficulté nouvelle, puisqu'un graphe peut ramener sur un sommet déjà visité et qu'il faut marquer.

Le tas y jouera un rôle précis. L'algorithme de Dijkstra a besoin, à chaque étape, du sommet non traité le plus proche : c'est une extraction de minimum, et c'est le passage d'une recherche linéaire à une file de priorité qui fait descendre son coût.

À retenir

Flashcards · 1 / 6Toucher pour retourner
Fin de la leçon

Vous avez parcouru les 8 sections.

Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.