J-0
00m
00j
00h
00min
00s

Version interactive avec LaTeX compilé

Centrale Option Informatique MP 2016

Notez ce sujet en cliquant sur l'étoile
0.0(0 votes)
Logo centrale
2025_08_29_73ba0d2450d302c69041g

Un algorithme de tri

I Présentation

I.A - Motivation

Trier des données est un problème récurrent dans tous les systèmes d'information. Dans un système travaillant en temps réel (par exemple un système de freinage d'une voiture) ou un système pouvant être soumis à des attaques (par exemple un serveur web), on s'intéresse à la complexité dans le pire des cas.
Or, dans une application réelle, les données que l'on veut trier ne sont pas quelconques mais suivent une certaine distribution aléatoire, qui est loin d'être uniforme : ainsi, il est fréquent que les données soient déjà presque triées. De plus, de nombreux algorithmes de tris (même les plus performants) atteignent leur complexité maximale lorsque les données sont déjà triées.
Le but de ce problème est d'étudier un algorithme de tri, proche du tri par tas mais présentant avec lui quelques différences significatives et notamment des performances intéressantes lorsque les données qu'il reçoit sont presque triées.
Dans tout le problème, on triera, par ordre croissant, des valeurs entières.
Dans toutes les questions de complexité en temps, la mesure de complexité à considérer est le nombre de comparaisons par la relation d'ordre entre entiers.

I.B - Notations et préliminaires

Dans la suite, si un objet mathématique est noté , on notera t l'objet Caml qui l'implante et, si appartient à un ensemble implanté en Caml par le type T , on écrira de manière équivalente ou . Par exemple, pour signifier que désigne une liste d'entiers, on notera : int list.
Étant donné deux fonctions et à valeurs positives, on note :
  • pour exprimer qu'il existe une constante telle que, pour tout suffisamment grand, ;
  • pour exprimer qu'il existe une constante telle que pour, tout suffisamment grand, ;
  • pour exprimer qu'on a et .
On tiendra compte dans la suite que la structure à trier n'est pas un ensemble, car le même élément peut être répété plusieurs fois. Ainsi on sera amené à manipuler en Caml des listes ou des vecteurs dans lesquels un même élément peut apparaitre plusieurs fois, et des arbres dans lesquels la même valeur peut étiqueter plusieurs sommets différents. Dans la suite, lorsqu'il s'agira de déterminer le minimum d'une telle structure, il pourra être atteint plusieurs fois ; de même, lorsqu'on triera ou « réunira » deux telles structures, ce sera toujours en tenant compte des répétitions, c'est-à-dire sans perte d'éléments. Ainsi, par exemple, pour les listes et , le minimum de est 2 , trier consiste à renvoyer la liste et «réunir» et consiste à renvoyer la liste .
De manière générale, lorsqu'on dira que deux structures de données contiennent les mêmes éléments, ce sera toujours en tenant compte des répétitions. Par exemple les listes et contiennent les mêmes éléments mais pas les listes et .

II Algorithme sur des arbres

II.A - Tri par insertion

II.A.1) Écrire la fonction insere : int int list int list insérant un élément dans une liste supposée triée, c'est-à-dire telle que pour toute liste u supposée triée et tout élément x , (insere x u) renvoie une liste v telle que
  • v contient les mêmes éléments que ;
  • v est triée.
    II.A.2) Écrire la fonction tri_insertion : int list int list triant la liste reçue en argument en utilisant la fonction précédente.
    II.A.3) Pour , on note le nombre de comparaisons effectuées par l'appel (tri_insertion l) dans le cas le pire pour une liste 1 de longueur . On note de même le nombre de comparaisons effectuées dans le cas le meilleur.
    Déterminer et .

II.B - Tas binaires

On appelle arbre un arbre binaire étiqueté par des éléments de . Un tel arbre est implanté en Caml à l'aide de la déclaration de type suivante:
type arbre =
    | Vide
    | Noeud of int * arbre * arbre ;;
