Files de priorité : le tas binaire
Cours complet · OCaml (option informatique), chapitre 19 · prépas MPSI et MP, option informatique
Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre
<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>19.1 Introduction et motivation
Une file de priorité est une collection d'où l'on extrait toujours l'élément le plus prioritaire (ici, le plus petit) — comme un service d'urgences traite d'abord les cas les plus graves. C'est l'outil qui manquait au chapitre 16 : Dijkstra a besoin, à chaque étape, du sommet non traité de plus petite distance. Avec une liste ou un tableau, le trouver coûte ; une file de priorité bien faite le donne en .
La structure reine pour cela est le tas binaire, un arbre binaire presque complet rangé… dans un simple tableau (chapitre 5). Ce chapitre l'implémente — insertion, extraction du minimum, construction — puis en tire le tri par tas (chapitre 9) et une version efficace de Dijkstra (chapitre 16).
19.2 Le tas binaire
Un tas binaire est un arbre binaire presque complet (tous les niveaux remplis sauf peut-être le dernier, de gauche à droite) vérifiant la propriété de tas : tout nœud est ses enfants. Conséquence : le minimum est à la racine.
On range cet arbre dans un tableau, niveau par niveau : la racine en 0, et pour le nœud d'indice i,
| parent de `i` | `(i - 1) / 2` |
|---|---|
| fils gauche de `i` | `2 i + 1` |
| fils droit de `i` | `2 i + 2` |
La taille du tas évoluant, on l'encapsule dans un enregistrement à champ mutable (chapitre 7) :
type tas = { mutable taille : int; donnees : int array }
let cree capacite = { taille = 0; donnees = Array.make capacite 0 }
let est_vide t = t.taille = 0
let minimum t = if est_vide t then failwith "tas vide" else t.donnees.(0)
19.3 Insérer : percolation vers le haut
On place le nouvel élément à la fin (dernière feuille), puis on le fait remonter tant qu'il est plus petit que son parent — pour rétablir la propriété de tas.
let insere t x =
let d = t.donnees in
d.(t.taille) <- x;
t.taille <- t.taille + 1;
let i = ref (t.taille - 1) in
while !i > 0 && d.((!i - 1) / 2) > d.(!i) do
let p = (!i - 1) / 2 in
let tmp = d.(!i) in d.(!i) <- d.(p); d.(p) <- tmp; (* échange avec le parent *)
i := p
done
Complexité : Insertion
L'élément remonte d'au plus la hauteur de l'arbre, soit échanges (un arbre presque complet de nœuds a une hauteur ). Insertion en .
19.4 Extraire le minimum : percolation vers le bas
Le minimum est à la racine. On le retire, on met la dernière feuille à sa place, puis on la fait descendre en l'échangeant avec le plus petit de ses enfants tant qu'elle viole la propriété de tas.
let extrait_min t =
if est_vide t then failwith "tas vide";
let d = t.donnees in
let mini = d.(0) in
t.taille <- t.taille - 1;
d.(0) <- d.(t.taille); (* la dernière feuille monte à la racine *)
let i = ref 0 and continuer = ref true in
while !continuer do
let g = 2 * !i + 1 and dr = 2 * !i + 2 in
let petit = ref !i in
if g < t.taille && d.(g) < d.(!petit) then petit := g;
if dr < t.taille && d.(dr) < d.(!petit) then petit := dr;
if !petit <> !i then begin
let tmp = d.(!i) in d.(!i) <- d.(!petit); d.(!petit) <- tmp;
i := !petit
end
else continuer := false
done;
mini
Complexité : Extraction
L'élément déplacé descend d'au plus la hauteur de l'arbre : . La file de priorité offre donc minimum en , insere et extrait_min en — bien mieux qu'une liste triée (insertion ) ou non triée (extraction ).
<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>19.5 Exercices résolus
Niveau (application directe du cours)
Donner les indices du parent, du fils gauche et du fils droit du nœud 4. Dessiner l'arbre du tableau [| 1; 3; 2; 7; 5 |] et vérifier que c'est un tas.
Démonstration
Parent de 4 : (4-1)/2 = 1 ; fils gauche : 2*4+1 = 9 ; fils droit : 10. Pour [|1;3;2;7;5|] : racine 1, enfants 3 et 2 ; enfants de 3 (indice 1) : 7 et 5. Chaque nœud est ses enfants ( ; ) : c'est bien un tas, de minimum 1.
Écrire cree, est_vide et minimum.
Démonstration
let cree capacite = { taille = 0; donnees = Array.make capacite 0 }
let est_vide t = t.taille = 0
let minimum t = if est_vide t then failwith "tas vide" else t.donnees.(0)
minimum se lit en donnees.(0) (la racine) en temps constant : c'est l'invariant du tas. La capacité fixe le nombre maximal d'éléments (tableau de taille fixe).
Écrire insere et dérouler l'insertion de 0 dans le tas [| 1; 3; 2 |].
Démonstration
let insere t x =
let d = t.donnees in
d.(t.taille) <- x;
t.taille <- t.taille + 1;
let i = ref (t.taille - 1) in
while !i > 0 && d.((!i - 1) / 2) > d.(!i) do
let p = (!i - 1) / 2 in
let tmp = d.(!i) in d.(!i) <- d.(p); d.(p) <- tmp;
i := p
done
On place 0 en indice 3 : [|1;3;2;0|]. Son parent (indice 1) est 3 > 0 : on échange → [|1;0;2;3|]. Le nouveau parent (indice 0) est 1 > 0 : on échange → [|0;1;2;3|]. 0 est remonté à la racine : c'est le nouveau minimum.
Niveau (raisonnement intermédiaire)
Écrire extrait_min.
Démonstration
let extrait_min t =
if est_vide t then failwith "tas vide";
let d = t.donnees in
let mini = d.(0) in
t.taille <- t.taille - 1;
d.(0) <- d.(t.taille);
let i = ref 0 and continuer = ref true in
while !continuer do
let g = 2 * !i + 1 and dr = 2 * !i + 2 in
let petit = ref !i in
if g < t.taille && d.(g) < d.(!petit) then petit := g;
if dr < t.taille && d.(dr) < d.(!petit) then petit := dr;
if !petit <> !i then begin
let tmp = d.(!i) in d.(!i) <- d.(!petit); d.(!petit) <- tmp; i := !petit
end
else continuer := false
done;
mini
On renvoie la racine, on y remonte la dernière feuille, puis on la fait redescendre vers le plus petit enfant tant qu'elle est trop grande. Les tests g < t.taille et dr < t.taille vérifient l'existence des enfants.
Écrire est_un_tas d n : le tableau d (longueur utile n) respecte-t-il la propriété de tas ?
Démonstration
let est_un_tas d n =
let ok = ref true in
for i = 1 to n - 1 do
if d.((i - 1) / 2) > d.(i) then ok := false (* parent > enfant : violé *)
done;
!ok
Il suffit de vérifier, pour chaque nœud i (sauf la racine), qu'il n'est pas plus petit que son parent. Parcourir une seule fois les indices 1..n-1 suffit : chaque relation parent-enfant est ainsi testée. Coût .
Écrire tri_par_tas arr qui trie un tableau d'entiers à l'aide d'un tas.
Démonstration
let tri_par_tas arr =
let n = Array.length arr in
let t = cree n in
for i = 0 to n - 1 do insere t arr.(i) done; (* tout insérer *)
for i = 0 to n - 1 do arr.(i) <- extrait_min t done (* extraire dans l'ordre *)
On insère tous les éléments dans un tas, puis on les extrait : l'extraction du minimum à répétition les rend dans l'ordre croissant. insertions et extractions, chacune en , soit — la même garantie que le tri fusion (chapitre 9).
Niveau (approfondissement)
Écrire k_plus_petits arr k : la liste des k plus petits éléments de arr, triés.
Démonstration
let k_plus_petits arr k =
let n = Array.length arr in
let t = cree n in
for i = 0 to n - 1 do insere t arr.(i) done;
let rec extraire j = if j = 0 then [] else extrait_min t :: extraire (j - 1) in
extraire k
On bâtit un tas de tous les éléments, puis on en extrait les k plus petits (les premiers à sortir). Coût ; on pourrait viser avec une construction de tas en (exercice 8).
On peut transformer un tableau quelconque en tas plus vite qu'en insérant un à un. Écrire construit d n qui réorganise d.(0..n-1) en tas par percolations vers le bas, des nœuds internes vers la racine.
Démonstration
let descendre d n i0 = (* percoler le nœud i0 vers le bas dans d.(0..n-1) *)
let i = ref i0 and continuer = ref true in
while !continuer do
let g = 2 * !i + 1 and dr = 2 * !i + 2 in
let petit = ref !i in
if g < n && d.(g) < d.(!petit) then petit := g;
if dr < n && d.(dr) < d.(!petit) then petit := dr;
if !petit <> !i then begin
let tmp = d.(!i) in d.(!i) <- d.(!petit); d.(!petit) <- tmp; i := !petit
end
else continuer := false
done
let construit d n =
for i = n / 2 - 1 downto 0 do descendre d n i done
On percole chaque nœud interne (indices n/2 - 1 à 0), des feuilles vers la racine : quand on traite i, ses sous-arbres sont déjà des tas. L'analyse fine donne un coût total (et non ). Remarque : OCaml propose downto ; si l'on s'en tient au seul to, on parcourt i de 0 à n/2-1 et l'on descend n/2 - 1 - i.
Pour Dijkstra, on a besoin d'une file de priorité de couples (distance, sommet) ordonnés par distance. Adapter le tas.
Démonstration
type tas_couples = { mutable taille : int; donnees : (int * int) array }
let cree_c capacite = { taille = 0; donnees = Array.make capacite (0, 0) }
let insere_c t x =
let d = t.donnees in
d.(t.taille) <- x; t.taille <- t.taille + 1;
let i = ref (t.taille - 1) in
while !i > 0 && fst d.((!i - 1) / 2) > fst d.(!i) do
let p = (!i - 1) / 2 in
let tmp = d.(!i) in d.(!i) <- d.(p); d.(p) <- tmp; i := p
done
Tout est identique au tas d'entiers, mais on compare la première composante (fst, la distance) : le couple de plus petite distance remonte à la racine. extrait_min s'adapte de même (comparer par fst). On dispose alors d'une file de priorité « distance, sommet ».
Esquisser Dijkstra (chapitre 16) en utilisant la file de priorité de couples, et donner sa complexité.
Démonstration
let dijkstra_tas adj depart n_aretes =
let n = Array.length adj in
let infini = 1_000_000 in
let dist = Array.make n infini in
let t = cree_c (n + n_aretes) in (* capacité : assez de poussées *)
dist.(depart) <- 0;
insere_c t (0, depart);
while t.taille > 0 do
let (du, u) = extrait_min_c t in
if du = dist.(u) then (* ignorer les entrées périmées *)
List.iter (fun (v, poids) ->
if du + poids < dist.(v) then begin
dist.(v) <- du + poids;
insere_c t (dist.(v), v) (* on pousse la nouvelle distance *)
end
) adj.(u)
done;
dist
Au lieu de chercher le minimum en , on l'extrait du tas en . À chaque relâchement réussi, on pousse le couple (nouvelle_distance, v) ; une même destination peut être poussée plusieurs fois, d'où le test du = dist.(u) qui ignore les entrées périmées. Coût — bien meilleur que le du chapitre 16 sur les graphes creux. (extrait_min_c est la version « couples » de extrait_min.)
- File de priorité : extraire toujours le plus prioritaire (ici, le minimum).
- Tas binaire : arbre binaire presque complet rangé dans un tableau ; nœud
i: parent(i-1)/2, fils2i+1et2i+2. Propriété : tout nœud ses enfants ; minimum à la racine. minimumen ;insere(percolation vers le haut) etextrait_min(percolation vers le bas) en .- Tri par tas : tout insérer puis tout extraire, . Construction d'un tas à partir d'un tableau quelconque en (percolations des nœuds internes vers la racine).
- Application : Dijkstra avec file de priorité passe de à — décisif sur les graphes creux.
19.6 Exercices d'entraînement
Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.
Thème A — Manipulations de base.
- [11.] Écrire
taille tetcapacite t. - [12.] Que se passe-t-il si l'on insère au-delà de la capacité ? Proposer une vérification.
- [13.] Adapter le tas pour un max-tas (extraire le maximum).
Thème B — Autour du tri.
- [14.] Comparer expérimentalement
tri_par_taset le tri fusion (chapitre 9) en comptant les comparaisons. - [15.] Écrire un tri par tas en place (sans tableau auxiliaire) à l'aide de
construitet de descentes. - [16.] Trouver le
k-ième plus petit élément sans trier entièrement.
Thème C — Variantes de file de priorité.
- [17.]
fusionne t1 t2: fusionner deux tas en un seul. - [18.] Implémenter une file de priorité par liste triée et comparer les complexités avec le tas.
- [19.] Maintenir le tas avec des priorités flottantes (
float).
Thème D — Applications.
- [20.] Implémenter complètement
extrait_min_cet faire tournerdijkstra_tassur un petit graphe. - [21.] Fusionner
klistes triées en une seule, à l'aide d'un tas (les têtes des listes). - [22.] Discuter : pour Dijkstra, quand le tas l'emporte-t-il vraiment sur la version (densité du graphe) ?