Vous êtes sur la page 1sur 11

Module : Algorithmique et structure de données - Année universitaire : 2016-2017

Université Sétif 1 - Faculté des Sciences


Département d’informatique
Filière : Licence Académique
Module : Algorithmique et structure de données
Année universitaire : 2016-2017
CHAP 4 : ARBRE-TAS
& TRI PAR TAS
Rappel : Un arbre binaire est complet si et seulement si toutes ses feuilles sont à la même profondeur.

un arbre parfait complet un autre arbre parfait incomplet

un arbre non parfait


un autre arbre non parfait

Un tas (monceau), en anglais heap, (ou plus précisément un tas binaire) est une structure de données
répondant aux conditions suivantes :
• c'est un arbre binaire complet (ou presque complet): tous les niveaux sauf le dernier doivent être
totalement remplis et si le dernier ne l'est pas totalement, alors il doit être rempli de gauche à droite (toutes ses feuilles sont de
même profondeur).
• il est ordonné en tas

On dit qu'un arbre est ordonné en tas lorsque la propriété suivante est vérifiée : les nœuds sont ordonnés par
leurs clés respectives,
- On parle d’un tas-max (Max-heap) si tout nœud a une valeur plus grande ou égale à celles de ses
deux fils. Pour tous A et B nœuds de l'arbre tels que B soit un fils de A clé(A) ≥ clé(B)
Dans ce cas, le plus grand élément de l’arbre est à la racine
Exemple :

- On parle d’un tas-min (Min-heap) si tout nœud a une valeur plus petite ou égale à celles de ses deux
fils. Dans ce cas, le plus petit élément de l’arbre est à la racine.
Exemple :
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

Ils sont ainsi très utilisés pour implémenter les files à priorités car ils permettent des insertions en temps
logarithmique et un accès direct au plus grand élément.
L'efficacité des opérations effectuée sur des tas est très importante dans de nombreux algorithmes sur les
graphes.

En d’autre terme :
Un tas (heap) est un arbre binaire T qui emmagasine une collection de clés (ou paires clé-élément) comme
nœuds internes et qui satisfait les deux propriétés suivantes:
- Propriété d’ordre: clé(parent) ≤ clé(enfant) pour le tas-min
clé(parent) ≥ clé(enfant) pour le tas-max
- Propriété structurelle: arbre binaire complet

Le fait qu'un tas soit un arbre binaire complet permet de le représenter d'une manière intuitive par un tableau
unidimensionnel.

• la racine de l'arbre est étiquetée 1,


• le fils gauche d'un nœud étiqueté n, est étiqueté 2n,
• le fils droit d'un nœud étiqueté n, est étiqueté 2n+1.
• Le père du nœud étiqueté n, est n/2

Le cas du tas est l'un des rares où un vecteur dont le premier élément a l'indice 1 pourrait être avantageux.

Un arbre binaire parfait a une représentation compacte sans trous dans le tableau:

L'arbre (qui n’est pas un tas) :


Module : Algorithmique et structure de données - Année universitaire : 2016-2017

Est représenté par :


b se trouve a la position : 2*1(indice de a) =2
c se trouve a la position : 2*1(indice de a) +1 =3
e se trouve a la position : 2*2(indice de b) + 1 =5
g se trouve a la position : 2*3(indice de c) +1 =7
h se trouve a la position : 2*5(indice de e) =10
i se trouve a la position : 2*5(indice de e) + 1 =11

1 2 3 4 5 6 7 8 9 10 11 12 13 14
a b C e F G h i j

Les deux opérations à définir sur un tas sont :