On définit la hauteur et la taille (appelée aussi nombre d'éléments) d'un arbre , notées respectivement haut(a) et par induction sur la structure de l'arbre:
et haut(Noeud(x,a1,a2))=1+max{haut(a1),haut(a2)}
Vide et Noeud
pour tous arbres binaires a1 et a2 et tout entier x .
x , a1 et a2 sont appelés respectivement la racine, le fils gauche et le fils droit de l'arbre a.
On dit que deux arbres ont mêmes éléments s'ils ont les mêmes ensembles d'étiquettes et que chaque étiquette présente apparait le même nombre de fois dans chacun des arbres.
On dit qu'un arbre binaire est parfait s'il s'agit de l'arbre vide Vide, ou s'il est de la forme Noeud( ) où a1 et a2 sont deux arbres parfaits de même hauteur.
On dit qu'un arbre binaire est un tas binaire parfait (ou simplement un tas parfait) si c'est un arbre parfait et que la valeur étiquetant chaque nœud de l'arbre est inférieure ou égale à celle de ses fils.
On dit qu'un arbre binaire est un quasi-tas si c'est un arbre de la forme Noeud(x,a1,a2) et que a1 et a2 sont des tas binaires parfaits de même taille : aucune contrainte d'ordre n'est donc imposée sur l'étiquette de la racine x . Étant donné un arbre non vide , on note le minimum des éléments qu'il contient.
II.B.1) Pour , on note la taille d'un arbre binaire parfait de hauteur . Déterminer pour tout . On justifiera la réponse en exprimant en fonction de .
II.B.2) Écrire la fonction min_tas: arbre int telle que pour tout tas binaire parfait a non vide, (min_tas a) renvoie . On fera en sorte que la complexité en temps de min_tas soit constante.
II.B.3) Écrire la fonction min_quasi: arbre int tel que pour tout quasi-tas a, (min_quasi a) renvoie en temps constant.
II.B.4) Écrire la fonction percole: arbre -> arbre telle que (percole a) renvoie a si a est l'arbre vide et, si a est un quasi-tas, renvoie un tas binaire parfait contenant les mêmes éléments. Donner la complexité de percole dans le cas le pire, en fonction de la hauteur du quasi-tas a.

II.C - Décomposition parfaite d'un entier

L'algorithme de tri que l'on va étudier repose sur une propriété remarquable des nombres obtenus à la question II.B.1.
Étant donné un entier naturel , on dit qu'un -uplet ( ) d'entiers naturels non-nuls vérifie la propriété QSC (pour «quasi strictement croissant») si l'une des trois conditions suivantes est vérifiée:
;
  • ou et ;
  • ou et .
En particulier, on convient qu'il existe un unique 0 -uplet, noté () et que ce 0 -uplet vérifie la propriété QSC.
La propriété remarquable des nombres qui nous intéressera est alors la suivante :
pour tout entier naturel non nul , il existe un unique entier et un unique -uplet d'entiers naturels non nuls vérifiant la propriété QSC et tel que cette somme étant par convention nulle si .
Une telle écriture où ( ) vérifie la condition QSC est appelée une décomposition parfaite de . Par exemple, les entiers de 1 à 5 admettent les décompositions parfaites suivantes : ; .
On peut remarquer que, du fait de la stricte croissance de la suite d'entiers , un -uplet d'entiers naturels vérifie la propriété QSC si et seulement si le -uplet ( ) vérifie également cette propriété. L'unicité d'une décomposition parfaite ne nous préoccupe pas ici (on l'admettra donc), mais seulement son existence. Plus précisément, l'outil dont nous aurons besoin par la suite est un algorithme récursif d'obtention d'une décomposition parfaite.
II.C.1) Donner la décomposition parfaite des entiers et 101.
II.C.2) Soit un entier naturel admettant une décomposition parfaite de la forme . Montrer qu'alors admet une décomposition parfaite de la forme:
II.C.3) Écrire la fonction decomp_parf : int int list telle que, pour tout entier naturel n, (decomp_parf n) renvoie la liste ( ) des entiers apparaissant dans la décomposition parfaite de n (dans cet ordre). Cette fonction devra avoir une complexité temporelle en .

II.D - Création d'une liste de tas

On appelle liste de tas une liste de couples de la forme ( ) où désigne un arbre binaire parfait et est la taille de l'arbre : il existe donc un entier naturel tel que .
Une liste de tas est implantée en Caml par le type (arbre * int) list.
Étant donnée une liste de tas de la forme précédente, on définit:
  • la longueur de , notée long(h), par
  • la taille de , notée , par
  • la hauteur de , notée haut(h), par
  • le minimum de , noté , par
Comme pour les arbres binaires, on dit que deux listes de tas ont mêmes éléments si les deux listes des arbres les constituant font apparaitre exactement les mêmes étiquettes avec exactement le même nombre d'apparitions de chaque étiquette. De même, une liste d'entiers naturels et une liste de tas constituée d'arbres dont les étiquettes appartiennent à ont mêmes éléments si les deux structures font apparaitre exactement les mêmes éléments avec le même nombre d'apparitions de chaque élément.
On dit qu'une liste de tas vérifie la condition (pour «tas croissants») si le -uplet d'entiers naturels vérifie la propriété QSC . On peut remarquer qu'une liste de tas vérifie la condition TC si et seulement si est une décomposition parfaite de . En particulier, la liste de tas vide vérifie la condition TC ; on constate enfin que toute liste de tas de la forme vérifie la condition TC.

