tp4-arbres-binaires-tas

publicité
T.P. 4 : des arbres binaires variés et leurs applications
1
Arbres binaires de recherche
1.1
Ce qu’on doit déjà savoir sur la recherche d’un mot dans une liste
triée et après ...
Supposons par exemple qu’on veuille constituer et manipuler un lexique anglais/français. Un
tel lexique sera représenté en Python par une liste de couples de mots, comme par exemple :
lexique = [(’blue’,’bleu’),(’green’,’vert’),(’red’,’rouge’),(’yellow’,’jaune’)]
On suppose que les mots anglais sont rangés dans l’ordre du dictionnaire (comme dans l’exemple
précédent). Cet ordre est l’ordre implanté en python sur les chaines de caractères avec le symbole <.
Notons aussi que pour les couples, python commence par comparer les premières entrées.
On suppose donc qu’on dispose d’une variable globale lexique comme ci-dessus.
a) Sachant que la liste des mots anglais est triée dans l’ordre du dictionnaire, écrire une fonction
traduit qui reçoit un mot anglais en argument (qui est supposé être dans le lexique) et
renvoie sa traduction avec une complexité en O(log(n)) où n est la longueur du lexique.
N.B. La complexité logarithmique demande une méthode de dichotomie.
b) Ecriture une fonction insere qui reçoit un argument qui est un couple (motanglais,
motfrancais) qui permet de rajouter ce couple à la bonne place dans la variable globale
lexique : cette fonction sera de complexité linéaire.
Le problème : la représentation de notre lexique sous cette forme fait que la fonction insere a
une complexité en O(n). Nous allons maintenant présenter une nouvelle façon de coder le lexique,
une autre structure de donnée, où l’insertion, comme la recherche, d’un mot dans le lexique, sera
en O(log(n)).
1.2
Présentation des arbres binaires de recherche
Définition mathématique 1 : un arbre sera pour nous un ensemble de points appelés noeuds
muni d’une relation pour laquelle chaque noeud a un père et un seul (on représente la relation
père fils par une flèche), sauf un noeud qui n’a pas de père qu’on appelle la racine. On convient de
représenter la racine en haut et les pères en dessus de leurs fils.
Si le noeud p est le père du noeud f on dit aussi que f est un fils de p. Les noeuds qui n’ont
pas de fils sont appelés feuilles.
Définition mathématique 2 : un arbre au sens précédent sera dit arbre binaire si chaque père
a au plus deux fils, appelés alors fils droit et fils gauche.
Lien avec notre problème : Chaque noeud sera un élément de notre ensemble de couples
(motanglais,motfrancais). La relation père/fils entre les mots sera définie au niveau des parties
motanglais (la partie motanglais du couple est ce qu’on appelle la clé d’enregistrement).
1
Par exemple, avec un lexique à quatre mots :
ici la racine sera le couple dont blue est la clé, avec deux fils de clés red et yellow et red a un fils
green
Définition 3 : définition informatique d’un arbre binaire, avec son implantation python : un arbre binaire est une structure de donnée qui peut être définie récursivement comme
suit : un arbre binaire est :
— soit vide, codé comme une liste vide [],
— soit codé comme une liste [racine, filsGauche, filsDroit] où racine est un couple
(motanglais,motfrancais), et filsDroits et filsGauche sont deux arbres binaires.
Exemple : Avec Arbrelexique=[(’blue’,’bleu’),filsGauche,filsDroit] où
filsGauche=[(’red’,’rouge’),[],[(’green’,’vert’),[],[]]] et
filsDroit=[(’yellow’,’jaune’),[],[]], on aura le codage informatique de l’arbre binaire
dessiné ci-dessus.
Définition 4 : arbre binaire de recherche : un arbre [racine, filsGauche, filsDroit] au
sens de la définition 3 est un arbre binaire de recherche si, et seulement si,
— la clé de racine est strictement supérieure à la clé de tous les noeuds de filsGauche,
— la clé de racine est strictement inférieure à la clé de tous les noeuds de filsDroit,
— filsDroit et filsGauche sont des arbres binaires de recherche.
L’exemple précédent ne donne pas un arbre binaire de recherche mais la version modifiée suivante oui :
1.3
Implantation en python de la fabrication récursive des arbres binaires de recherche... et recherche !
On considère donc des arbres binaires de recherche comme définis au paragraphe précédents,
dont les noeuds sont des couples (motanglais,motfrancais) que l’on compare pour l’ordre lexicographique des mots anglais.
a) Ecrire trois fonctions filsGauche, filsDroit, racine qui prennent en argument un arbre
binaire de recherche arbre et renvoie respectivement son fils Gauche, son fils Droit, et sa
racine.
2
b) On veut écrire alors une fonction récursive ajout(element, arbre) qui prend comme argument un élément i.e. un couple (motanglais,motfrancais) et un arbre binaire (qui au
départ peut être vide, ce sera notre cas de base) et qui retourne un nouvel arbre binaire de
recherche où element est incorporé à arbre.
N.B. on doit comparer les mots anglais.
Pour cela, le principe est le suivant :
● si le mot anglais de l’élément à rajouter est plus grand que le mot anglais à la racine de
l’arbre, on construit un arbre binaire en gardant la même racine, le même fils gauche, et on
se ramène au problème de rajouter l’élément au fils droit.
● si le mot anglais de l’élément à rajouter est plus petit que le mot anglais à la racine de
l’arbre ... à vous de deviner !
Ecrire la fonction python correspondante ! Par commodité pour la suite, on considérera aussi
le cas où le mot qu’on rajoute est déjà présent dans l’arbre : dans ce cas bien sûr l’arbre ne
devra pas être modifié.
c) Appliquer la fonction précédente pour fabriquer un arbre binaire de recherche pour notre
lexique en ajoutant successivement les éléments de la liste suivante :
lexique = [(’red’,’rouge’),(’blue’,’bleu’),(’yellow’,’jaune’),(’green’,’vert’)]
d) Ecrire enfin une fonction récursive traduit2(mot,arbre) qui renvoie la traduction d’un
mot anglais qui est stocké dans un arbre binaire de recherche comme précédemment.
(On comparera mot à la (partie anglaise de la) racine de l’arbre, et si ils sont différents,
ensuite récursivement soit à filsDroit soit à filsGauche..)
1.4
Complexités : arbres équilibrés
a) Définition : la hauteur (on dit aussi profondeur) d’un noeud de l’arbre est le nombre de
générations qui le séparent de la racine de l’arbre. Cette hauteur admet naturellement une
définition récursive rigoureuse (laquelle ?)
En déduire une fonction récursive hauteur(element, arbre) qui renvoie la hauteur du
noeud element supposé présent dans l’arbre arbre.
b) Par définition la hauteur de l’arbre est la maximum des hauteurs des éléments. On dira
qu’un arbre ayant n noeuds est équilibré ssi sa hauteur est minimale parmi tous les arbres
ayant n noeuds. Donner en l’expliquant, une relation entre la hauteur h d’un arbre équilibré
et son nombre total de noeuds n.
Le dessin suivant, qui est un cas particulier devrait suffire pour comprendre !
N.B. Pour les questions qui suivent, on admet qu’on peut toujours ranger nos données dans
un arbre binaire de recherche équilibré 1 .
c) Justifiez que la fonction traduit2 du paragraphe précédent, appliquée à un arbre équilibré,
est de complexité O(log(n)) où n est le nombre de noeuds de l’arbre.
d) La fonction ajout du paragraphe précédent fabrique-t-elle forcément des graphes équilibrés ?
e) Montrer en tous cas que si on applique cette fonction ajout a un arbre équilibré ayant n
noeuds, la complexité de l’ajout est en O(log(n)).
1. en fait il existe bien des algorithmes pour s’y ramener, en faisant des rotations
3
Moralité : par rapport à ce qu’on a dit au tout premier paragraphe, les arbres binaires de
recherche équilibrés ont l’avantage qu’aussi bien la recherche que l’ajout d’une donnée sont en
O(log(n)).
2
Une classe python fabriquée suivant les principes du § 1 :
les dictionnaires python
La classe dictionary n’est pas au programme, dans une épreuve d’écrit on devrait vous la présenter !
a) Au § 1, on a expliqué (modulo le problème d’équilibrage des arbres !) comment fabriquer une
structure qui permet de gérer un dictionnaire de n mots avec une complexité en O(log(n)) pour la
recherche et l’ajout d’un mot. En python, il existe une structure qui fait exactement cela, la classe
dict.
Par exemple dico={’red’: ’rouge’, ’blue’: ’bleu’, ’yellow’ : ’jaune’} sera un dictionnaire avec trois entrées, les mots en anglais ici seront les clés qui servent à accéder aux valeurs
qu’elles référencent (ici les mots en français).
Voici quelques commandes de cette classe :
a={} # création d’un dictionnaire vide appelé a
a[’truc’]=12 # création d’une entrée du dictionnaire avec la valeur 12 et à la clé ’truc’
# eh oui c’est exactement ce qu’on ne peut pas faire avec les listes.. pas de out of range ici
a[’truc’] # va renvoyer la valeur 12.
dico[’red’] # va renvoyer ’rouge’
’yellow’ in dico # va renvoyer True
’black’ in dico # va renvoyer False
Question (exemple) écrire une fonction freq qui prend en paramètre une chaı̂ne de caractères comme ’CABBAA’ et renvoie un dictionnaire où les lettres sont les clés et le nombre d’occurrence de chaque lettre est la valeur stockée pour chaque clés. Ainsi freq(’CABBAA’) renverra
{’A’: 3, ’B’: 2, ’C’: 1}. Noter qu’il n’y a pas d’ordre défini entre les clés d’un dictionnaire.
b) Quelque précision sur le parcours des dictionnaires :
(i) Avec dico comme ci-dessus, que donne le code :
for a in dico:
print(a)
(ii) Comment faire alors pour voir à la fois les clés et les valeurs qu’elles référencent dans
le dictionnaire dico ? Créer une fonction ListeCouple qui prend en argument un dictionnaire et
renvoie une liste des couples (clé,valeur). Par exemple ListeCouple(dico) renverra (à l’ordre
près) :
[(’blue’, ’bleu’), (’red’, ’rouge’), (’yellow’, ’jaune’)]
3
Tri avec la construction d’un arbre binaire de recherche
On a vu au § 1.3 comment on pouvait transformer une liste en arbre binaire de recherche.
Explicitement, à l’aide de la fonction ajout définie dans ce paragraphe, écrire une fonction ABR qui
fabrique un tel arbre à partir d’une liste. Ainsi pour L=[34,2,667,1,4,20], ABR(L) donnera :
[34, [2, [1, [], []], [4, [], [20, [], []]]], [667, [], []]]
Expliquer ce que fait alors la fonction suivante, appliquée la variable arbre ci-dessus, et à la
liste vide t=[] :
def parcoursProfondeur(arbre,t):
if arbre!=[]:
t=parcoursProfondeur(filsGauche(arbre),t)
4
t.append(racine(arbre))
t=parcoursProfondeur(filsDroit(arbre),t)
return t
En déduire une méthode pour trier une liste (par exemple une liste de nombres) qui commence
par transformer cette liste en arbre binaire de recherche.
4
Les tas et le tri par tas
4.1
La structure de tas : encore un arbre binaire
Définition : un tas binaire, ici on dira simplement un tas (en anglais heap) est un arbre binaire
(comme au § 1) qui est ordonné de sorte que la clé d’un noeud est toujours supérieure à la clé de
ses fils (de sorte que son plus grand élément est toujours la racine de l’arbre) 2 .
Du point de vue informatique, on va ici coder simplement ces arbres par un tableau (liste
python) de nombres.
Par exemple T=[9,5,6,2,1,5,1,0] codera l’arbre cicontre. Cet arbre est bien un tas, puisque chaque père a
une valeur supérieur à celles de ses fils.
L’intérêt des tas pour les tris : Au § 3, on a expliqué comment obtenir un algorithme de tri
en transformant une liste en arbre binaire de recherche. Ici c’est bien plus immédiat avec les tas
puisque si on sait ranger les données en un tas, à chaque fois la première entrée donnera le max.
de la liste.
4.2
Organisation en tas
But de cette partie : prendre une liste quelconque et la réorganiser en un tas.
On va donc gérer les tas à l’aide de listes pythons : chaque liste python sera interprétée mentalement par nous comme associée à un arbre binaire (qui n’est pas forcément un tas). Par exemple
pour T=[5,2,6,0,1,9,1,5]
2. En fait, il s’agit de tas-max, en remplaçant supérieur par inférieur, on a la notion de tas-min.
5
Ainsi, on ne manipule que des listes simples, la structure d’arbre n’apparaı̂t pas dans le codage,
mais il faut comprendre que pour chaque i où cela a un sens T[i] a pour fils T[2i+1] et T[2i+2].
A l’inverse le père de T[i] est T[(i-1)//2].
Par commodité, on notera pere(i) pour (i-1)//2, gauche(i)=2*i+1 et droite(i)=2i+2.
a) Ecrire une fonction estUnTas qui reçoit une liste T et renvoie True ou False ssi T représente
un tas.
b) Avec les hypothèses 0 ≤ i ≤ limite et limite≤ longueur(T), écrire un fonction maximum(T,i,limite)
qui retourne le plus petit entier iMax vérifiant toutes les conditions suivantes :
iMax<limite et iMax∈ {i,gauche(i),droite(i)}
T[iMax] ≥ T[i]
gauche(i)<limite ⇒ T[iMax] ≥ T[gauche(i)]
droite(i)<limite ⇒ T[iMax] ≥ T[droite(i)]
En d’autres termes maximum(T,i,limite) retourne l’indice (inférieur à limite) de la plus
grande des trois valeurs T[i], T[gauche(i)], T[droite(i)]. En cas de valeurs égales, le
plus petit indice est retourné.
Par exemple, avec tableau T ci-dessus, max(T,0,8)=2 puisque T[0]=5, T[1]=2, et T[2]=6,
donc la valeur maximum est atteinte pour i=2.
c) Soit la fonction récursive Python suivante :
avec :
def entasserRecursif(T,i,limite):
iMax=maximum(T,i,limite)
if iMax!=i:
echange(T,i,iMax)
entasserRecursif(T,iMax,limite)
def echange(T,i,j):
aux=T[i]
T[i]=T[j]
T[j]=aux
On fait l’appel de entasserRecursif(T,0,8) où T est la liste représentant l’arbre suivant :
Dessiner l’arbre représentant T après cet appel
de entasserRécursif(T,0,8)
d) L’algorithme entasserRecursif(T,i,limite) échange des valeurs du tableau de haut en
bas, en suivant une branche de l’arborescence. Cela a pour effet de faire descendre des petites
valeurs, et de faire monter les grandes valeurs. Il est donc possible de construire un tas, en
itérant cet algorithme sur les indices décroissants du tableau.
Construire ainsi une fonction construireTas(T) qui transforme un tableau T en tas.
e) En déduire un algorithme de tri, qui prend une liste et la trie (par ordre décroissant), grâce
à la structure de tas.
6
Téléchargement