• L'insertion (Entasser) d'un nouveau nœud lors de la création du tas
• L’extraction du nœud qui réalise le minimum pour la relation d'ordre
Toute la difficulté consiste à maintenir la structure de tas entre deux opérations.
Pour insérer un nouveau nœud, rien de plus simple, il suffit de l'ajouter tout en bas de l'arbre et de le faire
remonter à sa place, c'est-à-dire à l'échanger avec son père tant que le poids de celui-ci (le père) est supérieur
à son propre poids (le nouveau nœud). Cette opération porte généralement le joli nom de percolation. Au
pire, on remonte le nouveau nœud jusqu'à la racine, donc l'algorithme correspondant tourne en O(ln n).

Voici l’algorithme ajouter


Algorithme Ajouter //dans un tas-min
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

Explication détaillée :
Considérons que l'on veuille ajouter le nœud à notre tas binaire :
On insère à la prochaine position libre (la position libre la plus à droite possible sur le dernier niveau) puis
on effectue l'opération suivante (que l'on appelle percolation vers le haut ou percolate-up) pour rétablir si
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

nécessaire la propriété d'ordre du tas binaire : tant que n'est pas la racine de l'arbre et que est strictement
supérieur à son père on échange les positions entre et son père.

Soit la hauteur de notre tas binaire, lors de l'algorithme ci-dessus on effectue au plus échanges. Or
comme un tas binaire est un arbre binaire parfait on a (où est le nombre de nœuds du
tas binaire et une constante) donc la complexité est bien .

Programme C pour un tas-max


static void ajouter (int v)
{
++nTas;
int i = nTas - 1;
whi le (( i > 1) && ( a[pere (i)] < clé ))
{ a[i] = a[pere (i)];
i = pere (i);
}
a[i] = v;
ou plus précisément :
while (i > 0 && a [i/2] <= v)
{ a[i] = a[i/2];
i = i/2;
}
a[i] = v;
}

Exemple : On insère 50 dans un tas-max


On insère 50 à la prochaine position libre

On compare 50 et son père 28, comme 50>28 on échange On compare 50 et son père 41, comme 50>41 on échange
les positions de 50 et 28 les positions de 50 et 41

On compare 50 et son père 53, comme 50<53 on


n'échange pas les positions de 50 et 53, on a fini de
modifier notre arbre, à présent il respecte toutes les
contraintes.
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

L'extraction (supprimer un élémént)


Considérons l'opération de suppression du premier élément de la file. Il faut alors retirer la racine de l'arbre
représentant la file, ce qui donne deux arbres!
Le plus simple pour reformer un seul arbre est d'appliquer l'algorithme suivant:
- On met l'élément le plus à droite de la dernière ligne à la place de la racine,
- On compare sa valeur avec celle de ses enfants, on échange cette valeur avec celle du vainqueur de
ce tournoi, et
- On réitère cette opération jusqu'à ce que la condition des tas soit vérifiée.
Bien sûr, il faut faire attention, quand un noeud n'a qu'un enfants, et ne faire alors qu'un petit tournoi à deux.
Le placement de la racine en bonne position est illustré dans l’exemple suivant :

Explication détaillée :
On souhaite retirer la racine de notre tas binaire (c'est-à-dire le maximum de notre tas selon la relation
d'ordre associée). Cependant il faut conserver la structure de tas binaire après la suppression, on procède
donc de la manière suivante :
On supprime la racine et on met à sa place le nœud qui était en dernière position de l'arbre binaire (donc le
nœud le plus à droite sur le dernier niveau) que l'on notera . Puis on fait l'opération suivante (que l'on
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

appelle percolation vers le bas ou percolate-down) : tant que a des fils et que est strictement inférieur à
un de ses fils, on échange les positions entre et le plus grand de ses fils.

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

Programme en C pour un tas-min


static void supprimer ()
{
int i, j; int v; a[0] = a[nTas - 1]; // noter que le tas est indicé de 0 à n
--nTas;
i = 0;
v = a[0];
while (2*i + 1 < nTas)
{
j = 2*i + 1;
if (j + 1 < nTas)
if (a[j + 1] > a[j])
++j;
if (v >= a[j])
break;
a[i] = a[j];
i = j;
}
a[i] = v;
}
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