II.D.1)

a) Si est une liste non vide de tas, a-t-on nécessairement haut ? A-t-on nécessairement ? Justifier.
b) Même question si est une liste de tas vérifiant la condition TC.
II.D.2) Considérons un arbre réduit à sa racine (c'est-à-dire un couple ( ) correspondant à un tas binaire parfait) et une liste de tas vérifiant la condition TC. Si l'on ajoute le couple en tête de la liste , on obtient bien une liste de tas , mais qui ne vérifie peut-être plus la condition TC. L'objectif de cette question consiste à concevoir, en utilisant les outils mis en œuvre dans les questions précédentes, un algorithme qui construit une liste de tas ayant les mêmes éléments que telle que vérifie la condition TC.
a) On considère et deux listes de tas vérifiant la condition TC, où les arbres et sont donnés figure 1 .
Figure 1
Expliquer de manière détaillée (à l'aide de représentations graphiques) comment on construit les listes de tas et lors de l'ajout de l'arbre réduit à sa racine d'étiquette 8 dans chacune des listes de tas et .
b) Décrire le plus précisément possible un algorithme qui consiste à construire à partir d'un arbre réduit à sa racine et d'une liste de tas vérifiant la condition TC. On fera en sorte que cet algorithme ait une complexité dans le cas le pire en (où est le premier tas de la liste ) et en dans le cas le meilleur. On justifiera soigneusement la correction de la fonction et brièvement sa complexité dans le cas le pire.
c) Écrire la fonction ajoute: int -> (arbre * int) list -> (arbre * int) list telle que (ajoute ) renvoie la liste de tas vérifiant la condition TC construite par l'algorithme de la question précédente à partir d'un arbre réduit à sa racine x et une liste de tas vérifiant la condition TC .
II.D.3) On définit la fonction suivante, de type int list (arbre * int) list:
let rec constr_liste_tas l = match l with
    | [] -> []
    | x :: r -> ajoute x (constr_liste_tas r)
;;
Il est clair que l'appel (constr_liste_tas l) renvoie une liste de tas vérifiant la condition TC et ayant les mêmes éléments que la liste 1 .
a) Montrer que le coût en temps de l'appel (constr_liste_tas l) pour une liste l: int list déjà triée de longueur dans le cas le pire est en .
b) Montrer que, pour une liste 1 : int list de longueur , (constr_liste_tas 1) a une complexité temporelle en dans le cas le pire.

II.E - Tri des racines

On dit qu'une liste de tas vérifie la condition (pour «racines ordonnées») si les tas présents dans la liste apparaissent par ordre croissant de leurs racines ou, ce qui est équivalent, par ordre croissant de leurs minimums : .
On considère une liste de tas vérifiant la condition TC et ne vérifiant pas nécessairement la condition RO. On veut réarranger les éléments apparaissant dans de façon à obtenir une liste de tas vérifiant à la fois la condition TC et la condition RO. Si l'on trie brutalement par insertion les tas de dans l'ordre croissant des racines, on risque de perdre la condition TC. On va donc mettre en place un tri s'inspirant du tri par insertion mais consistant à échanger les racines des tas présents dans plutôt que les tas eux-mêmes.
II.E.1) Écrire la fonction echange_racines: arbre arbre (arbre * arbre) de complexité constante telle que, si a1 et a2 sont deux arbres binaires non vides, (echange_racines a1 a2) renvoie le couple d'arbres passés en argument en se contentant d'échanger les étiquettes de leurs racines.
II.E.2) On considère une liste de tas non vide vérifiant la condition RO et un quasi-tas de taille . Montrer que:
a) si , alors (percole ):: h est une liste de tas vérifiant la condition RO;
b) et si on pose ( ) = (echange_racines a a1), alors b est un tas binaire parfait, b1 est un quasi-tas et .
II.E.3) On examine maintenant trois exemples de couples ( ) pour lesquels est un quasi-tas et est une liste de tas non vide vérifiant la condition RO. On souhaite à chaque fois faire évoluer la liste de tas jusqu'à obtenir une liste de tas vérifiant la condition RO, en ne s'autorisant pour seules opérations
que d'éventuelles permutations entre des étiquettes d'un même arbre ou entre des étiquettes de deux arbres distincts (aucune modification de la forme ou de la taille d'aucun arbre en jeu n'est autorisée).
Les couples considérés sont notés et , avec et , où les arbres et sont donnés figure 2 .
Figure 2
Pour chacun de ces trois couples, détailler (à l'aide de représentations graphiques) les étapes de la transformation de la liste en une liste de tas vérifiant la condition RO. Chaque étape devra être clairement identifiée comme faisant appel à un procédé précédemment décrit.
II.E.4) Décrire et justifier le plus précisément possible un algorithme qui, à partir d'un quasi-tas , de sa taille et d'une liste de tas , renvoie une liste de tas identique à ( ) :: à permutation près des étiquettes des arbres et tel que si vérifie la condition RO, alors vérifie la condition RO.
Montrer que sa complexité en temps est en si est un tas non vide, que la liste de tas vérifie la condition RO et que .
Montrer que sa complexité en temps est en , haut et .
II.E.5) Écrire la fonction
insere_quasi: arbre -> int -> (arbre * int) list -> (arbre * int) list
telle que (insere_quasi ath) renvoie la liste de tas vérifiant la condition RO construite par l'algorithme de la question précédente à partir d'un quasi-tas a de taille et une liste de tas vérifiant la condition RO.
II.E.6) Écrire la fonction tri_racines: (arbre * int) list -> (arbre * int) list transformant une liste de tas supposée vérifier la condition TC en une liste de tas vérifiant à la fois la condition RO et la condition TC et telle que et aient mêmes éléments.
II.E.7) Montrer que la fonction tri_racines, appliquée à une liste de tas vérifiant la condition TC, a une complexité temporelle en .

