Chercher en \(\mathcal{O}(\log n)\) et toujours extraire le minimum
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
Des structures qui gardent l’ordre des clés : l’arbre binaire de recherche (ABR), l’arbre AVL qui garantit une hauteur en \(\mathcal{O}(\log n)\), et le tas binaire qui donne toujours le minimum. Chaque opération est montrée sur le code Java du cours, pas à pas.
D’où l’on vient, où l’on va
Amont. Les types abstraits et les interfaces Java (Séance 1) ; la dichotomie en \(\mathcal{O}(\log n)\) et l’analyse au pire et en moyenne (Séance 2) ; les types Set et Map et le hachage, qui donne \(\mathcal{O}(1)\) en moyenne mais ne garde aucun ordre (Séance 3).
Aval. La Séance 5 passe aux graphes : l’algorithme de Dijkstra extrait à chaque tour le sommet de plus petite distance, avec un tas comme celui de cette séance.
Le carnet de contacts de la Séance 3, rangé une fois par hachage, une fois dans un arbre (TreeSet, de la bibliothèque standard) :
List<String> noms = List.of("Moussa Ndiaye", "Awa Diop", "Fatou Sall",
"Cheikh Ba", "Coumba Faye", "Ibrahima Sy");
Set<String> h = new HashSet<>(noms);
TreeSet<String> t = new TreeSet<>(noms);
System.out.println(h); // ordre des cases
System.out.println(t); // ordre des cles
System.out.println(t.higher("Fatou Sall")); // la premiere apres
System.out.println(t.subSet("A", "D")); // les cles de [A, D[Résultat \(\rightarrow\)
[Awa Diop, Cheikh Ba, Coumba Faye, Ibrahima Sy, Moussa Ndiaye, Fatou Sall]
[Awa Diop, Cheikh Ba, Coumba Faye, Fatou Sall, Ibrahima Sy, Moussa Ndiaye]
Ibrahima Sy
[Awa Diop, Cheikh Ba, Coumba Faye]
Lecture. Le HashSet suit l’ordre de ses cases : Fatou Sall sort en dernier. Le TreeSet sort les noms triés, donne le premier nom après Fatou Sall et les noms de l’intervalle \([\texttt{A}, \texttt{D}[\) sans tout parcourir.
Trois propriétés à la fois
get, put et remove en \(\mathcal{O}(\log n)\) ;| structure | (1) opérations | (2) ordre | (3) intervalle |
|---|---|---|---|
| table de hachage | \(\mathcal{O}(1)\) en moyenne | non | non (tout parcourir) |
| tableau trié | get \(\mathcal{O}(\log n)\), put \(\Theta(n)\) |
oui | oui |
| arbre binaire de recherche | \(\mathcal{O}(h)\), \(h\) la hauteur | oui | oui |
Lecture. L’arbre réunit les trois, à condition que sa hauteur \(h\) reste en \(\mathcal{O}(\log n)\) : c’est l’objet des AVL. Le tas, en fin de séance, ne garde qu’une partie de l’ordre : le minimum.
Propriété d’ABR
Un arbre binaire est un arbre binaire de recherche {(binary search tree)} si, en tout nœud \(x\), les clés du sous-arbre gauche sont \(<x.\mathrm{cle}\) et celles du sous-arbre droit \(>x.\mathrm{cle}\). Les clés sont distinctes : un put sur une clé présente remplace sa valeur.
Vocabulaire
Profondeur : arêtes depuis la racine. Hauteur : la plus grande profondeur (vide : \(-1\), feuille seule : \(0\)).
Lecture. Un nœud = une clé, une valeur, deux liens. K extends Comparable<K> : les clés se comparent par compareTo.
Sortie \(\rightarrow\)
7 2
10
put en Javapublic void put(K cle, V valeur) {
racine = put(racine, cle, valeur);
}
private Noeud<K, V> put(Noeud<K, V> x, K cle, V valeur) {
if (x == null) { // place vide : nouvelle feuille
n++;
return new Noeud<>(cle, valeur);
}
comparaisons++;
int c = cle.compareTo(x.cle);
if (c < 0) x.gauche = put(x.gauche, cle, valeur);
else if (c > 0) x.droite = put(x.droite, cle, valeur);
else x.valeur = valeur; // deja presente : remplacer
return x;
}Lecture. put est récursif (Sedgewick et Wayne) : chaque appel rend la racine du sous-arbre, que le parent raccroche à la place de l’ancienne. Le seul nœud créé est une feuille, au bout du chemin ; on y arrive après une comparaison par nœud traversé.
Sortie \(\rightarrow\)
v60 3
null 3
Lecture. Ligne 1 : un ABR vide.
Lecture. Ligne 2 : les sept clés, dans l’ordre d’insertion de l’exemple précédent.
Lecture. Ligne 3 : le même arbre ; chaque valeur est "v" suivi de la clé.
Lecture. Ligne 4 : le compteur d’observation est remis à zéro.
Lecture. Ligne 5 : get descend comme put : 50, 70, puis 60, trouvée. Seul le chemin est examiné.
Lecture. Ligne 6 : la valeur et 3 comparaisons, pour 7 clés.
Lecture. Ligne 7 : remise à zéro.
Lecture. Ligne 8 : 65 suit 50, 70, 60, puis tombe sur le fils droit de 60, vide : la clé est absente.
Lecture. Ligne 9 : null 3 : une recherche coûte au plus \(h+1\) comparaisons, présente ou absente.
get en JavaLecture. get descend en boucle, sans récursion : une comparaison compareTo par nœud examiné, jusqu’à la clé ou jusqu’à null. contains suit le même chemin et rend un booléen.
private Noeud<K, V> remove(Noeud<K, V> x, K cle) {
if (x == null) return null; // cle absente
int c = cle.compareTo(x.cle);
if (c < 0) {
x.gauche = remove(x.gauche, cle);
} else if (c > 0) {
x.droite = remove(x.droite, cle);
} else if (x.gauche == null || x.droite == null) { // cas 1 et 2
n--;
return x.gauche != null ? x.gauche : x.droite; // feuille : null
} else { // cas 3
Noeud<K, V> s = min(x.droite); // successeur infixe
x.cle = s.cle;
x.valeur = s.valeur;
x.droite = remove(x.droite, s.cle); // s : 0 ou 1 enfant
}
return x;
}Lecture. Cas 3 : la clé du successeur \(s\) remplace celle du nœud, puis \(s\), sans enfant gauche, est retiré.
Sortie \(\rightarrow\)
50(30(-,40),70(60,80))
50(40,70(60,80))
60(40,70(-,80))
Lecture. Ligne 1 : un ABR vide.
Lecture. Ligne 2 : les sept clés de l’exemple.
Lecture. Ligne 3 : l’arbre des sept clés.
Lecture. Ligne 4 : 20 est une feuille : remove rend null à son parent, qui la détache.
Lecture. Ligne 5 : l’état écrit : - marque le sous-arbre gauche vide de 30.
Lecture. Ligne 6 : 30 n’a qu’un enfant, 40 : remove rend 40 au parent, qui l’accroche à la place de 30.
Lecture. Ligne 7 : 40, désormais feuille, s’écrit seule.
Lecture. Ligne 8 : 50 a deux enfants : son successeur infixe est le minimum du sous-arbre droit, trouvé en descendant à gauche depuis 70 : 60.
Lecture. Ligne 8 (suite) : la clé et la valeur de 60 sont recopiées dans le nœud de 50, puis 60 est retirée du sous-arbre droit, où elle n’a pas d’enfant gauche (cas 1 ou 2).
Lecture. Ligne 9 : 60(40,70(-,80)) : l’ordre infixe 40, 60, 70, 80 est conservé.
Parcours en profondeur
Pour un nœud \(x\) :
Infixe d’un ABR = clés triées
Par récurrence sur la hauteur : le sous-arbre gauche sort ses clés triées, toutes \(<x\) ; puis \(x\) ; puis les clés du sous-arbre droit, triées, toutes \(>x\).
Lecture. Chaque nœud est visité une fois : un parcours coûte \(\Theta(n)\). prefixe et postfixe ne diffèrent que par la place de l.add(x.cle).
Sortie \(\rightarrow\)
[20, 30, 40, 50, 60, 70, 80]
[50, 30, 20, 40, 70, 60, 80]
[20, 40, 30, 60, 80, 70, 50]
[40, 50, 60]
Lecture. Ligne 1 : un ABR vide.
Lecture. Ligne 2 : les sept clés de l’exemple.
Lecture. Ligne 3 : l’arbre des sept clés.
Lecture. Ligne 4 : infixe : un nœud est visité après tout son sous-arbre gauche et avant tout son sous-arbre droit : les clés sortent triées.
Lecture. Ligne 5 : préfixe : le nœud d’abord. Réinsérer les clés dans cet ordre redonne le même arbre.
Lecture. Ligne 6 : postfixe : le nœud en dernier, après ses deux sous-arbres ; la racine sort à la fin.
Lecture. Ligne 7 : cles(35, 65) ne descend à gauche que si \(35<\) clé, à droite que si clé \(<65\) : 5 nœuds visités, 3 retenus (fond orangé), dans l’ordre.
Insertions dans un ABR vide
On insère, dans cet ordre : 41, 22, 67, 15, 30, 58, 80, 26.
get(26), puis de get(50).| clé | 41 | 22 | 67 | 15 | 30 | 58 | 80 | 26 |
|---|---|---|---|---|---|---|---|---|
| profondeur | 0 | 1 | 1 | 2 | 2 | 2 | 2 | 3 |
Correction
41(22(15,30(26,-)),67(58,80)), hauteur 3.30(26,-) : 26. 41(26(15,30),67(58,80)).Coût de get, put, remove
\[ T(n)=\Theta(h)\qquad\text{avec}\qquad \lfloor\log_2 n\rfloor\;\leq\; h\;\leq\; n-1 \]
Chaque opération suit un seul chemin depuis la racine : au plus \(h+1\) comparaisons. Un arbre binaire de hauteur \(h\) a au plus \(2^{h+1}-1\) nœuds, d’où \(h\geq\lfloor\log_2 n\rfloor\) ; une chaîne atteint \(h=n-1\).
L’ordre d’insertion décide
Les mêmes clés donnent un arbre de hauteur 2 ou 6 selon l’ordre d’arrivée. Or les données arrivent souvent triées : par date, par matricule croissant, par ordre alphabétique.
Lecture. Sur des clés en ordre aléatoire, la recherche réussie coûte en moyenne \(2(1+1/n)H_n-3\sim 2\ln n\approx1{,}39\log_2 n\) comparaisons (\(H_n\) : nombre harmonique).
Sortie \(\rightarrow\)
6 21
7
Lecture. Ligne 1 : un ABR vide ; les clés vont arriver dans l’ordre croissant.
Lecture. Ligne 2 : 20 devient la racine.
Lecture. Ligne 3 : \(30>20\) : à droite.
Lecture. Ligne 4 : 40 est plus grande que toutes les clés présentes : elle descend toujours à droite, 2 comparaisons.
Lecture. Ligne 5 : 50 traverse les 3 nœuds déjà présents : 3 comparaisons, profondeur 3.
Lecture. Ligne 6 : 60 traverse les 4 nœuds déjà présents : 4 comparaisons, profondeur 4.
Lecture. Ligne 7 : 70 traverse les 5 nœuds déjà présents : 5 comparaisons, profondeur 5.
Lecture. Ligne 8 : 80 traverse les 6 nœuds déjà présents : 6 comparaisons, profondeur 6.
Lecture. Ligne 9 : hauteur 6 pour 7 nœuds : l’arbre est une liste chaînée, et les insertions ont coûté \(0+1+\dots+6=21\) comparaisons.
Lecture. Ligne 10 : remise à zéro du compteur.
Lecture. Ligne 11 : get(80) traverse les 7 nœuds, comme une recherche dans une liste.
Lecture. Ligne 12 : 7 comparaisons, contre 3 dans l’arbre équilibré des mêmes clés.
for (int n : new int[] {100, 1000, 10000}) { // java -Xss16m : pile agrandie
List<Integer> cles = new ArrayList<>();
for (int i = 1; i <= n; i++) cles.add(i);
SimpleBST<Integer, Integer> trie = new SimpleBST<>();
for (int k : cles) trie.put(k, k); // ordre croissant
Collections.shuffle(cles, new Random(42));
SimpleBST<Integer, Integer> alea = new SimpleBST<>();
for (int k : cles) alea.put(k, k); // ordre aleatoire
alea.comparaisons = 0;
for (int k : cles) alea.get(k); // n recherches reussies
System.out.println(n + " : " + trie.hauteur() + " " + alea.hauteur()
+ " " + alea.comparaisons / (double) n);
}Résultat \(\rightarrow\)
100 : 99 13 7.76
1000 : 999 26 13.401
10000 : 9999 29 16.1003
Lecture. Triées : \(h=n-1\) ; aléatoires : 13, 26, 29 ; \(16{,}10\) comparaisons par get (formule \(16{,}58\)).
{.t-scriptsize}SimpleBST, \(n=16\) à \(16\,384\) clés mélangées (Collections.shuffle, graine 221), 100 arbres par taille.
Lecture. Le coût mesuré suit la formule à 1 % près (\(17{,}74\) contre \(17{,}56\) pour \(16\,384\)). La hauteur moyenne, \(32{,}7\), croît aussi en \(\log n\) et tend lentement vers \(2{,}99\log_2 n\) (Reed, 2003).
Quinze clés, deux ordres
A : 40, 20, 60, 10, 30, 50, 70, 5, 15, 25, 35, 45, 55, 65, 75.
B : 5, 10, 15, …, 75 (croissant).
get(75) et de get(5).get coûte \(\Theta(\dots)\) ; au pire \(\Theta(\dots)\) ; équilibré \(\Theta(\dots)\).| ordre | hauteur | get(75) |
get(5) |
|---|---|---|---|
| A | 3 | 4 | 4 |
| B | 14 | 15 | 1 |
Correction
Facteur d’équilibre et invariant
\(h(x)\) : hauteur du sous-arbre de racine \(x\) (vide : \(-1\), feuille : \(0\)). Le facteur d’équilibre est
\[ \mathrm{bf}(x)=h(\mathrm{gauche}(x))-h(\mathrm{droite}(x)) \]
Un ABR est un AVL (Adelson-Velsky et Landis, 1962) si \(|\mathrm{bf}(x)|\leq1\) en tout nœud \(x\).
Garantie de hauteur
\[ h\;\leq\;1{,}44\,\log_2(n+2)-0{,}328\;=\;\mathcal{O}(\log n) \]
Le pire AVL de hauteur \(h\) a \(N_h=N_{h-1}+N_{h-2}+1\) nœuds (un sous-arbre de hauteur \(h-1\), l’autre \(h-2\)) : \(N_h\) croît comme Fibonacci, en \(\varphi^h\), \(\varphi\approx1{,}618\), d’où \(h\leq\log_\varphi n\approx1{,}44\log_2 n\).
Lecture. get est celui de l’ABR ; seuls put et remove doivent rétablir l’invariant, par des rotations.
private static class Noeud<K, V> {
final K cle;
V valeur;
int hauteur; // feuille : 0
Noeud<K, V> gauche;
Noeud<K, V> droite;
}
private int h(Noeud<K, V> x) {
return x == null ? -1 : x.hauteur; // arbre vide : -1
}
private int equilibre(Noeud<K, V> x) {
return h(x.gauche) - h(x.droite); // facteur d'equilibre
}Lecture. Constructeur non montré. La hauteur mémorisée donne equilibre en \(\mathcal{O}(1)\).
{.t-scriptsize}\(T_1\), \(T_2\), \(T_3\) : sous-arbres quelconques, peut-être vides.
Une réorganisation locale
Une rotation change trois liens : \(b\) monte, \(a\) descend, et le sous-arbre du milieu \(T_2\) passe de \(b\) à \(a\). Elle conserve la propriété d’ABR (l’ordre infixe ne change pas), change les hauteurs (\(T_1\) remonte d’un niveau, \(T_3\) descend d’un niveau) et coûte \(\mathcal{O}(1)\). La rotation gauche est symétrique.
Lecture. a doit être mis à jour avant b, puisque a est désormais l’enfant de b. Chaque méthode rend la nouvelle racine du sous-arbre : l’appelant la raccroche à la place de a.
Après une insertion, on remonte le chemin ; au premier nœud \(a\) avec \(|\mathrm{bf}(a)|=2\), le cas se lit sur le chemin qui descend de \(a\) vers la clé insérée :
| cas | signes | chemin depuis \(a\) | rééquilibrage |
|---|---|---|---|
| LL | \(\mathrm{bf}(a)=+2\), \(\mathrm{bf}(\mathrm{gauche})\geq0\) | gauche, gauche | rotation droite sur \(a\) |
| RR | \(\mathrm{bf}(a)=-2\), \(\mathrm{bf}(\mathrm{droite})\leq0\) | droite, droite | rotation gauche sur \(a\) |
| LR | \(\mathrm{bf}(a)=+2\), \(\mathrm{bf}(\mathrm{gauche})=-1\) | gauche, droite | gauche sur l’enfant, puis droite sur \(a\) |
| RL | \(\mathrm{bf}(a)=-2\), \(\mathrm{bf}(\mathrm{droite})=+1\) | droite, gauche | droite sur l’enfant, puis gauche sur \(a\) |
Le nom du cas
Le cas nomme le chemin vers la clé insérée, pas le sens de la rotation : LL se répare par une rotation droite. Dans LR, une seule rotation droite sur \(a\) redonne un arbre déséquilibré, de l’autre côté.
private Noeud<K, V> reequilibrer(Noeud<K, V> a) {
majHauteur(a);
int bf = equilibre(a);
if (bf > 1) { // trop haut a gauche
if (equilibre(a.gauche) < 0) { // cas LR
a.gauche = rotationGauche(a.gauche);
}
return rotationDroite(a); // cas LL
}
if (bf < -1) { // trop haut a droite
if (equilibre(a.droite) > 0) { // cas RL
a.droite = rotationDroite(a.droite);
}
return rotationGauche(a); // cas RR
}
return a; // |bf| <= 1
}Lecture. put est celui de l’ABR, à une ligne près : au retour de chaque appel récursif, return reequilibrer(x); au lieu de return x;. Le rééquilibrage a donc lieu en remontant, du bas vers la racine.
Sortie \(\rightarrow\)
20(10,35(30,40))
3 2
Lecture. Ligne 1 : un AVL vide. Chaque nœud porte sa hauteur ; le facteur d’équilibre \(\mathrm{bf}=h(g)-h(d)\) est affiché à droite des nœuds du chemin.
Lecture. Ligne 2 : une feuille : hauteur 0, \(\mathrm{bf}=-1-(-1)=0\).
Lecture. Ligne 3 : 20 à droite de 10 : \(\mathrm{bf}(10)=-1\), toléré.
Lecture. Ligne 4 : 30 à droite de 20 : en remontant, \(\mathrm{bf}(20)=-1\), puis \(\mathrm{bf}(10)=-1-1=-2\). Le chemin descend deux fois à droite : cas RR.
Lecture. Ligne 4 (suite) : rotation gauche sur 10 : 20 monte, 10 devient son fils gauche. L’ordre infixe 10, 20, 30 est inchangé, la hauteur repasse à 1.
Lecture. Ligne 5 : 40 sous 30 : \(\mathrm{bf}(30)=-1\), \(\mathrm{bf}(20)=0-1=-1\) : aucun nœud hors de \([-1,1]\).
Lecture. Ligne 6 : 35 à gauche de 40 : \(\mathrm{bf}(40)=+1\), \(\mathrm{bf}(30)=-1-1=-2\). Droite puis gauche : cas RL.
Lecture. Ligne 6 (suite) : première rotation, droite sur 40 : 35 monte au-dessus de 40. Le cas RL devient un cas RR.
Lecture. Ligne 6 (suite) : seconde rotation, gauche sur 30 : 35 prend la place de 30, entre 30 et 40.
Lecture. Ligne 7 : 20(10,35(30,40)) : cinq clés en hauteur 2.
Lecture. Ligne 8 : 3 rotations (une pour RR, deux pour RL) et une hauteur de 2 : un ABR, avec les mêmes insertions, aurait la hauteur 4.
Sortie \(\rightarrow\)
20(5(3,8(7,10)),35(30,40))
3 3
Lecture. Ligne 1 : un AVL vide.
Lecture. Ligne 2 : les cinq clés de l’exemple précédent.
Lecture. Ligne 3 : l’arbre 20(10,35(30,40)).
Lecture. Ligne 4 : le compteur de rotations repart de 0.
Lecture. Ligne 5 : 5 sous 10 : \(\mathrm{bf}(10)=+1\), \(\mathrm{bf}(20)=1-1=0\).
Lecture. Ligne 6 : 3 sous 5 : \(\mathrm{bf}(5)=+1\), \(\mathrm{bf}(10)=1-(-1)=+2\). Gauche puis gauche : cas LL.
Lecture. Ligne 6 (suite) : rotation droite sur 10 : 5 monte, 10 devient son fils droit ; \(\mathrm{bf}(20)\) redevient \(+1\).
Lecture. Ligne 7 : 7 sous 10 : \(\mathrm{bf}(10)=+1\), \(\mathrm{bf}(5)=-1\), \(\mathrm{bf}(20)=2-1=+1\) : équilibré.
Lecture. Ligne 8 : 8 à droite de 7 : \(\mathrm{bf}(7)=-1\), \(\mathrm{bf}(10)=1-(-1)=+2\). Gauche puis droite : cas LR.
Lecture. Ligne 8 (suite) : rotation gauche sur 7 : 8 monte au-dessus de 7 ; le cas LR devient un cas LL.
Lecture. Ligne 8 (suite) : rotation droite sur 10 : 8 prend la place de 10, entre 7 et 10.
Lecture. Ligne 9 : neuf clés, hauteur 3.
Lecture. Ligne 10 : 3 rotations depuis la remise à zéro : une pour LL, deux pour LR. Chaque insertion a demandé au plus un rééquilibrage.
int n = 10000; // java -Xss16m : pile agrandie
SimpleBST<Integer, Integer> abr = new SimpleBST<>();
AVLTree<Integer, Integer> avl = new AVLTree<>();
for (int k = 1; k <= n; k++) abr.put(k, k); // cles croissantes
for (int k = 1; k <= n; k++) avl.put(k, k);
abr.comparaisons = 0;
avl.comparaisons = 0;
for (int k = 1; k <= n; k++) abr.get(k); // n recherches reussies
for (int k = 1; k <= n; k++) avl.get(k);
System.out.println("ABR " + abr.hauteur() + " " + abr.comparaisons / (double) n);
System.out.println("AVL " + avl.hauteur() + " " + avl.comparaisons / (double) n
+ " " + avl.rotations);Résultat \(\rightarrow\)
ABR 9999 5000.5
AVL 13 12.3631 9986
Lecture. ABR : hauteur \(9\,999\), \(5\,000{,}5\) comparaisons par get. AVL : hauteur 13 \(=\lfloor\log_2 10\,000\rfloor\), le minimum, et \(12{,}36\), pour \(9\,986\) rotations.
Quoi
Un ABR dont chaque nœud garde sa hauteur ; après chaque insertion ou suppression, une ou deux rotations par nœud déséquilibré rétablissent \(|\mathrm{bf}|\leq1\).
Pourquoi c’est logarithmique
L’invariant borne la hauteur par \(1{,}44\log_2(n+2)\) ; une insertion demande au plus un rééquilibrage (une ou deux rotations), car il rend au sous-arbre sa hauteur d’avant.
Quand ça échoue
Une suppression peut demander une rotation à chaque niveau, \(\Theta(\log n)\). java.util.TreeMap utilise un arbre rouge-noir : hauteur jusqu’à \(2\log_2(n+1)\), mais au plus 2 rotations par insertion et 3 par suppression.
L’AVL 50(30(20,40),70), on insère 45
Départ : 50, de fils 30 (enfants 20 et 40) et 70.
| nœud | 40 | 30 | 50 |
|---|---|---|---|
| bf après insertion | \(-1\) | \(-1\) | \(+2\) |
| bf final | 0 | \(+1\) | 0 |
Correction
40(30(20,-),50(45,70)) : 40 monte à la racine ; hauteur 2.Le type abstrait file de priorité
insert(x) ; extractMin() : retirer et rendre le plus petit ; peekMin() ; size(). Aucune recherche d’une clé quelconque : Dijkstra (Séance 5) extrait le sommet le plus proche.
Résultat \(\rightarrow\)
1 [1, 2, 4, 5, 3]
1 2 3 4 5
Lecture. java.util.PriorityQueue est un tas binaire en tableau (champ queue, siftUp, siftDown) : l’affichage montre ce tableau, non trié ; poll rend les clés dans l’ordre.
Tas binaire min {(min-heap)}
Un arbre binaire complet (tous les niveaux pleins, sauf peut-être le dernier, rempli de gauche à droite) tel que, pour tout nœud \(x\) autre que la racine :
\[ \mathrm{cle}(\mathrm{parent}(x))\;\leq\;\mathrm{cle}(x) \]
Le minimum est à la racine : peekMin coûte \(\mathcal{O}(1)\). Un tas de \(n\) clés a la hauteur \(\lfloor\log_2 n\rfloor\).
Un tas n’est pas un ABR
ABR : gauche \(<x<\) droite, l’arbre est trié. Tas : parent \(\leq\) enfants, l’arbre est partiellement ordonné : entre deux frères, aucun ordre. On n’y cherche pas une clé quelconque, et son parcours infixe ne sort pas les clés triées : [2, 5, 3, 10, 7, 8, 6] donne 10, 5, 7, 2, 8, 3, 6.
{.t-scriptsize}Le tas [2, 5, 3, 10, 7, 8, 6] : case \(i\) = nœud \(i\), niveau par niveau, de gauche à droite.
Indices (tableau indexé à partir de 0)
\[ \mathrm{parent}(i)=\Big\lfloor\frac{i-1}2\Big\rfloor\qquad\mathrm{gauche}(i)=2i+1\qquad\mathrm{droite}(i)=2i+2 \]
Un arbre complet n’a pas de trou : les cases \(0\) à \(n-1\), sans aucun lien. Indexé à partir de 1 (CLRS) : \(2i\) et \(2i+1\).
insert et la percolation vers le hautpublic void insert(K cle) {
if (size == data.length) resize();
data[size] = cle; // en queue : premiere case libre
percolateUp(size);
size++;
}
private void percolateUp(int i) {
while (i > 0) {
int parent = (i - 1) / 2;
if (plusPetit(i, parent)) { // plus petit que son parent :
swap(i, parent); // il monte d'un niveau
i = parent;
} else {
return; // ordre du tas respecte
}
}
}Lecture. La clé prend la première case libre, puis remonte : au plus \(\lfloor\log_2 n\rfloor\) échanges.
Sortie \(\rightarrow\)
[2, 5, 3, 10, 7, 8, 6]
[1, 2, 3, 5, 7, 8, 6, 10] 3
Lecture. Ligne 1 : un tas vide : un tableau de 8 cases, size vaut 0.
Lecture. Ligne 2 : sept clés, déjà dans l’ordre d’un tas.
Lecture. Ligne 3 : chaque insert range sa clé en queue, sans échange ici : chaque parent est plus petit que ses enfants. La case \(i\) est le nœud \(i\), niveau par niveau.
Lecture. Ligne 4 : le tableau, dans l’ordre des indices : le minimum 2 est en case 0.
Lecture. Ligne 5 : le compteur d’échanges est remis à zéro.
Lecture. Ligne 6 : 1 est rangé en queue, case 7 (fils gauche de la case 3). \(1<10\) : l’ordre du tas est violé.
Lecture. Ligne 6 (suite) : percolateUp : 1 et 10 sont échangés ; 1 est en case 3, son parent est la case 1, qui contient 5.
Lecture. Ligne 6 (suite) : \(1<5\) : nouvel échange ; 1 monte en case 1, sous la racine.
Lecture. Ligne 6 (suite) : \(1<2\) : dernier échange ; 1 atteint la racine et la boucle s’arrête (\(i=0\)).
Lecture. Ligne 7 : 3 échanges : la clé est remontée de la profondeur 3 à la racine, \(\lfloor\log_2 8\rfloor=3\) niveaux au plus.
extractMin et la percolation vers le baspublic K extractMin() {
K min = data[0];
size--;
data[0] = data[size]; // le dernier monte a la racine
data[size] = null;
if (size > 0) percolateDown(0);
return min;
}
private void percolateDown(int i) {
while (2 * i + 1 < size) { // i a au moins un enfant
int petit = 2 * i + 1; // enfant gauche
if (petit + 1 < size && plusPetit(petit + 1, petit)) petit++;
if (!plusPetit(petit, i)) return; // ordre du tas respecte
swap(i, petit); // il descend d'un niveau
i = petit;
}
}Lecture. Le dernier comble la racine, puis descend vers le plus petit enfant (test du tas vide non montré).
Sortie \(\rightarrow\)
2 [3, 5, 6, 10, 7, 8]
3 [5, 7, 6, 10, 8]
Lecture. Ligne 1 : un tas vide.
Lecture. Ligne 2 : les sept clés de l’exemple précédent.
Lecture. Ligne 3 : le tas [2, 5, 3, 10, 7, 8, 6] ; le minimum est en case 0.
Lecture. Ligne 4 : extractMin garde 2, puis range le dernier élément, 6, à la racine : le tableau compte 6 cases, mais 6 est plus grand que ses enfants.
Lecture. Ligne 4 (suite) : percolateDown compare les deux enfants, 5 et 3, et échange 6 avec le plus petit, 3. Avec 5, l’ordre serait violé entre 5 et 3.
Lecture. Ligne 4 (suite) : en case 2, 6 n’a plus qu’un enfant (\(2\times2+2=6=\) size) : \(6<8\), la percolation s’arrête.
Lecture. Ligne 5 : 2 [3, 5, 6, 10, 7, 8] : le nouveau minimum, 3, est à la racine.
Lecture. Ligne 6 : second extractMin : 3 sort, le dernier élément, 8, monte à la racine.
Lecture. Ligne 6 (suite) : le plus petit enfant est 5 : échange avec la case 1.
Lecture. Ligne 6 (suite) : en case 1, les enfants sont 10 et 7 : échange avec 7. En case 4, \(2\times4+1=9\geq5\) : une feuille, fin.
Lecture. Ligne 7 : 3 [5, 7, 6, 10, 8] : deux extractions ont rendu 2 puis 3, dans l’ordre croissant.
Random r = new Random(221);
for (int n : new int[] {1000, 10000, 100000, 1000000}) {
BinaryMinHeap<Integer> tas = new BinaryMinHeap<>();
for (int i = 0; i < n; i++) tas.insert(r.nextInt());
double ins = tas.echanges / (double) n; // par insert
tas.echanges = 0;
for (int i = 0; i < n; i++) tas.extractMin();
double ext = tas.echanges / (double) n; // par extractMin
double lg = Math.log(n) / Math.log(2);
System.out.printf(Locale.ROOT, "%7d : %.3f %.3f %.2f%n", n, ins, ext, lg);
}Résultat \(\rightarrow\)
1000 : 1.280 7.366 9.97
10000 : 1.275 10.680 13.29
100000 : 1.275 14.009 16.61
1000000 : 1.280 17.307 19.93
Lecture. Pire cas : \(\lfloor\log_2 n\rfloor\) échanges. Clés aléatoires : insert, \(1{,}28\) échange en moyenne quel que soit \(n\) ; extractMin, \(17{,}31\) pour \(\log_2 n=19{,}93\).
Quoi
Un arbre complet rangé dans un tableau, parent \(\leq\) enfants ; insert percole vers le haut, extractMin vers le bas.
Pourquoi c’est logarithmique
L’arbre complet a la hauteur \(\lfloor\log_2 n\rfloor\), et chaque percolation suit un seul chemin : \(\mathcal{O}(1)\) pour peekMin, \(\mathcal{O}(\log n)\) pour insert et extractMin, sans aucun pointeur.
Quand ça échoue
Chercher ou retirer une clé quelconque : \(\Theta(n)\), il faut parcourir le tableau. Changer la priorité d’une clé déjà rangée demande de connaître sa case.
Le tas [4, 9, 6, 12, 10, 8, 15]
insert(5) : le tableau à chaque étape.extractMin() : valeur et tableaux.Correction
| case | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| départ | 4 | 9 | 6 | 12 | 10 | 8 | 15 | |
| en queue | 4 | 9 | 6 | 12 | 10 | 8 | 15 | 5 |
| échange 7, 3 | 4 | 9 | 6 | 5 | 10 | 8 | 15 | 12 |
| échange 3, 1 | 4 | 5 | 6 | 9 | 10 | 8 | 15 | 12 |
| 12 monte | 12 | 5 | 6 | 9 | 10 | 8 | 15 | |
| échange 0, 1 | 5 | 12 | 6 | 9 | 10 | 8 | 15 | |
| échange 1, 3 | 5 | 9 | 6 | 12 | 10 | 8 | 15 |
Integer[] a = {14, 9, 17, 11, 6, 15, 12, 8};
BinaryMinHeap<Integer> tas = new BinaryMinHeap<>();
for (Integer x : a) tas.insert(x); // n insertions
for (int i = 0; i < a.length; i++) a[i] = tas.extractMin();
System.out.println(Arrays.toString(a));
int n = 1000000;
Random r = new Random(221);
BinaryMinHeap<Integer> grand = new BinaryMinHeap<>();
for (int i = 0; i < n; i++) grand.insert(r.nextInt());
for (int i = 0; i < n; i++) grand.extractMin();
double nlgn = n * Math.log(n) / Math.log(2);
System.out.println(grand.comparaisons + " " + grand.comparaisons / nlgn);Résultat \(\rightarrow\)
[6, 8, 9, 11, 12, 14, 15, 17]
37195773 1.8661738974847382
Lecture. Tri par tas {(heapsort)} : \(n\) insertions puis \(n\) extractions, \(\mathcal{O}(n\log n)\) au pire ; mesuré : \(1{,}87\,n\log_2 n\) comparaisons pour \(n=10^6\). Sedgewick et Wayne (§ 2.4) : le même tri en place.
| structure | get |
put |
minimum | mesuré, \(n=10\,000\) : getaléatoire / croissant |
|---|---|---|---|---|
| table de hachage (S3) | \(\Theta(1)\) moyen | \(\Theta(1)\) amorti | \(\Theta(n)\) | — (voir ci-dessous) |
| ABR | \(\Theta(h)\), pire \(\Theta(n)\) | \(\Theta(h)\) | \(\Theta(h)\) | \(15{,}33\) / \(5\,000{,}5\) |
| AVL | \(\Theta(\log n)\) pire | \(\Theta(\log n)\) | \(\Theta(\log n)\) | \(12{,}61\) / \(12{,}36\) |
| tas binaire | — | \(\mathcal{O}(\log n)\) | \(\Theta(1)\), retrait \(\mathcal{O}(\log n)\) | — |
Lecture. Comparaisons par recherche réussie ; clés \(2, 4, \dots, 20\,000\) mélangées puis croissantes ; hauteurs : ABR 26 / \(9\,999\), AVL 15 / 13. \(15{,}33\) au lieu des \(16{,}10\) de la hauteur mesurée : autre tirage (graine 221 au lieu de 42). Table de hachage : \(1{,}37\) en S3 (\(n=7\,500\), \(\alpha=0{,}75\)). Sans ordre : HashMap ; avec ordre : TreeMap ; le minimum : PriorityQueue.
À retenir
Séance 5 — Graphes
Les structures de cette séance rangent des données isolées. Les graphes représentent des relations : Dakar est reliée à Thiès, Thiès à Touba. La Séance 5 construit :
À préparer : le TP de la Séance 4 (SimpleBST, AVLTree et BinaryMinHeap, tests JUnit et mesures).
java.util.TreeMap (arbre rouge-noir) et de java.util.PriorityQueue (tas en tableau), JDK 21.Structures de données et algorithmes avancés — Séance 4 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture. Ligne 1 : un ABR vide :
racinevautnull,nvaut 0.Lecture. Ligne 2 : l’arbre est vide : 50 devient la racine, sans comparaison.
Lecture. Ligne 3 : \(30<50\) :
putdescend à gauche, trouvenullet y accroche la nouvelle feuille.Lecture. Ligne 4 : \(70>50\) : à droite de la racine. Une comparaison par nœud traversé.
Lecture. Ligne 5 : \(20<50\), puis \(20<30\) : deux comparaisons, feuille de profondeur 2.
Lecture. Ligne 6 : \(40 < 50\) puis \(40 > 30\) :
Ziguinchorse range en feuille de profondeur 2.Lecture. Ligne 7 : \(60 > 50\) puis \(60 < 70\) :
Lougase range en feuille de profondeur 2.Lecture. Ligne 8 : \(80 > 50\) puis \(80 > 70\) :
Matamse range en feuille de profondeur 2.Lecture. Ligne 9 : 7 nœuds et une hauteur de 2 : chaque niveau est plein.
Lecture. Ligne 10 : 10 comparaisons au total : insérer une clé coûte la profondeur où elle atterrit.