Aller au contenu

Tas

On va définir ici des fonctions dans un fichier tas.ml, cela permettra ensuite de l'utiliser dans un autre fichier avec Tas (c'est ce module qui est utilisé dans le TP sur la compression de Huffman). C'est la raison pour laquelle on ne va pas préfixer les noms de fonctions par tas.

On réalise ici des tas max, rangés dans un tableau : la racine est à l'indice 0 et les fils du nœud d'indice \(i\) sont aux indices \(2i+1\) et \(2i+2\).

On propose ici une interface assez simple mais deux améliorations sont proposées librement :

  • faire en sorte de ne pas avoir à donner tout de suite un élément du bon type avant le premier ajout ;
  • faire en sorte de ne pas avoir à donner une capacité initiale en ayant un tas redimensionnable comme les tableaux dynamiques.

On définit le type

type 'a t = {
    elements : 'a array;
    mutable taille : int
}

Question 1. Écrire une fonction

val cree : int -> 'a -> 'a t

telle que cree taille_max defaut crée un tas de capacité maximale taille_max et dont les éléments sont initialisés à defaut.

Corrigé
let cree taille_max defaut = 
    { 
        elements = Array.make taille_max defaut; 
        taille = 0 
    }

Remarque

C'est une restriction pour le typage d'avoir à passer une valeur par défaut. Vous pouvez vous en passer en changeant le type des éléments du tableau en 'a option et en mettant None partout au début.

Question 2. Écrire des fonctions

val est_vide : 'a t -> bool
val taille : 'a t -> int

qui indiquent respectivement si un tas est vide et son nombre d'éléments.

Corrigé
let est_vide tas = tas.taille = 0

let taille tas = tas.taille

Question 3. Écrire des fonctions

val pere : int -> int
val gauche : int -> int
val droite : int -> int

qui indiquent les indices respectifs du père, du fils gauche et du fils droit d'un indice donné.

Corrigé
let pere a =
    if a = 0
    then failwith "racine"
    else (a+1)/2 - 1

let gauche a = 2*(a+1) - 1
let droite a = 2*(a+1)

On a bien \(\text{pere}(i) = \left\lfloor \frac{i-1}{2} \right\rfloor\), \(\text{gauche}(i) = 2i + 1\) et \(\text{droite}(i) = 2i+2\).

Question 4. Écrire une fonction

val maximum : 'a t -> 'a

qui renvoie le maximum d'un tas non vide.

Corrigé

On en profite pour définir deux fonctions utilitaires.

let maximum tas = tas.elements.(0)

let nth tas i = tas.elements.(i)

let swap v x y =
    let t = v.(x) in
    v.(x) <- v.(y);
    v.(y) <- t

Question 5. Écrire une fonction

val ajoute : 'a -> 'a t -> unit

qui rajoute un élément dans le tas.

Corrigé

On place l'élément dans la première case libre puis on le fait remonter tant qu'il est plus grand que son père.

let ajoute x tas =
    let pos = ref tas.taille in
    tas.elements.(tas.taille) <- x;
    tas.taille <- tas.taille + 1;
    while !pos > 0 && nth tas (pere !pos) < nth tas !pos do
        let p = pere !pos in
        swap tas.elements p !pos;
        pos := p
    done

Question 6. Écrire une fonction

val max_pere_fils : 'a t -> int -> int

telle que max_pere_fils tas i renvoie l'indice du plus grand élément entre i, gauche i et droite i (parmi ceux qui sont dans le tas). Ce sera utile pour la fonction suivante.

Corrigé
let max_pere_fils tas a =
    let max_courant = ref a in

    if tas.taille > gauche a 
        && nth tas (gauche a) > nth tas !max_courant
    then max_courant := gauche a;
    if tas.taille > droite a 
        && nth tas (droite a) > nth tas !max_courant
    then max_courant := droite a;
    !max_courant

Question 7. Écrire une fonction

val extrait_maximum : 'a t -> 'a

qui extrait et renvoie le maximum d'un tas non vide.

Corrigé

On remplace la racine par le dernier élément, puis on le fait descendre en l'échangeant avec le plus grand de ses fils tant que nécessaire.