II.F - Extraction des éléments d'une liste de tas

On souhaite dans cette sous-partie récupérer une liste d'étiquettes à partir d'une liste de tas vérifiant les propriétés précédentes. Soit une liste de tas non vide vérifiant RO et TC. Pour supprimer de , si et ne sont pas vides, il suffit de construire (insere_quasi (insere_quasi )), où et peuvent se calculer en temps constant.
II.F.1) Montrer que vérifie RO et TC.
II.F.2) Donner la complexité temporelle de l'évaluation de (insere_quasi (insere_quasi )) dans le cas le pire, sous la forme pour une fonction que l'on précisera.
II.F.3) Écrire la fonction extraire: (arbre * int) list int list prenant en argument une liste de tas vérifiant les conditions RO et TC et renvoyant la liste triée des éléments de en utilisant les idées ci-dessus.
II.F.4) Montrer que la fonction extraire, appliquée à une liste de tas vérifiant les conditions RO et TC, a une complexité temporelle en dans le pire des cas.

II.G - Synthèse

II.G.1) Écrire la fonction tri_lisse: int list -> int list qui trie une liste en construisant une liste de tas intermédiaire vérifiant RO et TC avant d'en extraire les éléments.
II.G.2) Montrer que la complexité de cette fonction est en est la longueur de la liste donnée en argument.
II.G.3) Déterminer la complexité temporelle de la fonction tri_lisse dans le cas particulier où la liste passée en argument est déjà triée.

III Implantation dans un tableau

On s'intéressera dans cette partie à la complexité spatiale de certaines fonctions. Par complexité spatiale d'une fonction, on entend ici la quantité de mémoire dont elle a besoin pour s'exécuter, la place utilisée par les données qu'elle reçoit en argument n'étant pas prise en compte. Dit autrement, c'est la quantité de mémoire minimum qui doit rester disponible sur l'ordinateur au moment de l'appel de la fonction pour que son exécution ne provoque pas une erreur de capacité mémoire.
Le but de cette partie est de proposer un algorithme avec la même complexité temporelle que tri_lisse et une meilleure complexité spatiale.
III. - Justifier brièvement que la complexité spatiale de tri_lisse sur une liste de longueur est un .
Au lieu d'utiliser comme précédemment une structure d'arbres persistante, on va utiliser une structure impérative : on représentera des arbres sous forme d'une partie d'un tableau : si est un arbre de taille , on le représente dans les cases de commençant à l'indice comme suit:
  • si est vide, la représentation de ne nécessite aucune place ;
  • si est de la forme , où et sont de tailles respectives et , on met dans la case du tableau, puis on représente dans les cases commençant à l'indice , puis on représente dans le tableau dans les cases commençant à l'indice (cf. figure 3 ).