Complexité : Par le même argument que pour l'algorithme de ajouter, on fait au plus échange donc la
complexité est bien

Exemple : On retire la racine du tas-max suivant :


On remplace donc la racine par le nœud en
dernière position (ici c'est 20)

On compare 20 et son fils maximum qui est 41,


comme 41>20 on échange 20 et 41
On compare 20 et son fils maximum qui est 36,
comme 36>20 on échange 20 et 36

On compare 20 et son fils maximum qui est 31,


comme 31>20 on échange 20 et 31

20 n'a plus de fils donc on a fini. On a rétabli les


propriétés d'ordre et de structure.
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

Construction d’un tas :


Interpréter le tableau comme un tas (incorrect) puis rétablir l’ordre en partant des feuilles (vues comme des
tas corrects).
 Comme dans le tri fusion on part des tas préexistant (d’abord les feuilles)
 Et on construit ensuite l’arbre dont la racine est le père de ces feuilles… etc…

Exemple :

Application de tri
Ces opérations sur les tas permettent de résoudre un problème de tri à l’aide d’un algorithme appelé:
HeapSort. Cet algorithme de tri possède la bonne propriété d'avoir une complexité temporelle de 0(n log n).

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é.

L’algorithme de HeapSort se divise en deux phases,


- la première consiste à construire un tas dans le tableau à trier,
- la seconde à répéter l'opération de prendre l'élément maximal, le retirer du tas en le mettant à droite
du tableau.

Il reste à comprendre comment on peut construire un tas à partir d'un tableau quelconque.

On remarque d'abord que l'élément de gauche du tableau est à lui seul un tas. Puis on ajoute à ce tas, le
deuxième élément avec la procédure Ajouter que nous venons de voir, puis le troisième, .... etc. A la fin,
on obtient bien un tas de N éléments dans le tableau à trier. Le programme est:

static void heapSort ()


{
int i,v; nTas = 0;
for (i = 0; i < N; ++i)
ajouter (a[i]);
for (i = N - 1; i >= 0; --i)
{
v = maximum();
supprimer();
a[i] = v;
}
}
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

Rappel : TRI PAR FUSION


Ce tri est basé sur la technique algorithmique divisé pour régner (comme pour la recherche dichotomique).
L'opération principale de l'algorithme est la fusion, qui consiste à réunir deux listes triées en une seule.
L'efficacité de l'algorithme vient du fait que deux listes triées peuvent être fusionnées en temps linéaire.
Le tri fusion se décrit naturellement sur des listes et c'est sur de telles structures qu'il est à la fois le plus
simple et le plus rapide. Cependant, il fonctionne aussi sur des tableaux. La version la plus simple du tri
fusion sur les tableaux a une efficacité comparable au tri rapide, mais elle n'opère pas en place : une zone
temporaire de données supplémentaire de taille égale à celle de l'entrée est nécessaire (des versions plus
complexes peuvent être effectuées sur place mais sont moins rapides). Sur les listes, sa complexité est
optimale, il se montre très simplement et ne requiert pas de copie en mémoire temporaire.

Algorithme

L'algorithme est naturellement décrit de façon récursive.

1. Si le tableau n'a qu'un élément, il est déjà trié. On le renvoie.


2. Sinon, on sépare le tableau en deux parties à peu près égales.
3. On appelle de façon récursive l'algorithme sur chacune des deux parties.
4. On fusionne ces tableaux en un tableau trié, qu'on renvoie.

Le procédé récursif s'arrête car on manipule à chaque appel de la fonction des tableaux de taille moitié. En
détail :