let extrait_maximum tas =
    assert (tas.taille > 0);
    tas.taille <- tas.taille - 1;
    let x = tas.elements.(0) in
    if tas.taille > 0
    then begin
        let pos = ref 0 in
        tas.elements.(0) <- tas.elements.(tas.taille);
        while max_pere_fils tas !pos <> !pos do
            let max_fils = max_pere_fils tas !pos in
            swap tas.elements !pos max_fils;
            pos := max_fils
        done
    end;
    x

Tri par tas

Question 8. Écrire une fonction

val tri_par_tas : 'a list -> 'a list

qui trie une liste dans l'ordre croissant à l'aide d'un tas.

Corrigé
let tri_par_tas l =
    match l with
    | [] -> []
    | x :: _ ->
        let tas = cree (List.length l) x in
        List.iter (fun y -> ajoute y tas) l;
        (* on extrait les maxima : on construit la liste
           triée en commençant par la fin *)
        let res = ref [] in
        while not (est_vide tas) do
            res := extrait_maximum tas :: !res
        done;
        !res

Chaque ajout et chaque extraction coûte \(O(\log n)\), le tri coûte donc \(O(n \log n)\).

Comme on l'a vu en classe, on peut réaliser un tri par tas en place dans un tableau.

Question 9. Écrire des fonctions

val vers_tas : 'a array -> unit
val depuis_tas : 'a array -> unit

qui vont respectivement :

  • prendre un tableau et le transformer en place en tableau d'un tas ;
  • prendre un tableau d'un tas et le transformer en place en tableau trié dans l'ordre croissant.
Corrigé
(* descente de l'élément d'indice i dans le tas formé
   des n premières cases de t *)
let rec descend t n i =
    let m = ref i in
    if gauche i < n && t.(gauche i) > t.(!m) then m := gauche i;
    if droite i < n && t.(droite i) > t.(!m) then m := droite i;
    if !m <> i
    then begin
        swap t i !m;
        descend t n !m
    end

let vers_tas t =
    let n = Array.length t in
    (* les feuilles sont déjà des tas : on traite les nœuds
       internes du dernier vers la racine *)
    for i = n/2 - 1 downto 0 do
        descend t n i
    done

let depuis_tas t =
    let n = Array.length t in
    for k = n-1 downto 1 do
        (* le maximum du tas t.(0..k) va à sa place définitive *)
        swap t 0 k;
        descend t k 0
    done

Pour trier un tableau t en place, il suffit d'appeler vers_tas t puis depuis_tas t.

Files de priorité

On va définir ici un module dans filePriorite.ml qui permettra donc d'accéder à ces fonctions avec FilePriorite.

Comme il faut utiliser Tas, vous pouvez passer par dune pour gérer les modules (voir le TP sur la compression de Huffman) ou compiler à la main.

On considère le type :

type ('a, 'b) t = ('b * 'a) Tas.t

Un élément de type 'a de priorité de type 'b est ainsi stocké dans le tas sous la forme du couple (priorite, element) : comme les couples sont comparés d'abord selon leur première composante, la racine du tas est un élément de priorité maximale.

Question 10. Écrire une fonction

val cree : int -> 'a -> 'b -> ('a, 'b) t

telle que cree taille_max defval defprio crée une file de priorité de taille maximale taille_max et dont les éléments sont initialisés à la valeur defval de priorité defprio.

Remarque

Là aussi, on peut faire mieux avec une fonction cree : unit -> ('a, 'b) t si on le souhaite.

Question 11. Écrire une fonction

val ajoute : ('a, 'b) t -> 'a -> 'b -> unit

qui ajoute un élément avec une priorité donnée.

Question 12. Écrire une fonction

val retire : ('a, 'b) t -> 'a

qui retire un élément quelconque de priorité maximale.

Corrigé des questions 10 à 12
type ('a, 'b) t = ('b * 'a) Tas.t

let cree taille_max defval defprio = 
    Tas.cree taille_max (defprio, defval)

let ajoute file x p =
    Tas.ajoute (p, x) file

let retire file =
    let _, x = Tas.extrait_maximum file in
    x

On remarque qu'à priorités égales, ce sont les éléments eux-mêmes qui départagent les couples : il faut donc que le type 'a soit comparable avec < (pas de fonctions par exemple).