Figure 3
Au lieu de trier une liste d'entiers, on va trier un tableau d'entiers. Ce tableau servira à la fois à représenter les données initiales et les tas que nous manipulerons. Le tableau sera alors trié par échanges successifs.
Par la suite, tous les arbres que l'on représentera seront ou bien l'arbre vide, ou bien des quasi-tas (qui pourront éventuellement être des tas). On définit le type enregistrement suivant pour représenter l'arbre vide ou un quasi-tas stocké dans un tableau:
type tasbin = {donnees : int vect ; pos : int ; taille : int} ;;
Les champs donnees, pos et taille d'un tel l'enregistrement contiennent respectivement le tableau où sont stockés les éléments, la position de la racine dans le tableau et le nombre d'éléments du quasi-tas (si ce nombre d'éléments est nul, le tableau et la position n'ont aucune importance).
Si le tableau stocké dans le champ donnees de cet enregistrement est le tableau à trier, dont la consommation mémoire n'est pas comptée, la place mémoire prise par un élément de type tasbin est constante.
Par la suite, tous les éléments : tasbin que nous manipulerons partageront le même tableau . donnees sous-jacent, qui sera le tableau à trier.
III.B - Écrire les fonctions fg: tasbin tasbin et fd: tasbin tasbin telles que si a représente un quasi-tas, ( fg a) et ( fd a) retournent respectivement une représentation de son fils-gauche et son fils-droit. Ces fonctions devront avoir une complexité constante.
III. - Écrire les fonctions min_tas_vect et min_quasi_vect, de type tasbin int qui prennent en argument respectivement une représentation d'un tas binaire parfait non vide et une représentation d'un quasi-tas binaire et qui renvoient le minimum de leurs éléments.
III. - Écrire la fonction percole_vect : tasbin unit tel que si a est un quasi-tas, (percole_vect a) échange des éléments dans le tableau a.data de façon que a devienne un tas avec préservation de l'ensemble des étiquettes aux répétitions près. Si a est un tas vide, (percole_vect a) ne fait rien.
Comme précédemment, on s'intéressera à des listes de tas. De plus, tous les éléments de type tasbin list que nous manipulerons seront soit la liste vide soit des listes de la forme ( ) telles que pour tout .pos .taille .pos et que .pos . taille est la longueur du tableau sous-jacent. La place mémoire occupée par une liste de tas est alors linéaire en sa longueur (et non en la longueur du tableau contenant ses éléments). Dans le cas où tous les éléments du tableau sont représentés dans la liste de tas, on aura de plus .pos .
Figure 4
III.E - Étant donné un tableau t , on va construire un h : tasbin en parcourant t de droite à gauche de la façon suivante: on démarre avec h vide et, pour allant de à 0 inclus, on ajoute l'élément situé à l'indice à h , de façon similaire à l'algorithme utilisé pour ajoute, de façon à garantir que la condition TC est préservée. À la fin, tous les éléments de t sont représentés dans h . On trie alors h , puis on extrait successivement le minimum de h .
Écrire la fonction ajoute_vect : int vect int tasbin tasbin analogue à la fonction ajoute précédemment définie et telle que (ajoute_vect d p h) ajoute l'élément d'indice p du tableau d à h. On supposera que est vide ou que la position du premier tas de est .
On définit alors la fonction constr_liste_tas_vect : int vect -> tasbin list, analogue à constr_liste_tas, comme suit :
(* constr_liste_tas_aux : int vect -> int -> tasbin -> tasbin
        ajoute les elements du tableau d d'indice i,
        pour i<p, a la liste de h.
        Precondition : h est vide vide ou la position du premier tas dans h
        est p. *)
let rec constr_liste_tas_aux d p h =
    if p = 0 then h
    else
        let h' = ajoute_vect d (p-1) h in
        constr_liste_tas_aux d (p-1) h
;;
let constr_liste_tas_vect d = constr_liste_tas d (vect_length d) [];;
III.F - Écrire la fonction echange_racines : tasbin -> tasbin -> unit échangeant les racines des tas qui lui sont donnés en argument.
III. - Écrire la fonction insere_quasi_vect : tasbin tasbin list unit analogue de la fonction insert_quasi.
III. - Écrire la fonction tri_racines_vect : tasbin -> tasbin list -> unit analogue de la fonction tri_racines.
III.I - Écrire la fonction extraire_vect : tasbin list unit analogue de la fonction extraire.
III. - Écrire la fonction tri_lisse_vect : int vect -> unit triant un tableau en utilisant les fonctions précédentes.
III. - Quelle est la complexité temporelle de tri_lisse_vect dans le cas le pire?
III.L - Quelle est la complexité temporelle de tri_lisse_vect pour un tableau déjà trié ?
III. - Quelle est la complexité spatiale de tri_lisse_vect dans le cas le pire?

  1. On peut en fait démontrer que cette complexité est un mais cela n'est pas demandé.
Centrale Option Informatique MP 2016 - Version Web LaTeX | WikiPrépa | WikiPrépa