Procédure tri_fusion(tableau t)
n = longueur(t)
si n > 1 alors
u = tri_fusion(t[1], …, t[n / 2]) // première moitié
v = tri_fusion(t[n / 2 + 1], …, t[n]) // deuxième moitié
a = 1 // pointeur pour parcourir les éléments de u
b = 1 // pointeur pour parcourir les éléments de v
pour i allant de 1 à n faire
si (a <= longueur(u)) // si tous les éléments de u n'ont pas été parcourus
et (b > longueur(v) // et si tous les éléments de v ont été parcourus
ou u[a] ≤ v[b]) alors // ou si l'élément courant de u est plus petit
t[i] = u[a] // on choisit l'élément courant de u comme élément suivant de t
a = a + 1 // on avance dans u
sinon'''
t[i] = v[b] // on choisit l'élément courant de v comme élément suivant de t
b = b + 1 // on avance dans v
fin si
fin pour
fin si
renvoyer t
fin procédure

Mise en œuvre sur des listes

L'algorithme suivant est détaillé de sorte qu'il soit traduisible en n'importe quel langage impératif en
quelques minutes. La liste à trier est de taille n. Pour la concision et l'efficacité de l'algorithme, on suppose
que la liste à trier comporte au moins 2 éléments et que :

 soit elle est simplement chaînée, non circulaire et précédée par un maillon racine p (hors de la liste
mais pointant vers son premier élément) ;
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

 soit elle est simplement chaînée et circulaire ;


 soit elle est doublement chaînée et circulaire.

Dans tous les cas, la liste est triée après le chaînon p passé en paramètre, c'est-à-dire que le chainon
successeur de p sera le plus petit de la liste. Des descriptions un peu moins concises mais libérées de ces
contraintes structurelles existent.

fonction trier(p, n)
Q := n/2 (division entière)
P := n-Q
si P >= 2
q := trier(p, P)
si Q >= 2 p := trier(q, Q)
sinon
q := p.suivant
fin
q := fusionner(p, P, q, Q)
renvoyer q
fin

fonction fusionner(p, P, q, Q)
répéter indéfiniment
si valeur(p.suivant) > valeur(q.suivant)
déplacer le maillon q.suivant après le maillon p
si Q = 1 quitter la boucle
Q := Q-1
sinon
si P = 1
tant que Q >= 1
q := q.suivant
Q := Q-1
fin
quitter la boucle
fin
P := P-1
fin
p := p.suivant
fin
renvoyer q
fin

Le déplacement d'un maillon q.suivant après un maillon p nécessite un pointeur temporaire t. Si la liste est
simplement chainée, le déplacement se fait par cet échange de liens :

t := q.suivant
q.suivant := t.suivant
t.suivant := p.suivant
p.suivant := t

Si la liste est doublement chainée et circulaire, le déplacement se fait par cet échange de liens :

t := q.suivant

q.suivant := t.suivant
q.suivant.précédent := q

t.précédent := p
t.suivant := p.suivant
p.suivant.précédent := t
p.suivant := t
Module : Algorithmique et structure de données - Année universitaire : 2016-2017

Ainsi décrit, l'algorithme peut être hybridé très facilement avec d'autres tris. Cela se fait en rajoutant une
condition sur la toute première ligne de la fonction fonction trier(p, n). Sur de petites sous-listes, elle a pour
rôle de remplacer toutes les opérations qui suivent par un tri de complexité quadratique mais en pratique
plus rapide. Dans la suite, les conditions P >= 2 et Q >= 2 peuvent alors être retirées.

Exemple : Opération de fusion

Fusionner [1;2;5] et [3;4] : le premier élément de la liste fusionnée sera le premier élément d'une des deux
listes d'entrée (soit 1, soit 3) car ce sont des listes triées.

 Comparer 1 et 3 : 1 est plus petit


o [2;5] - [3;4] → [1]
 Comparer 2 et 3 : 2 est plus petit
o [5] - [3;4] → [1;2]
 Compare 5 et 3 → 3 est plus petit
o [5] - [4] → [1;2;3]
 Compare 5 et 4 : 4 est plus petit
o [5] → [1;2;3;4]
 Résultat de la fusion :
o [1;2;3;4;5]

Vous aimerez peut-être aussi