Probleme – Le tri par tas, en place et en
Exercice · niveau 3 (difficile) · informatique (MP2I/MPI), chapitre 11 — Arbres de recherche, tas et files de priorité
Énoncé
- Écrire
entasser(t, n, i), qui rétablit l'invariant de tas maximum en faisant descendre l'élément d'indice , en supposant les deux sous-arbres de déjà valides. Donner son variant et sa complexité. - En déduire
construire, qui transforme un tableau quelconque en tas maximum. Prouver sa correction par un invariant, et montrer que son coût est — et non . - Écrire le tri par tas en place, et prouver sa correction.
- Mesurer, et comparer à la construction par insertions.
Corrigé
1. Le tassage d'un élément.
/* Fait descendre t[i] jusqu'à sa place, en supposant que les sous-arbres
enracinés en 2i+1 et 2i+2 vérifient déjà l'invariant de tas MAXIMUM.
Postcondition : le sous-arbre enraciné en i vérifie l'invariant.
Précondition : 0 <= i < n <= taille du tableau. */
void entasser(int t[], int n, int i) {
assert(0 <= i && i < n);
while (1) {
int g = 2 * i + 1, d = 2 * i + 2, plus_grand = i;
if (g < n && t[g] > t[plus_grand]) { plus_grand = g; }
if (d < n && t[d] > t[plus_grand]) { plus_grand = d; }
if (plus_grand == i) { return; } /* la place est trouvée */
int tmp = t[i]; t[i] = t[plus_grand]; t[plus_grand] = tmp;
i = plus_grand;
}
}
Terminaison : le variant est , c'est-à-dire le nombre de niveaux sous ; à chaque tour est remplacé par un de ses fils, donc descend d'un niveau. La boucle fait au plus tours. Correction : après l'échange, le plus grand des trois est en , donc l'invariant tient en ; le seul endroit où il peut être rompu est la nouvelle position, et c'est précisément là qu'on continue. Complexité : , donc au pire, et comparaisons par niveau.
2. La construction, et son coût linéaire.
/* Transforme le tableau t de n éléments en un tas MAXIMUM, EN PLACE.
Complexité : Theta(n). Mémoire supplémentaire : O(1). */
void construire(int t[], int n) {
/* INVARIANT : tous les sous-arbres enracinés en i+1, ..., n-1 sont des tas. */
for (int i = n / 2 - 1; i >= 0; i = i - 1) { entasser(t, n, i); }
}
Pourquoi partir de : les indices à sont les feuilles, et une feuille est un tas à elle seule. L'invariant est donc vrai avant le premier tour. Conservation : au tour , les sous-arbres de et sont des tas par l'invariant — ce sont des indices supérieurs à —, donc entasser s'applique et rend le sous-arbre de valide. Terminaison : le variant est , qui décroît. À la sortie, : tous les sous-arbres, dont celui de la racine, sont des tas.
Le coût. L'analyse naïve — appels à — donne et surestime. Le point est que entasser coûte la hauteur sous , pas la hauteur de l'arbre : les nœuds nombreux sont les moins chers. Un arbre complet à nœuds compte au plus nœuds de hauteur , d'où
en employant , qui vaut en . C'est la convergence de cette série qui rend la construction linéaire, et c'est le seul point du calcul qui compte.
3. Le tri en place.
/* Trie t par ordre CROISSANT, en place. Complexité : Theta(n log n) dans TOUS
les cas. Mémoire supplémentaire : O(1). */
void tri_par_tas(int t[], int n) {
construire(t, n); /* tas MAXIMUM : le plus grand en t[0] */
/* INVARIANT : t[j+1..n-1] contient les n-1-j plus grands éléments, TRIÉS,
et t[0..j] est un tas maximum. */
for (int j = n - 1; j >= 1; j = j - 1) {
int tmp = t[0]; t[0] = t[j]; t[j] = tmp; /* le maximum à sa place */
entasser(t, j, 0); /* on rétablit sur t[0..j-1] */
}
}
Pourquoi un tas MAXIMUM pour un tri croissant : c'est la clé du procédé. Le maximum sort en premier, et on le range à la fin — c'est-à-dire dans la case que l'on vient de libérer. Un tas minimum obligerait à ranger le minimum au début, où il faudrait décaler tout le reste. Le tas maximum et l'ordre croissant vont ensemble parce que la zone triée grandit par la droite.
Correction : à l'initialisation , la zone triée est vide et est un tas ; conservation : est le maximum de , donc son rang est exactement dans le tableau final, et entasser(t, j, 0) rétablit l'invariant de tas sur ; terminaison : le variant est , la boucle fait tours. À la sortie : est trié et est le plus petit.
Complexité : pour la construction, puis tassages à : dans tous les cas, meilleur comme pire. Mémoire supplémentaire : trois entiers, soit .
4. Les mesures. Mesuré en OCaml sur des entiers tirés uniformément :
| insertions | tassage de Floyd | |||
|---|---|---|---|---|
| comparaisons | échanges | comparaisons | échanges | |
Les deux colonnes croissent linéairement, et le rapport reste à . Sur des données aléatoires, les insertions sont linéaires elles aussi — la remontée moyenne est bornée. La séparation n'apparaît qu'au pire cas : mesuré sur une suite décroissante insérée dans un tas minimum, à , les insertions demandent comparaisons contre pour le tassage, soit un facteur — et ce facteur croît en .
Le tassage, lui, coûte comparaisons et échanges, quelles que soient les données : mesuré de à , ces deux constantes ne bougent pas. Le tri complet, lui, demande comparaisons à , et le tableau ressort trié — vérifié contre Array.sort.
Les autres exercices de ce chapitre Le cours du chapitre
Un blocage sur cet exercice ? Le tuteur d'Adloun guide par questions, sans donner la réponse.