SPÉ MP, MP∗ TD03 Année 2016/2017 Tas et files de priorité On dit

publicité
SPÉ MP, MP∗
Année 2016/2017
TD03
Tas et files de priorité
On dit d’un arbre binaire qu’il est presque complet, lorsqu’il est rempli sur tous ses niveaux, sauf
éventuellement le plus profond, sur lequel les noeuds situés le plus à droite peuvent éventuellement être
vides. Un tas est un arbre binaire presque complet T vérifiant la propriété suivante : pour tout noeud x
de T , les valeurs des fils de x sont inférieures ou égales à la valeur de x.
Pour simplifier, tous les arbres utilisés dans ce TD auront des noeuds dont les valeurs sont du type
int. Dans des situations plus concrètes, on stockerait dans chaque noeud un objet x ainsi qu’une clé qui,
elle, serait un nombre entier.
On peut représenter un arbre binaire presque complet T par un tableau t :
— La racine de T est placée en t.(0).
— Le fils gauche et le fils droit de cette racine sont placés respectivement en t.(1) et t.(2).
— Les noeuds de profondeur 2 sont rangés aux emplacements t.(3) à t.(6).
— Les noeuds de profondeur 3 sont rangés aux emplacements t.(7) à t.(14).
— etc.
Question : étant donné un noeud de l’arbre T , placé dans le tableau t à l’indice i, quel est l’indice
dans le tableau t du père de ce noeud (lorsque i > 0) ? de son fils gauche ? de son fils droit ?
Question : dans un tas, où se trouve le noeud de plus grande valeur ? de plus petite valeur ?
On représente la structure d’arbre binaire presque complet par le type Caml suivant :
type abpc = {
data : int array;
mutable taille : int
};;
Étant donné un arbre binaire presque complet t, le champ t.data est un tableau contenant les valeurs
des noeuds de t. Le champ t.taille est un entier compris (au sens large) entre 0 et Array.length
t.data : l’arbre correspondant au sous-tableau de t.data dont les indices sont compris entre 0 et
t.taille - 1 est organisé en tas. Ainsi, pour un arbre « désorganisé », le champ taille est égal à
0. Si, au contraire, tout l’arbre est organisé en tas, ce champ est mis à la valeur maximale. Dans la suite,
lorsque l’on parlera du tas t, on s’intéressera donc uniquement aux indices de t.data compris entre 0 et
t.taille-1.
1. Soit t un tas. À quels indices de t.data se trouvent les feuilles de t ?
2. Soit x un noeud (d’indice i dans le tableau t.data) de l’arbre t. On suppose que le sous-arbre
de t de racine x est organisé en tas, à cela près que le noeud x est (peut-être) inférieur à l’un
de ses fils, et risque de compromettre la structure de tas. On rétablit la structure de tas pour le
sous-arbre de racine x au moyen de l’algorithme suivant :
— Déterminer parmi les trois noeuds x, son fils gauche (s’il existe) et son fils droit (s’il existe), le
noeud y qui a la plus grande valeur. S’il y a violation de la structure de tas, alors :
— Échanger les valeurs des noeuds x et y.
— Recommencer l’opération sur le noeud y.
Écrire la fonction entasser : tas -> int -> unit réalisant les opérations décrites ci-dessus.
3. On désire maintenant construire, à partir d’un arbre binaire presque complet t, un arbre presque
complet ayant une structure de tas. Pour cela :
— On donne à t.taille la valeur maximale Array.length t.
— Pour i variant de t.taille/2 - 1 à 0, on applique entasser t i.
Écrire la fonction construire_tas : tas -> unit.
4. L’algorithme suivant, appelé assez logiquement « tri par tas », trie un tableau v d’entiers :
— Créer un tas t dont les noeuds sont les éléments de v.
— Pour i variant de Array.length t -1 à 0 :
— Échanger t.data.(0) et t.data.(i) : t.data.(0) est en effet le noeud maximal du tas.
— Diminuer t.taille de 1 : l’ex-élément d’indice 0 est maintenant hors du tas.
— Entasser le noeud t.data.(0) : il viole en effet la structure de tas.
Écrire la fonction tri_tas : int array -> unit.
5. Quelle taille maximale de tableau votre fonction peut-elle trier en 10 secondes ?
6. Les tas permettent également d’implémenter ce que l’on appelle des files de priorité. Une file de
priorité est un ensemble F d’objets munis chacun d’une priorité. On peut
— Ajouter dans F un objet x avec une priorité p (push).
— Retirer de F l’objet de priorité maximale (pop).
— Rechercher sans le supprimer l’objet de F de priorité maximale (top).
Les files de priorité sont utillisées dans de très nombreux algorithmes. Pour n’en citer qu’un,
l’algorithme de Dijkstra qui permet de trouver des plus courts chemins à l’intérieur d’un graphe.
Encore une fois, par souci de simplicité, nous supposerons que nos files de priorité contiennent
uniquement des entiers représentant des priorités (et pas les objets auxquels sont associées ces
priorités). Une file de priorité sera donc pour nous un tas.
(a) La fonction de recherche de la plus grande priorité est immédiate. Écrire top : abpc -> int.
(b) Pour supprimer de la file t l’objet de plus petite priorité, on échange les valeurs t.data.(0)
et t.data.(t.taille - 1). Après avoir décrémenté t.taille, on entasse depuis la racine.
Écrire la fonction pop : abpc -> int.
(c) La fonction d’insertion dans la file de priorité est la suivante :
let push x t =
t.data.(t.taille) <- x;
t.taille <- t.taille + 1;
detasser t (t.taille - 1)
Il reste à écrire detasser : soit x un noeud (d’indice i dans le tableau t.data) de l’arbre t. On
suppose que t est organisé en tas, à cela près que le noeud x a (peut-être) une valeur supérieure
à celle de son père, et risque de compromettre la structure de tas. On rétablit la structure de
tas en échangeant éventuellement x et son père, puis en réitérant s’il y a eu échange. Écrire la
fonction detasser: abpc -> int -> unit.
7. On part d’une file de priorité initialement vide, puis on effectue n entassements et n détassements
de nombres aléatoires. Quelle valeur de n votre algorithme peut-il traiter en 10 secondes ?
Téléchargement