Le tri par arbre


     



    C'est un tri également appelé tri par tas (heapsort, en anglais). Il utilise une structure de données temporaire dénommée "tas" comme mémoire de travail. C'est une variante de méthode de tri par sélection où l'on parcourt le tableau des éléments en sélectionnant et conservant les minimas successifs (plus petits éléments partiels) dans un arbre parfait partiellement ordonné.
     
     
    Définitions préliminaires
    d'arbres parfaits, partiellement ordonnés et tas

    Définition - 1 / Arbre parfait :
    c'est un arbre binaire dont tous les noeuds de chaque niveau sont présents sauf éventuellement au dernier niveau où il peut manquer des noeuds (noeuds terminaux  = feuilles), dans ce cas l'arbre parfait est un arbre binaire incomplet et les feuilles du dernier niveau doivent être regroupées à partir de la gauche de l'arbre.

     

    un arbre parfait complet


    un autre arbre  parfait incomplet


    un arbre non parfait


    un autre arbre non parfait

    Définition - 2 / Arbre partiellement ordonné :
    C'est un arbre étiqueté dont les noeuds appartiennent à un ensemble muni d'une relation d'ordre total (les nombres entiers, réels etc... en sont des exemples) tel que pour un noeud donné tous ses fils ont une valeur supérieure ou égale à celle de leur père.

    Exemple de deux arbres partiellement ordonnés sur l'ensemble  {20,27,29,30,32,38,45,45,50,51,67,85} d'entiers naturels :

    Nous remarquons que la racine d'un tel arbre est toujours l'élément de l'ensemble possédant la valeur minimum (le plus petit élément de l'ensemble), car la valeur de ce noeud par construction est inférieure à celle de ses fils et par transitivité de la relation d'ordre à celles de ses descendants c'est le minimum. Si donc nous arrivons à ranger une liste d'éléments dans un tel arbre le minimum de cette liste est atteignable immédiatement comme racine de l'arbre.
     

    Définition - 3 / Le tas :
    On appelle tas un tableau représentant un arbre parfait partiellement ordonné.

     
     
    Principe du tri par tas 

    A) Spécification abstraite

    Soit une liste (a1, a2, ... , an) d'éléments appartenant à un ensemble totalementordonné (entiers, chaînes de caractères,...) le principe est de parcourir la liste (a1, a2, ... , an) en ajoutant chaque élément ak dans un arbre parfait partiellement ordonné.
     


     

    B) Spécification concrète

    La suite (a1, a2, ... , an) est rangée dans un tableau T[...] correspondant au tableau d'initialisation. Puis les éléments de ce tableau sont ajouter et traiter un par un dans un arbre avant d'être ajoutés dans un tableau trié en ordre décroissant ou croissant, selon le choix de l'utilisateur.

    Rappelons qu'un arbre binaire parfait se représente classiquement par un tableau :
    Si t est ce tableau, on a les règles suivantes :
    • t[1] est la racine :


    •  
    • t[i div 2] est le père de t[i] pour i > 1 :


    •  
    • t[2 * i] et t[2 * i + 1] sont les deux fils, s'ils existent, de t[i] :


    •  
    • si p est le nombre de noeuds de l'arbre et si 2 * i = p, t[i] n'a qu'un fils, t[p].

    • si i est supérieur à p div 2, t[i] est une feuille.

     
     
     
    B-1 ) Figures illustrant le placement des éléments de la liste dans l'arbre :

    Exemple d'initialisation sur un tableau à 15 éléments  :


    Insertion du premier élément (le nombre 7) à la racine de l'arbre :

     


    Insertion du second élément (le nombre 27, 27 > 7 donc c'est un fils du noeud 7)  :

     


    Insertion du troisième élément (le nombre 41, 41 > 7 c'est un fils du noeud 7)  :

     
     


    Insertion du quatrième élément (le nombre 30,  comme le niveau des fils du noeud 7 est complet, 30 est placé automatiquement sur une nouvelle feuille d'un nouveau niveau, puis il est comparé à son père 27, 30 > 27 c'est donc un fils du noeud 27 il n'y a pas d'échange )  :

     
     


    Insertion du cinquième élément (le nombre 10) : L'insertion du nouvel élément 10 dans l'arbre a lieu automatiquement dans la dernière feuille vide de l'arbre à partir de la gauche, ici le fils droit de 27  :

    Puis 10 est comparé à son père 27, cette fois 10 est plus petit que 27, il y a donc échange des places entre 27 et 10, ce qui donne un nouvel arbre :
    Puis 10 est comparé à son nouveau père 7, cette fois il n'y a pas d'échange car 10 est plus grand que 7.
     

    Le processus continue avec l'élément suivant de la liste le nombre 31:

    31  est automatiquement rangé sur la première feuille disponible à gauche soit le fils gauche de 41:

    Puis 31 est comparé à son père, comme 31 < 41 il y a échange des valeurs, puis 31 est comparé à son nouveau père 7 comme 31 > 7 il n'y a plus d'échange :
    etc....


    Observons maintenant comment l'élément minimum de la liste qui est le onzième élément, soit le nombre 3, est rangé dans l'arbre.


    Voici l'état de l'arbre avant introduction du nombre 3 :

    Le nombre 3 est introduit sur la première feuille libre du niveau bleu :

    Il est comparé à son père le noeud 17, comme 3 < 17, il y a alors échange :

    Il est ensuite comparé à son nouveau père le noeud 10, comme 3 < 10, il y a alors échange :

    Il est enfin comparé à son dernier père (la racine de l'arbre), comme 3 < 7, il y a alors échange :

    C'est l'élément 3 qui est maintenant la racine de l'arbre, comme les 4 éléments suivants sont plus grand que 3, ils seront rangés plus bas dans l'arbre (cf. figure ci-dessous) :

    Conclusion sur le premier passage :
     
     
    La liste initiale :

    est finalement stockée ainsi :


     
     
    B-2 ) Figures illustrant la suppression de la racine  :

    Le nombre 3 est le plus petit élément, on le supprime de l'arbre et l'on prend l'élément de la dernière feuille du dernier niveau (ici le nombre 25) et on le place à la racine (à la place qu'occupait le nombre 3)

    et l'on recommence les échanges éventuels en comparant la racine avec ses descendants :
    le fils gauche 23 est inférieur à 25 => échange
    Arrêt du processus (33 > 25 et 30 > 25)

    On obtient le deuxième plus petit élément à la racine de l'arbre, ici le nombre 7.

    On continue à "vider" ainsi l'arbre et déplaçant les feuilles vers la racine et en échangeant les noeuds mal placés. A la fin nous avons extrait à chaque étape le plus petit élément de chaque sous liste restante et nous obtenons les éléments de la liste classés par ordre (croissant ou décroissant selon notre choix).

    Notre version de tri par tas comporte les étapes suivantes :


    On en déduit l'algorithme ci-dessous composé de 2 sous algorithmes Ajouter pour la première étape, et Supprimer pour la seconde.
     
     

    C) Algorithme :
     
    Algorithme Ajouter 
    Entrée  P : entier ; x : entier // P nombre d'éléments dans le tas, x élément à ajouter
                  Tas[1..max]  : tableau d'entiers // le tas
    Sortie  P : entier 
                  Tas[1..max] // le tas
    Local  j, temp : entiers 

    début
      P ¬ P + 1 ; // incrémentation du nombre d'éléments du tas
      j ¬ P ; // initialisation de j à la longueur du tas (position de la dernière feuille)
      Tas[P] ¬ x ; // ajout l'élément x à la dernière feuille dans le tas
      Tantque (j > 1) et (Tas[j] < Tas[j div 2]) faire ; // tant que l'on est pas arrivé à la racine et que le "fils" est inférieur à son "père", on permute les 2 valeurs
             temp ¬ Tas[j] ;
             Tas[j] ¬ Tas[j div 2] ;
             Tas[j div 2] ¬ temp ;
             j ¬ j div 2 ;
      finTant
    FinAjouter

    Algorithme Supprimer 
    Entrée : P : entier // P nombre d'éléments contenu dans l'arbre
                  Tas[1..max]  : tableau d'entiers // le tas
    Sortie : P : entier // P nombre d'éléments contenu dans l'arbre
                 Tas[1..max]  : tableau d'entiers // le tas
                 Lemini : entier  // le plus petit de tous les éléments du tas
    Local i, j, temp : entiers ;

    début
      Lemini ¬ Tas[1] ; // retourne  la racine (minimum actuel) pour stockage éventuel
      Tas[1] ¬ Tas[P] ; // la racine prend la valeur de la dernière feuille de l'arbre
      P ¬ P - 1 ; 
      j ¬ 1 ; 
      Tantque j <= (P div 2) faire
          // recherche de l'indice i du plus petit des descendants de Tas[j]
          si (2 * j = P ) ou (Tas[2 * j] < Tas[2 * j + 1]) 
                  alors i ¬ 2 * j ;
          sinon i ¬ 2 * j +1 ;
          Fsi;
         // Echange éventuel entre Tas[j] et son fils Tas[i]
         si Tlab[j] > Tlab[i] alors
                temp ¬ Tlab[j] ;
                Tlab[j] ¬ Tlab[i] ;
                Tlab[i] ¬ temp ;
                j ¬ i ;
         sinon Sortir ;
         Fsi;
      finTant
    FinSupprimer

    Algorithme Tri_par_arbre
    Entrée  Tas[1..max] // le tas
                 TabInit[1..max] // les données initiales
    Sortie    TabTrie[1..max]: tableaux d'entiers //  tableau une fois trié
    Local    P  : Entier //  P le nombre d'éléments à trier
               Lemin : entier  // le plus petit de tous les éléments du tas

    début
        P ¬ 0;
       Tantque P < max faire
           Ajouter(P,Tas,TabInit[P+1]); // appel de l'algorithme Ajouter
       finTant;
       Tantque P >= 1 faire
           Supprimer(P,Tas,Lemin) ; // appel de l'algorithme Supprimer
           TabTrie[max-P] ¬ Lemin ; // stockage du minimum dans le nouveau tableau
       finTant;
    Fin Tri_par_arbre


     

    D) Complexité :

    Cette version de l'algorithme construit le tas par n appels de la procédure Ajouter et effectue les sélections par n - 1 appels de la procédure Supprimer.

    Le coût et de l'ordre de      comparaisons, au pire.

    La complexité au pire en nombre de comparaisons  est en O(n log n).
     

    Le nombre d'échanges dans le tas est majoré par le nombre de comparaisons et il est du même ordre de grandeur.

    La complexité au pire en nombre de transferts du tri par tas est donc en O(n log n).
     
     

    E) Programme pascal :
     
    program TriParArbre;
    const Max =10; // nombre maximal d'éléments du tableau
    type TTab=array [1..Max] of integer;  // TTab : Type Tableau
    var Tas, TabInit, TabTrie : TTab; // Tas, tableau initial puis tableau triéTableau
          P, Lemin : integer;
     
    procedure Ajouter (var Tas : TTab; var P, x : integer);
      var j, temp : integer ;
    begin
        P := P + 1 ;
        J := P ;
        Tas[P] := x ;
        if j>1 then
        While Tas[j] < Tas[j div 2] do
         begin
             temp := Tas[j] ;
             Tas[j] := Tas[j div 2] ;
             Tas[j div 2] := temp ;
             j := j div 2 ;
            if j<=1 then break;
         end
    end; // Ajouter

    procedure Supprimer (var Tas:TTab; var P, Lemin : integer);
     var i, j, temp : integer ;
     begin
       Lemin := Tas[1] ;
       Tas[1] := Tas[P] ;
       P := P - 1 ;
       j := 1 ; 
       While j <= (P div 2) do
       begin
          if (2 * j = P ) or (Tas[2 * j] < Tas[2 * j + 1])
               then i := 2 * j
          else i := 2 * j +1 ;
          if Tas[j] > Tas[i] then
          begin
             temp := Tas[j] ;
             Tas[j] := Tas[i] ;
             Tas[i] := temp ;
             j := i ;
          end
          else break ;
       end
    end; // Supprimer

    procedure Initialisation(var Tab:TTab) ;
    // Tirage aléatoire de Max nombres de 1 à 100
    var i : integer;  // i : Indice de tableau 
    begin
      randomize;
      for i := 1 to Max do
         Tab[i] := random(100);
    end;

    procedure Impression(Tab:TTab) ; 
    // Affichage des Max nombres 
    var i : integer;
    begin
      writeln('---------------------');
      for i:= 1 to Max do write(Tab[i] : 3, ' | ');
           writeln;
    end;

    begin
      Initialisation(TabInit);
      writeln('TRI PAR ARBRE');
      writeln;
      Impression(TabInit);
      P:=0;
      while p < Max do
        Ajouter ( Tas, p, TabInit[p+1] ); 
      while p >= 1 do
      begin
          Supprimer ( Tas, P, Lemin ) ; 
          TabTrie[Max-P]:=Lemin
      end;
      Impression(TabTrie);
      writeln('----------------------');
      readln
    end.

    REMARQUE IMPORTANTE

    Notons que dans la procédure nous avons  traduit la condition de la boucle :
    Tantque (j > 1) et (Tas[j] < Tas[j div 2]) faire
             temp ¬ Tas[j] ;
             Tas[j] ¬ Tas[j div 2] ;
             Tas[j div 2] ¬ temp ;
             j ¬ j div 2 ;
      finTant

    par les lignes de programmes suivantes :
    if j>1 then
        While Tas[j] < Tas[j div 2] do
         begin
             temp := Tas[j] ;
             Tas[j] := Tas[j div 2] ;
             Tas[j div 2] := temp ;
             j := j div 2 ;
            if j<=1 then break
         end

    ceci afin d'éviter un incident dû à un effet de bord. Lorsque l'indice "j" prend par exemple la valeur 1, l'indice "j div 2" vaut alors 0 et cette valeur n'est pas valide  puisque l'indice de tableau varie entre 1..Max.
     

    On peut pallier aussi d'une autre manière à cet inconvenient en ajoutant une sentinelle "à gauche dans le tableau" en étendant la borne inférieure à la valeur 0, les indices pouvant alors varier entre 0..Max. On obtient une autre écriture de la procédure "Ajouter" suivant l'algorithme de près :
     
    type TTab=array [0..Max] of integer;   // 0 est l'indice sentinelle 

    procedure Ajouter (var Tas : TTab; var P, x : integer);
      var j, temp : integer ;
    begin
        P := P + 1 ;
        J := P ;
        Tas[P] := x ;
        While (j > 1) et (Tas[j] < Tas[j div 2]) do
         begin
             temp := Tas[j] ;
             Tas[j] := Tas[j div 2] ;
             Tas[j div 2] := temp ;
             j := j div 2 ;
         end
    end; // Ajouter
     

    Résultat de l'exécution du programme précédent :