Adloun

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é

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 :

insertionstassage de Floyd
comparaisonséchangescomparaisonsé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.