Union-find, tri par insertion, tri fusion, tri rapide et borne inférieure
Département de Mathématiques et Informatique, Faculté des Sciences et Techniques, Université Cheikh Anta Diop de Dakar
Ce que la séance construit
Deux familles d’algorithmes qui terminent le cours : les ensembles disjoints (union-find), qui répondent en un temps presque constant à la question « \(p\) et \(q\) sont-ils reliés ? », et les tris par comparaison, dont la séance établit la borne inférieure. Chaque structure est montrée sur le code Java du cours, pas à pas, et mesurée.
id[] et parent[] et la forêt.D’où l’on vient, où l’on va
Amont. Les tableaux et l’analyse des coûts (Séances 1 et 2) ; les arbres et le tas binaire, avec le tri par tas (Séance 4) ; l’algorithme de Kruskal, dont le test de cycle parcourait la forêt à chaque arête (Séance 5).
Aval. Cette séance clôt le cours : sa dernière partie rassemble les structures des six séances, leurs opérations et leurs coûts.
Huit villages de la Petite Côte, raccordés au réseau électrique liaison après liaison.
Lecture. Après les sept raccordements, une seule composante. Les liaisons de droite servent à Kruskal (section 5).
UFEnsembles disjoints
Une partition de \(\{0,\dots,n-1\}\) en composantes : union(p, q) les réunit, find(p) identifie celle de \(p\), connected(p, q) teste \(\texttt{find(p)}=\texttt{find(q)}\).
Ce que la Séance 5 attendait
Kruskal rejette \(v\)–\(w\) si connected(v, w), et garde l’arête par union(v, w). Le test par parcours atteignait \(38\,476\,757\) sommets pour \(V=4\,000\).
Lecture. acces() : lectures et écritures de id[] ou parent[] (Sedgewick et Wayne, § 1.5).
Lecture. id[p] est l’identifiant de la composante de \(p\) : find coûte un accès, mais union lit les \(n\) cases et réécrit toute la composante de \(p\).
Sortie \(\rightarrow\)
true
[7, 7, 7, 7, 7, 7, 7, 7]
protected final int[] parent;
protected int count;
protected long acces; // acces a parent[]
public QuickUnionUF(int n) {
parent = new int[n];
for (int i = 0; i < n; i++) parent[i] = i;
count = n;
}
public int find(int p) {
acces++; // premier test
while (p != parent[p]) {
p = parent[p]; // monter
acces += 2; // lecture + test
}
return p; // la racine
}
Sortie \(\rightarrow\)
true
Lecture. Ligne 1 : huit arbres d’un seul nœud : chaque élément est sa propre racine.
Lecture. Lignes 3–4, 7–10, 13 : union(0, 1) : 0 et 1 sont des racines ; parent[0] \(=1\).
Lecture. Lignes 3–4, 7–10, 13 : union(1, 2) : 1 et 2 sont des racines ; parent[1] \(=2\).
Lecture. Lignes 3–4, 7–10, 13 : union(2, 3) : 2 et 3 sont des racines ; parent[2] \(=3\).
Lecture. Lignes 3–4, 7–10, 13 : union(4, 5) : 4 et 5 sont des racines ; parent[4] \(=5\).
Lecture. Lignes 3–4, 7–10, 13 : union(6, 7) : 6 et 7 sont des racines ; parent[6] \(=7\).
Lecture. Lignes 3–4, 7–10, 13 : union(5, 7) : 5 et 7 sont des racines ; parent[5] \(=7\).
Lecture. Lignes 3–4, 7–10, 13 : union(3, 7) : 3 et 7 sont des racines ; parent[3] \(=7\).
Lecture. Lignes 3–4, 14 : find(0) monte 0 → 1 → 2 → 3 → 7 : 4 liens, 9 accès ; find(6) : 6 → 7. La hauteur est 4.
for (int n : new int[] {1000, 2000, 4000, 8000}) {
QuickUnionUF qu = new QuickUnionUF(n);
WeightedQuickUnionUF wqu = new WeightedQuickUnionUF(n);
for (int i = 1; i < n; i++) {
qu.union(0, i); // la racine de 0 sous i
wqu.union(0, i);
}
System.out.printf(Locale.ROOT, "%4d : QuickUnion %8d acces, hauteur %4d ; "
+ "ponderee %5d acces, hauteur %d%n", n, qu.acces(), qu.hauteur(),
wqu.acces(), wqu.hauteur());
}Résultat \(\rightarrow\)
1000 : QuickUnion 999999 acces, hauteur 999 ; ponderee 2997 acces, hauteur 1
2000 : QuickUnion 3999999 acces, hauteur 1999 ; ponderee 5997 acces, hauteur 1
4000 : QuickUnion 15999999 acces, hauteur 3999 ; ponderee 11997 acces, hauteur 1
8000 : QuickUnion 63999999 acces, hauteur 7999 ; ponderee 23997 acces, hauteur 1
Lecture. union(0, i) place la racine de 0 sous \(i\) : la chaîne s’allonge d’un lien à chaque appel, et find(0) la remonte en entier. \(n^2-1\) accès et hauteur \(n-1\) pour QuickUnion, \(3(n-1)\) accès et hauteur 1 pour l’union pondérée.
protected final int[] taille; // taille[r], r racine
@Override
public void union(int p, int q) {
int i = find(p);
int j = find(q);
if (i == j) return;
if (taille[i] < taille[j]) { // i sous j
parent[i] = j;
taille[j] += taille[i];
} else { // j sous i
parent[j] = i;
taille[i] += taille[j];
}
acces++;
count--;
}Hauteur au plus \(\log_2 n\)
La profondeur d’un élément \(x\) n’augmente que lorsque la racine de son arbre passe sous une autre racine, d’un arbre au moins aussi grand : la taille de l’arbre de \(x\) au moins double. Elle ne dépasse pas \(n\) : la profondeur de \(x\) augmente au plus \(\log_2 n\) fois.
Coût. find et union en \(\mathcal{O}(\log n)\) (les accès à taille[] ne sont pas comptés). L’union par le rang (CLRS, § 19.3) range une borne de la hauteur au lieu de la taille ; elle donne la même borne.
Sortie \(\rightarrow\)
0 8
Lecture. Lignes 2–5, 9–10, 14 : mêmes raccordements, find de QuickUnion ; au départ, taille[i] \(=1\) : 1 passe sous 0, taille[0] \(=2\).
Lecture. Lignes 2–5, 9–10, 14 : union(1, 2) : racines 0 et 2 ; taille[0] \(=2\geq1\) : 2 passe sous 0, taille[0] \(=3\).
Lecture. Lignes 2–5, 9–10, 14 : union(2, 3) : racines 0 et 3 ; taille[0] \(=3\geq1\) : 3 passe sous 0, taille[0] \(=4\).
Lecture. Lignes 2–5, 9–10, 14 : union(4, 5) : racines 4 et 5 ; tailles égales (1) : 5 passe sous 4, taille[4] \(=2\).
Lecture. Lignes 2–5, 9–10, 14 : union(6, 7) : racines 6 et 7 ; tailles égales (1) : 7 passe sous 6, taille[6] \(=2\).
Lecture. Lignes 2–5, 9–10, 14 : union(5, 7) : racines 4 et 6 ; tailles égales (2) : 6 passe sous 4, taille[4] \(=4\).
Lecture. Lignes 2–5, 9–10, 14 : union(3, 7) : racines 0 et 4 ; tailles égales (4) : 4 passe sous 0, taille[0] \(=8\).
Lecture. Ligne 15 : find(7) : 7 → 6 → 4 → 0 ; hauteur 3 \(=\log_2 8\), contre 4 pour QuickUnion.
Six éléments, cinq unions
union(0, 1), union(1, 2), union(3, 4), union(2, 4), union(5, 4).
parent[] après chaque union, en QuickUnion.parent[] et la taille de la racine, en union pondérée.find(4) dans chacune.| union | QuickUnion | pondérée | taille |
|---|---|---|---|
| (0, 1) | 1 1 2 3 4 5 |
0 0 2 3 4 5 |
[t[0] = 2]{.fragment .etape-only fragment-index=1} |
| (1, 2) | 1 2 2 3 4 5 |
0 0 0 3 4 5 |
[t[0] = 3]{.fragment .etape-only fragment-index=1} |
| (3, 4) | 1 2 2 4 4 5 |
0 0 0 3 3 5 |
[t[3] = 2]{.fragment .etape-only fragment-index=1} |
| (2, 4) | 1 2 4 4 4 5 |
0 0 0 0 3 5 |
[t[0] = 5]{.fragment .etape-only fragment-index=1} |
| (5, 4) | 1 2 4 4 4 4 |
0 0 0 0 3 0 |
[t[0] = 6]{.fragment .etape-only fragment-index=1} |
Correction
Compression de chemin
Après avoir trouvé la racine, find parcourt une seconde fois le chemin et rattache chaque élément directement à la racine. Le chemin suivant est plus court pour tous ces éléments et pour leurs descendants.
PathCompressionUF hérite de l’union pondérée et ne remplace que find : \(4d+1\) accès pour un élément de profondeur \(d\).
Sortie \(\rightarrow\)
7
[7, 7, 7, 7, 5, 7, 7, 7]
Lecture. Lignes 2, 14 : find(0) sur la forêt finale de QuickUnion ; racine part de 0.
Lecture. Lignes 3–4 : racine \(=1\) : la première boucle monte d’un niveau.
Lecture. Lignes 3–4 : racine \(=2\) : la première boucle monte d’un niveau.
Lecture. Lignes 3–4 : racine \(=3\) : la première boucle monte d’un niveau.
Lecture. Lignes 3–4 : racine \(=7\) : c’est la racine, la boucle s’arrête.
Lecture. Lignes 6–9 : \(p=0\) : parent[0] passe de 1 à 7, puis \(p=1\).
Lecture. Lignes 6–9 : \(p=1\) : parent[1] passe de 2 à 7, puis \(p=2\).
Lecture. Lignes 6–9 : \(p=2\) : parent[2] passe de 3 à 7, puis \(p=3\).
Lecture. Lignes 6–9 : \(p=3\) : parent[3] vaut déjà 7, la case est réécrite ; \(p=7\).
Lecture. Lignes 6, 11, 14 : \(p=7\), fin de la boucle ; find(0) rend 7, le chemin est aplati.
Lecture. Ligne 15 : hauteur 2 au lieu de 4 : un nouveau find(0) ne suit qu’un lien.
Random r = new Random(2026);
System.out.println(" n paires QuickFind QuickUnion ponderee compression");
for (int n = 1000; n <= 64000; n *= 2) {
int[][] paires = Paires.jusquaConnexe(n, r);
UF[] ufs = {new QuickFindUF(n), new QuickUnionUF(n),
new WeightedQuickUnionUF(n), new PathCompressionUF(n)};
System.out.printf("%5d %7d", n, paires.length);
for (UF uf : ufs) {
for (int[] x : paires) {
if (!uf.connected(x[0], x[1])) uf.union(x[0], x[1]);
}
double parPaire = (double) uf.acces() / paires.length;
System.out.printf(Locale.ROOT, " %10.1f", parPaire);
}
System.out.println();
}Lecture. Paires.jusquaConnexe tire des paires \((p, q)\) au hasard jusqu’à ce qu’une seule composante reste ; chaque version reçoit les mêmes paires et fait un connected, puis un union si la paire n’est pas reliée.
{.t-scriptsize}Paires au hasard (Random(2026)), \(n\) de \(1\,000\) à \(64\,000\) ; à \(64\,000\) : \(327\,882\) paires.
Lecture. QuickFind et QuickUnion coûtent des milliers d’accès par paire, qui croissent avec \(n\) (\(\times38\) et \(\times68\) de \(1\,000\) à \(64\,000\)). L’union pondérée coûte 10,2 accès par paire et garde une hauteur de 7 pour \(\log_2 n\approx16\) ; la compression ramène la hauteur à 2, pour 10,8 accès.
Quoi
Une forêt rangée dans parent[] ; find remonte à la racine, union relie deux racines, la plus petite sous la plus grande, et find aplatit le chemin parcouru.
Pourquoi on l’utilise
Avec union pondérée (par la taille ou par le rang) et compression, \(m\) opérations sur \(n\) éléments coûtent \(\mathcal{O}(m\,\alpha(n))\), où \(\alpha\) est l’inverse de la fonction d’Ackermann : \(\alpha(n)\leq4\) pour tout \(n\) utile (Tarjan, 1975 ; CLRS, § 19.4). Connexité dynamique, Kruskal, composantes d’une image.
Quand ça échoue
La structure ne sait pas séparer : une liaison coupée ne se retire pas d’une composante. Les éléments sont les entiers \(0..n-1\) : des noms de villages passent d’abord par une table de hachage (Séance 3).
Une forêt de huit éléments
parent = [1, 2, 3, 3, 3, 4, 5, 6].
find(7) sans compression.parent[] et les accès de find(7) avec compression.find(7), la nouvelle hauteur.| élément | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| avant | 1 | 2 | 3 | 3 | 3 | 4 | 5 | 6 |
| après | 1 | 2 | 3 | 3 | 3 | 3 | 3 | 3 |
Correction
private void construire(GraphePondere g) {
BinaryMinHeap<Arete> tas = new BinaryMinHeap<>();
for (Arete e : g.aretes()) tas.insert(e);
while (!tas.isEmpty() && arbre.size() < g.V() - 1) {
Arete e = tas.extractMin();
examinees++;
if (uf.connected(e.v, e.w)) { // meme composante : cycle
rejets++;
continue;
}
uf.union(e.v, e.w);
arbre.add(e);
poids += e.poids;
}
comparaisonsTas = tas.comparaisons;
}Lecture. Le code de la Séance 5, où relies(foret, v, w) devient uf.connected(v, w) et foret.addEdge(v, w) devient uf.union(v, w) : la forêt n’est plus parcourue.
0 Mbour, 1 Saly, 2 Malicounda, 3 Nguékhokh, 4 Joal, 5 Fadial, 6 Nianing, 7 Pointe Sarène ; coûts en millions de FCFA.
Sortie \(\rightarrow\)
99 119 11 129 11 12 149 11 12 14 159 11 12 14 15 169 11 12 14 15 16 22
total 99
Lecture. Lignes 1–3 : les 11 liaisons entrent dans le tas ; huit composantes.
Lecture. Lignes 4–10 : 4–5 (9) : racines 4 et 5, 5 passe sous 4.
Lecture. Lignes 4–10 : 6–7 (11) : racines 6 et 7, 7 passe sous 6.
Lecture. Lignes 4–10 : 0–1 (12) : racines 0 et 1, 1 passe sous 0.
Lecture. Lignes 4–10 : 0–2 (14) : racines 0 et 2, 2 passe sous 0.
Lecture. Lignes 4–10 : 2–3 (15) : racines 0 et 3, 3 passe sous 0.
Lecture. Lignes 4–10 : 0–6 (16) : racines 0 et 6, 6 passe sous 0.
Lecture. Lignes 4–6 : 1–2 (18) : find rend 0 pour les deux, rejetée.
Lecture. Lignes 4–6 : 2–6 (20) : find rend 0 pour les deux, rejetée.
Lecture. Lignes 4–10 : 4–7 (22) : find(7) rattache 7 à 0 (compression), puis 4 passe sous 0.
Lecture. Ligne 12 : 99 millions de FCFA ; 9 liaisons examinées, 2 rejetées par find, sans parcours.
Random r = new Random(221); // les graphes de la Seance 5
for (int V : new int[] {500, 1000, 2000, 4000}) {
GraphePondere g = new GraphePondere(V, false);
for (int[] a : Generateur.aretes(V, 4 * V, r)) g.addEdge(a[0], a[1], a[2]);
Kruskal k = new Kruskal(g); // test de cycle par parcours
KruskalUF u = new KruskalUF(g); // test de cycle par union-find
System.out.printf(Locale.ROOT, "%4d : %5d examinees ; parcours %8d visites ; "
+ "union-find %6d acces%n", V, u.examinees, k.visites, u.acces());
}Résultat \(\rightarrow\)
500 : 1718 examinees ; parcours 611838 visites ; union-find 19301 acces
1000 : 2971 examinees ; parcours 1986671 visites ; union-find 33723 acces
2000 : 5681 examinees ; parcours 7389527 visites ; union-find 65343 acces
4000 : 13589 examinees ; parcours 38476757 visites ; union-find 152747 acces
Lecture. Les graphes de la Séance 5 et les mêmes arêtes examinées. À \(V=4\,000\) : \(38\,476\,757\) sommets atteints contre \(152\,747\) accès, 252 fois moins ; environ 11 accès par arête examinée, quel que soit \(V\). Kruskal coûte alors \(\mathcal{O}(E\log E)\), le tas des arêtes.
public long comparaisons; // appels de plusPetit
public long acces; // acces au tableau
public abstract void trier(K[] a);
protected boolean plusPetit(K[] a, int i, int j) {
comparaisons++;
acces += 2;
return a[i].compareTo(a[j]) < 0;
}
protected void echanger(K[] a, int i, int j) {
K t = a[i];
a[i] = a[j];
a[j] = t;
acces += 4;
}Tri par comparaison
Un tri qui n’apprend rien des clés autrement qu’en les comparant deux à deux (compareTo). Il est en place s’il n’utilise qu’une mémoire auxiliaire constante (hors pile des appels).
Tri stable
Deux éléments de clés égales restent dans leur ordre de départ : un classement par note garde l’ordre alphabétique entre deux notes égales.
Lecture. Les quatre tris de la séance héritent de Tri : les mêmes compteurs les comparent.
Invariant
Avant le tour \(i\), a[0..i-1] est trié ; a[i] recule par échanges jusqu’à rencontrer un élément qui ne lui est pas supérieur. Chaque échange supprime une inversion (une paire mal ordonnée) : il y a autant d’échanges que d’inversions.
Lecture. La comparaison est stricte : à égalité, a[j] ne passe pas devant ; le tri est stable et en place (Sedgewick et Wayne, § 2.1 ; CLRS, § 2.1).
Sortie \(\rightarrow\)
[150, 300, 500, 650, 700, 900]
Lecture. Ligne 1 : six prix relevés au marché, en FCFA ; la partie triée, en jaune, commence par la case 0.
Lecture. Lignes 2–6 : \(i=1\), \(j=1\) : 300 \(<\) 650 : échange, 300 recule en case 0. \(j=0\) : la boucle s’arrête sans comparer.
Lecture. Lignes 2–3 : \(i=2\), \(j=2\) : 900 \(\geq\) 650 : une comparaison place 900.
Lecture. Lignes 2–6 : \(i=3\), \(j=3\) : 150 \(<\) 900 : échange, 150 recule en case 2.
Lecture. Lignes 3–6 : \(i=3\), \(j=2\) : 150 \(<\) 650 : échange, 150 recule en case 1.
Lecture. Lignes 3–6 : \(i=3\), \(j=1\) : 150 \(<\) 300 : échange, 150 recule en case 0. \(j=0\) : la boucle s’arrête sans comparer.
Lecture. Lignes 2–6 : \(i=4\), \(j=4\) : 500 \(<\) 900 : échange, 500 recule en case 3.
Lecture. Lignes 3–6 : \(i=4\), \(j=3\) : 500 \(<\) 650 : échange, 500 recule en case 2.
Lecture. Ligne 3 : \(i=4\), \(j=2\) : 500 \(\geq\) 300 : la boucle s’arrête, 500 est à sa place.
Lecture. Lignes 2–6 : \(i=5\), \(j=5\) : 700 \(<\) 900 : échange, 700 recule en case 4.
Lecture. Ligne 3 : \(i=5\), \(j=4\) : 700 \(\geq\) 650 : la boucle s’arrête, 700 est à sa place.
Lecture. Ligne 9 : 10 comparaisons, 7 échanges : un échange par inversion du tableau de départ.
Random r = new Random(2026);
for (int n : new int[] {1000, 2000, 4000}) {
Integer[][] cas = {Donnees.croissant(n), Donnees.auHasard(n, r),
Donnees.decroissant(n)};
System.out.printf("%4d", n);
for (Integer[] a : cas) {
TriInsertion<Integer> t = new TriInsertion<>();
t.trier(a);
System.out.printf(" %11d", t.comparaisons);
}
System.out.printf(" %9d%n", n * n / 4);
}Résultat \(\rightarrow\)
1000 999 251591 499500 250000
2000 1999 997770 1999000 1000000
4000 3999 3999750 7998000 4000000
Lecture. Colonnes : \(n\), comparaisons sur un tableau croissant, au hasard, décroissant, puis \(n^2/4\). Croissant : \(n-1\) ; décroissant : \(n(n-1)/2\), toutes les paires inversées ; au hasard : près de \(n^2/4\).
Cinq notes
a = [13, 9, 16, 9, 11].
| tour | tableau après | comp. | éch. |
|---|---|---|---|
| \(i=1\) | 9 13 16 9 11 |
1 | 1 |
| \(i=2\) | 9 13 16 9 11 |
1 | 0 |
| \(i=3\) | 9 9 13 16 11 |
3 | 2 |
| \(i=4\) | 9 9 11 13 16 |
3 | 2 |
| total | 8 | 5 |
Correction
@Override
public void trier(K[] a) {
K[] aux = Arrays.copyOf(a, a.length);
trier(a, aux, 0, a.length - 1);
}
private void trier(K[] a, K[] aux, int lo, int hi) {
if (hi <= lo) return; // 0 ou 1 element
int mid = lo + (hi - lo) / 2;
trier(a, aux, lo, mid); // moitie gauche
trier(a, aux, mid + 1, hi); // moitie droite
fusionner(a, aux, lo, mid, hi);
}Diviser, trier, fusionner
Trier chaque moitié par un appel récursif, puis fusionner les deux moitiés triées dans a[lo..hi]. Le tableau aux, de taille \(n\), est alloué une fois : le tri fusion n’est pas en place.
Lecture. Sedgewick et Wayne, § 2.2 ; CLRS, § 2.3. La récursion s’arrête à un élément (hi <= lo).
private void fusionner(K[] a, K[] aux, int lo, int mid, int hi) {
for (int k = lo; k <= hi; k++) aux[k] = a[k];
acces += 2L * (hi - lo + 1);
int i = lo; // tete de la moitie gauche
int j = mid + 1; // tete de la moitie droite
for (int k = lo; k <= hi; k++) {
if (i > mid) a[k] = aux[j++];
else if (j > hi) a[k] = aux[i++];
else if (plusPetit(aux, j, i)) a[k] = aux[j++];
else a[k] = aux[i++]; // egalite : la gauche d'abord
acces += 2;
}
}Lecture. Chaque tour de boucle écrit une case de a : la tête la plus petite des deux moitiés, ou le reste de la moitié non épuisée. À égalité, plusPetit(aux, j, i) est faux : la gauche passe d’abord, et le tri est stable.
Sortie \(\rightarrow\)
[150, 200, 450, 450, 500, 600, 850, 900]
Lecture. Lignes 2, 13 : fusionner(0, 3, 7) : les deux moitiés triées sont copiées dans aux, la gauche en bleu, la droite en orange.
Lecture. Lignes 3–4 : \(i\) pointe la tête de la moitié gauche, \(j\) celle de la droite.
Lecture. Lignes 5, 8 : \(k=0\) : 150 \(<\) 200 : a[0] reçoit 150, de la droite, et \(j\) avance.
Lecture. Lignes 5, 9 : \(k=1\) : 450 \(\geq\) 200 : a[1] reçoit 200, de la gauche, et \(i\) avance.
Lecture. Lignes 5, 9 : \(k=2\) : égalité, 450 et 450 : la gauche passe d’abord. C’est ce qui rend le tri stable.
Lecture. Lignes 5, 8 : \(k=3\) : 450 \(<\) 600 : a[3] reçoit 450, de la droite, et \(j\) avance.
Lecture. Lignes 5, 8 : \(k=4\) : 500 \(<\) 600 : a[4] reçoit 500, de la droite, et \(j\) avance.
Lecture. Lignes 5, 9 : \(k=5\) : 900 \(\geq\) 600 : a[5] reçoit 600, de la gauche, et \(i\) avance.
Lecture. Lignes 5, 9 : \(k=6\) : 900 \(\geq\) 850 : a[6] reçoit 850, de la gauche, et \(i\) avance.
Lecture. Lignes 5–6 : \(k=7\) : \(i>\) mid, la gauche est épuisée : a[7] reçoit 900.
Lecture. Ligne 14 : 8 cases écrites, 7 comparaisons : une fusion de \(n\) éléments compare au plus \(n-1\) fois.
Récurrence (Séance 2)
\(C(n)\leq C(\lceil n/2\rceil)+C(\lfloor n/2\rfloor)+n-1\), d’où \(C(n)\leq n\lceil\log_2 n\rceil\) : \(\log_2 n\) niveaux, au plus \(n-1\) comparaisons par niveau. Le théorème maître (\(a=b=2\), \(f(n)=\Theta(n)\)) donne \(\Theta(n\log n)\), quel que soit l’ordre de départ.
Lecture. Le tableau de départ de la fusion pas à pas sort des deux premiers niveaux : \(4+6+7=17\) comparaisons pour \(n=8\).
public int partition(K[] a, int lo, int hi) {
int r = lo + hasard.nextInt(hi - lo + 1);
echanger(a, r, hi); // le pivot en a[hi]
int i = lo; // a[lo..i-1] < pivot
for (int j = lo; j < hi; j++) {
if (plusPetit(a, j, hi)) {
echanger(a, i, j);
i++;
}
}
echanger(a, i, hi); // le pivot a sa place
return i;
}Lecture. Partition de Lomuto (CLRS, § 7.1) : le pivot, placé en a[hi], est comparé à chaque élément ; les plus petits sont rangés à gauche de \(i\). Pivot aléatoire (CLRS, § 7.3) : tiré par hasard.nextInt. Le pivot finit à sa place définitive, et les deux côtés sont triés séparément.
Sortie \(\rightarrow\)
4
Lecture. Lignes 2, 15 : partition(0, 7) : hasard (graine 2026) tire \(r=4\) ; le pivot est 600.
Lecture. Ligne 3 : le pivot 600 passe en case 7, la dernière ; 300 prend sa place.
Lecture. Ligne 4 : \(i=0\) : la zone des éléments plus petits que le pivot est vide.
Lecture. Lignes 5–6 : \(j=0\) : 750 \(\geq\) 600 : il reste dans la zone de droite, \(i\) ne bouge pas.
Lecture. Lignes 5–8 : \(j=1\) : 250 \(<\) 600 : échange des cases 0 et 1, puis \(i=1\).
Lecture. Lignes 5–6 : \(j=2\) : 950 \(\geq\) 600 : il reste dans la zone de droite, \(i\) ne bouge pas.
Lecture. Lignes 5–8 : \(j=3\) : 400 \(<\) 600 : échange des cases 1 et 3, puis \(i=2\).
Lecture. Lignes 5–8 : \(j=4\) : 300 \(<\) 600 : échange des cases 2 et 4, puis \(i=3\).
Lecture. Lignes 5–8 : \(j=5\) : 100 \(<\) 600 : échange des cases 3 et 5, puis \(i=4\).
Lecture. Lignes 5–6 : \(j=6\) : 850 \(\geq\) 600 : il reste dans la zone de droite, \(i\) ne bouge pas.
Lecture. Ligne 11 : le pivot passe en case 4 : à sa gauche les 4 prix plus petits, à sa droite les 3 plus grands.
Lecture. Lignes 12, 15 : partition rend 4 : 7 comparaisons. Le tri rapide trie ensuite a[0..3] et a[5..7].
Résultat \(\rightarrow\)
1000 : 499500 comparaisons ; n(n-1)/2 = 499500
2000 : 1999000 comparaisons ; n(n-1)/2 = 1999000
4000 : 7998000 comparaisons ; n(n-1)/2 = 7998000
Lecture. Aucun élément n’est strictement plus petit que le pivot : chaque partition rend lo, et le sous-tableau ne perd qu’un élément. \(n(n-1)/2\) comparaisons et \(n\) appels imbriqués, quels que soient les tirages.
Quoi
Partitionner autour d’un pivot tiré au hasard, puis trier récursivement les deux côtés ; en place, sans tableau auxiliaire.
Pourquoi on l’utilise
En moyenne, sur des clés distinctes, \(2(n+1)H_n-4n\approx2n\ln n\approx1{,}39\,n\log_2 n\) comparaisons (CLRS, § 7.4 ; Sedgewick et Wayne, § 2.3, proposition K) : \(25\,428\,905\) attendues pour \(n=1\,024\,000\), \(25\,033\,925\) mesurées. Le hasard porte sur les tirages, pas sur l’ordre des données : un tableau déjà trié n’est pas un pire cas.
Quand ça échoue
Le pire cas, \(n(n-1)/2\), reste possible ; il est certain avec des clés égales (Lomuto). La partition en trois zones (Sedgewick et Wayne, § 2.3) le traite. Le tri rapide n’est pas stable.
Sept prix (centaines de FCFA)
a = [35, 80, 15, 60, 45, 20, 70] ; le tirage donne \(r=4\).
echanger(r, hi).| étape | test | \(i\) | tableau |
|---|---|---|---|
| pivot | --- | 0 | 35 80 15 60 70 20 45 |
| \(j=0\) | \(35<45\) | 1 | 35 80 15 60 70 20 45 |
| \(j=1\) | \(80\geq45\) | 1 | 35 80 15 60 70 20 45 |
| \(j=2\) | \(15<45\) | 2 | 35 15 80 60 70 20 45 |
| \(j=3\) | \(60\geq45\) | 2 | 35 15 80 60 70 20 45 |
| \(j=4\) | \(70\geq45\) | 2 | 35 15 80 60 70 20 45 |
| \(j=5\) | \(20<45\) | 3 | 35 15 20 60 70 80 45 |
| fin | --- | 3 | 35 15 20 45 70 80 60 |
Correction
a[0..2] \(=\) 35, 15, 20 et a[4..6] \(=\) 70, 80, 60.Etudiant[] liste = {new Etudiant("Awa", 14), new Etudiant("Cheikh", 12),
new Etudiant("Coumba", 15), new Etudiant("Fatou", 12),
new Etudiant("Moussa", 14), new Etudiant("Ndeye", 12)}; // ordre alphabetique
Etudiant[] f = liste.clone();
new TriFusion<Etudiant>().trier(f);
System.out.println("fusion : " + Arrays.toString(f));
Etudiant[] q = liste.clone();
new TriRapide<Etudiant>(2026).trier(q);
System.out.println("rapide : " + Arrays.toString(q));Résultat \(\rightarrow\)
fusion : [Coumba 15, Awa 14, Moussa 14, Cheikh 12, Fatou 12, Ndeye 12]
rapide : [Coumba 15, Awa 14, Moussa 14, Ndeye 12, Cheikh 12, Fatou 12]
Lecture. Le tri fusion garde Cheikh, Fatou, Ndeye dans l’ordre alphabétique ; le tri rapide donne Ndeye, Cheikh, Fatou : un échange à longue distance fait passer un élément devant un autre de même clé.
Arbre de décision
Pour une taille \(n\) fixée, un tri par comparaison se représente par un arbre binaire : chaque nœud est une comparaison, chaque feuille un ordre de sortie. Une exécution suit un chemin de la racine à une feuille ; sa longueur est le nombre de comparaisons.
Lecture. Le tri par insertion sur \(a\), \(b\), \(c\) : 6 feuilles, une par ordre possible ; 2 comparaisons au mieux, 3 au pire.
Théorème (CLRS, § 8.1 ; Sedgewick et Wayne, § 2.2)
Tout tri par comparaison fait au moins \(\lceil\log_2 n!\rceil\) comparaisons au pire : l’arbre doit avoir au moins \(n!\) feuilles, une par permutation, et un arbre binaire de hauteur \(h\) a au plus \(2^h\) feuilles. Donc \(2^h\geq n!\), soit \(h\geq\log_2 n!\).
Ordre de grandeur
\[ \log_2 n!\;\geq\;\log_2\Big(\big(\tfrac n2\big)^{n/2}\Big)=\tfrac n2\log_2\tfrac n2,\qquad \log_2 n!=n\log_2 n-n\log_2 e+\mathcal{O}(\log n) \]
La première inégalité vient des \(n/2\) facteurs de \(n!\) au moins égaux à \(n/2\) ; la seconde est la formule de Stirling.
Lecture. Le tri fusion, en \(n\log_2 n\), est donc optimal à un facteur près. Le tri par dénombrement (CLRS, § 8.2) échappe à la borne : il ne compare pas, il compte des clés entières.
Petites tailles
| \(n\) | \(n!\) | \(\lceil\log_2 n!\rceil\) | fusion, pire cas |
|---|---|---|---|
| 4 | 24 | 5 | 5 |
| 5 | 120 | 7 | 8 |
| 8 | 40 320 | 16 | 17 |
Correction
| tri | pire cas | moyenne | mesuré (\(n=1\,024\,000\)) | mémoire | stable |
|---|---|---|---|---|---|
| insertion | \(n(n-1)/2\) | \(\approx n^2/4\) | \(0{,}25\,n^2\) (à \(n=16\,000\)) | en place | oui |
| fusion | \(\leq n\lceil\log_2 n\rceil\) | \(\Theta(n\log n)\) | \(0{,}94\,n\log_2 n\) | aux de \(n\) cases |
oui |
| rapide | \(n(n-1)/2\) | \(\approx1{,}39\,n\log_2 n\) (1,24 à cette taille) | \(1{,}22\,n\log_2 n\) | en place (pile des appels) | non |
| tas (Séance 4) | \(\mathcal{O}(n\log n)\) | \(\Theta(n\log n)\) | \(1{,}87\,n\log_2 n\) | tas de \(n\) éléments | non |
| borne | \(\lceil\log_2 n!\rceil\) | \(0{,}93\,n\log_2 n\) |
Lecture. Comparaisons des classes du cours sur les mêmes entiers au hasard (Random(2026)). Le tri fusion est à 1 % de la borne ; le tri par insertion reste le plus économe sur un tableau presque trié (\(n-1\) comparaisons s’il est trié).
{.t-scriptsize}Classes TriInsertion, TriFusion, TriRapide (graine 2026) et TriTas ; \(n\) de \(1\,000\) à \(1\,024\,000\) (\(16\,000\) pour l’insertion).
À retenir
find, union, connected sur une partition de \(0..n-1\).find en un accès, union en \(\Theta(n)\) ; QuickUnion : une forêt, find en \(\Theta(d)\), \(d\leq\) hauteur, jusqu’à \(n-1\).| séance | structures et algorithmes | coûts à retenir |
|---|---|---|
| 1. Types abstraits | listes, piles, files ; tableau ou liste chaînée | ajout en fin \(\mathcal{O}(1)\) amorti |
| 2. Analyse | \(\mathcal{O}\), \(\Omega\), \(\Theta\), récurrences, théorème maître | \(2T(n/2)+n=\Theta(n\log n)\) |
| 3. Hachage | chaînage, sondage linéaire, rehash | \(\mathcal{O}(1)\) en moyenne, charge bornée |
| 4. Arbres et tas | ABR, AVL, tas binaire, tri par tas | \(\mathcal{O}(\log n)\) (AVL, tas) |
| 5. Graphes | parcours, tri topologique, Dijkstra, Kruskal | \(\Theta(V+E)\), \(\mathcal{O}((V+E)\log V)\) |
| 6. Disjoints et tris | union-find, insertion, fusion, rapide | \(\mathcal{O}(\alpha(n))\), \(\Theta(n\log n)\) |
Lecture. Chaque structure se choisit par les opérations qu’elle rend rapides : l’accès par clé (hachage), l’ordre (arbres, tri), le minimum (tas), la connexité (graphes, union-find). Le tas de la Séance 4 sert à Dijkstra, à Kruskal et au tri par tas ; l’union-find de cette séance achève Kruskal.
Structures de données et algorithmes avancés — Séance 6 Dr. El Hadji Bassirou TOURÉ • DMI/FST/UCAD
Lecture. Ligne 1 : huit villages, chacun seul dans sa composante :
id[p]vaut \(p\).Lecture. Lignes 3–7, 11 :
union(0, 1):pid\(=0\),qid\(=1\) ; la boucle lit les 8 cases et la case 0 passe à 1.Lecture. Lignes 3–7, 11 :
union(1, 2):pid\(=1\),qid\(=2\) ; la boucle lit les 8 cases et les cases 0, 1 passent à 2.Lecture. Lignes 3–7, 11 :
union(2, 3):pid\(=2\),qid\(=3\) ; la boucle lit les 8 cases et les cases 0, 1, 2 passent à 3.Lecture. Lignes 3–7, 11 :
union(4, 5):pid\(=4\),qid\(=5\) ; la boucle lit les 8 cases et la case 4 passe à 5.Lecture. Lignes 3–7, 11 :
union(6, 7):pid\(=6\),qid\(=7\) ; la boucle lit les 8 cases et la case 6 passe à 7.Lecture. Lignes 3–7, 11 :
union(5, 7):pid\(=5\),qid\(=7\) ; la boucle lit les 8 cases et les cases 4, 5 passent à 7.Lecture. Lignes 3–7, 11 :
union(3, 7):pid\(=3\),qid\(=7\) ; les quatre cases de valeur 3 passent à 7.Lecture. Ligne 12 :
connected(0, 6)se lit en deux accès :id[0]etid[6]valent 7.Lecture. Ligne 13 : 7 unions, chacune a lu les 8 cases : une union coûte \(\Theta(n)\), un
findun accès.