Algorithmie – TD 3 Chercher et Trouver 1 Méthodes de Recherche

Transcription

Algorithmie – TD 3 Chercher et Trouver 1 Méthodes de Recherche
Algorithmie – TD 3
Chercher et Trouver
1
Méthodes de Recherche
Nous allons chercher à savoir si un élément particulier se trouve dans un
ensemble d’éléments. On verra que la performance de la recherche dépend
de la structure utilisée pour stocker les éléments. Il faut donc s’assurer que
la structure est conservée lorsque l’on retire ou ajoute un élément.
1.1
Recherche séquentielle
Ici, la structure pour stocker les éléments est une liste. Tout nouvel élément
est placé en queue de liste. Quelle est la complexité de recherche d’un élément
dans une telle structure ? Et en moyenne ?
Solution :
Ici, pour trouver un élément, il faut parcourir la liste élément après
élément. Ainsi, en cas de recherche infructueuse, il faut comparer tous les
éléments de la liste à l’élément recherché : on a donc besoin de N comparaisons.
Dans le cas d’une recherche fructueuse, l’élément recherché peut être de
façon équiprobable dans toutes les cases de la liste (il a 1/N chance d’être en
position i, pour tout 1 ≤ i ≤ N ).
Ainsi, s’il est en position i, il y aura juste
à effectuer.
P i comparaisons
N +1
1
L’espérance du nombre d’opération est alors 1≤i≤N ( N i) = 2 1.2
Recherche dichotomique
Ici, notre structure est une liste triée. À chaque nouvel élément, on re-trie
la liste. Quel est la complexité d’un ajout ou d’une suppression d’élément ?
Quel est le coût d’une recherche ?
Solution :
1
Il faut retrier à chaque fois la liste. Comme on ne rajoute ou on ne
supprime qu’un élément, ceci revient à trouver où l’élément doit être placé
ou supprimer, puis à décaler tous les autres éléments.
De là, le fait de décaler les éléments revient – au pire – à tous les décaler et
donc amène à une complexité de N. En moyenne, comme l’élément à déplacer
peut être de façon équiprobable à toutes les positions de la liste, et comme
si le premier élément à décaler est
Pen position i, il faut en décaler N − i, le
nombre moyen de décalage est : 1≤i≤N ( N1 (N − i)) = N2+1 .
De façon dichotomique, on coupe successivement la liste en 2 et on choisit
où regarder (puisque la liste est triée, on sait a priori dans quelle partie de
la liste est l’élément recherché s’il est présent).
On a donc besoin de log2 (N) opération. En effet, à chaque étape on coupe
la liste en 2 jusqu’à trouver le bon élément.
En effet, s’il y a 2 éléments une étape suffit, s’il y a au plus 4 éléments, 2
étapes suffisent, et ainsi de suite. Par là, trouver un élément parmi 2k peut
se faire en k étapes.
1.3
Arbres binaires de recherche
On utilise ici des arbres binaires de recherche stocker nos données. Un arbre
binaire de recherche est un arbre tel que pour tout nœud, son sous arbre
gauche ne contienne que des valeurs strictement plus petites que sa propre
valeur, et que son sous-arbre droit ne contienne que des valeurs plus grande
ou égale à sa valeur.
Proposez des algorithmes (avec complexités maximales et moyennes) pour :
• création de la structure,
• recherche d’un élément,
• insertion d’un élément (qu’il soit vide ou pas),
• suppression d’un élément.
Solution :
La figure 1 montre un Arbre binaire de recherche.
Nous ne détaillerons pas ici les algorithmes d’insertion et de suppression
mais leur complexité est la même :
2
5
1
20
9
3
15
14
19
12
Figure 1: Un arbre binaire de recherche
• complexité maximale O(n),
• complexité moyenne O(log(n)).
Effecuter une recherche se fait depuis la racine et on parcours les nœuds
(à gauche si la valeur à chercher est inférieure à la valeur du nœud, à droite
sinon) jusqu’à arriver à la solution. L’insertion peut se faire selon le même
principe, on parcours l’arbre pour rechercher la valeur et on rajoute la valeur
à la dernière feuille visitée. Ainsi, si on veut rajouter la valeur 8 à l’arbre de
la figure 1, on parcours l’arbre de la façon suivante 5 ← 20 ← 9 et on insère
8 à gauche de 9.
Pour la suppression, c’est un petit peu plus technique. Certains nœuds
sont faciles à supprimer : ceux qui n’ont qu’au plus 1 fils. Pour ceux qui
ont deux fils (comme la racine), il suffit de prendre la valeur qui lui est
strictement supérieure (ici 9). Ce nœud ne peut posséder qu’un seul fils à
droite (car entre la racine et sa valeur strictmeent supérieure il n’y a personne
dans l’arbre). On peut alors échanger la valeur de la racine avec le nœud
trouvé (ici 9) et de le supprimer. Le résultat est présenté en figure 2
La hauteur d’un arbre de recherche peut être égale au nombre de données
si l’arbre est mal construit (des données triées par exemple), ainsi, au maximum la recherche, la suppression ou l’insertion d’une donnée peut prendre
un temps en O(n).
Le temps moyen est donc égal à 1/N fois la longeur totale interne (la
somme de tous les chemins des nœuds qui ne sont pas des feuilles à la racine)
moyenne d’un arbre binaire de recherche.
3
9
1
20
3
15
14
19
12
Figure 2: Un arbre binaire de recherche
La longeur totale moyenne d’un arbre binaire de recherche peut être
trouvée à partir de la formule de récurrence suivante :
X
(C(i − 1) + C(N − i))
C(N ) = N − 1 + 1/N
1≤i≤N
Et C(1)=1.
Le premier terme tient compte du fait que la racine contribue de 1 à la
longueur des chemins des N-1 autres éléments, et le deuxième terme précise
la longueur des sous-arbres à gauche et à droite de la racine. La racine à en
effet 1/N chance d’être la i ième plus petite valeurs et donc que son arbre
gauche contienne i éléments et son arbre droit N-i éléments.
Cette récurrence est presque identique à celle trouvée en PC2 pour le tri
rapide et peut être résolue de la même manière. On a alors C(N) =O(n log(n)).
On peut prouver que, de même que pour l’insertion ou la suppression, ce
temps moyen est en O(log(n)). Cette méthode est donc plus efficace que la
recherche dichotomique et séquentielle. 2
Tas
Un tas (ou tas-max) est un arbre binaire tel que la valeur d’un nœud est
plus grande de les valeurs de ses nœuds fils (il existe aussi des tas-min) Cette
définition implique en particulier que la clef de la racine soit la plus grande.
Nous allons nous intéresser ici à l’implémentation la plus concise possible
d’un tas.
4
2.1
Algorithmes
Proposez des algorithmes (et donnez leurs complexités) permettant de :
• créer un tas,
• insérer un élément dans un tas (qu’il soit vide ou pas),
• changer la valeur d’un élément,
• supprimer l’élément le plus grand d’un tas.
Solution :
Algorithmes classiques récursifs. Pour insérer un élément dans un tas
vide on place sa valeur. Pour un tas non vide on compare la valeur du nœud
à la valeur à insérer. Si la valeur à insérer est strictement pus grande que
celle du nœud, on échange la valeur du nœud et la valeur à insérer et on
recommence avec un des fils de la racine. Pour la suppression, on compare
les valeurs des deux fils de la racine et on recopie la valeur la plus grande
dans la racine. On supprime ensuite ce noeud. 2.2
Tas – Tableaux
Une liste l de n éléments est un tas si pour 1 ≤ i ≤ n (les −1 viennent du
fait que les indices d’une liste commencent à 0) :
l[i − 1] ≥ max{t[(2 · i) − 1], t[(2 · i + 1) − 1]
Montrer que cette définition est équivalente à l’“original”. Adaptez les
algorithmes de la section précédente à cette nouvelle définition.
Solution :
La figure 3 montre 3 arbres binaires. L’arbre (c) est particulier, tous les
nœuds ont deux fils à part peut-être ceux du dernier niveau, et le dernier
niveau est rassemblé à gauche. On dira que ce type d’arbre binaire est
complet.
Par la suite tas et tas complet seront considérés comme équivalents.
On montre que l’on peut toujours transformer un tas pour qu’il soit complet par récurrence. On suppose la propriété vrai pour n éléments (c’est vrai
si n = 1) et soit T un tas à n + 1 éléments. On considère le nœud de plus
petite valeur. Ce nœud n’a pas de fils on peut donc le supprimer de T en
5
(a)
(b)
(c)
Figure 3: Trois arbres binaires
conservant la structure de tas. On se retrouve avec un tas T 0 à n éléments
qui satisfait l’hypothèse de récurrence. Une fois T 0 transformé, on rajoute le
plus petit élément au dernier niveau et le plus à gauche possible, ce qui est
faisable puisque cet élément est de plus petite valeur.
Au niveau 1 de l’arbre se trouve la racine et au niveau 2 ses deux fils. Les
deux fils ayant chacun 2 fils, il y en a au maximum 4, etc. Un arbre binaire
complet a donc 2i−1 éléments par niveau, à part pour le dernier niveau.
1
2
3
4
8
5
9
10
6
7
11
Figure 4: Code d’un tas
La figure 4 montre un tel code en action. Il est alors facile de se convaincre
que si le numéro d’un nœud est i, son père est en i/2 (on prend la partie
entière si i est impair).
De là, on voit aussi que les fils du nœud i sont en 2i et en 2i + 1.
Ce code est particulier au tas. En effet, si les niveaux intermédiaires ne
sont pas remplis et si les nœuds du dernier niveau ne sont pas à gauche, ça
6
ne marchera pas. On pourra essayer sur les arbres (a) et (b) de la figure 3
pour s’en convaincre.
On commence par ajouter un élément au tas à la fin de la liste. Si son
père est plus petit que lui, on échange. Si après l’échange le nouveau père est
plus petit on échange encore, et ainsi de suite jusqu’à la racine. Ces échanges
à répétitions assurent le fait qu’à la fin la structure maximier est respectée.
Il faut cependant faire attention à ne pas dépasser la racine (une sentinelle
est placée).
L’algorithme 1 explicite cette méthode.
Algorithme 1 Insertion d’un élément dans un tas
Données
tab (les indices vont de 0 à n - 1)
obj (l’objet à rajouter)
Début
ajouter obj à tab (tab[n] est maintenant égal à obj)
k←n
n ← n+1
Tant que k>0 et tab[k] > tab[(k + 1) / 2 - 1] :
(Attention : dèpart à l’indice 0)
tab[k], tab[(k + 1) / 2 - 1] ← tab[(k + 1) / 2 - 1], tab[k]
k ← (k + 1) / 2 - 1
Fin
Codage du remplacement de la valeur de la racine.
Cela se fait de la façon opposée à l’insertion. On commence par remplacer
la valeur de la racine par la nouvelle valeur. Si la valeur de la nouvelle racine
est plus petite que la plus grande valeur de ses fils, on échange les valeurs.
Si le problème persiste, on continue les échanges de la même manière. Ces
échanges successifs assurent le fait à la fin de la procédure, la structure de
tas est respectée.
L’algorithme 2 explicite cette méthode.
Enfin, codez une façon de supprimer la racine d’un tas.
Solution :
La suppression d’un élément peut se faire très simplement. Il suffit remplacer (en utilisant l’algorithme 2) la valeur de la racine par la valeur du
7
Algorithme 2 Remplacement de la racine dans un tas
Données
tab (les indices vont de 0 à n-1)
n
obj (nouvelle valeur de la racine)
Début
tab[0] ← obj
i←0
k ← -1
Tant que k 6= i:
k←i
filsG ← 2(i + 1) - 1
filsD ← 2(i + 1)
si filsG < n et tab[filsG] > tab[k]:
k ← filsG
si filsD < n et tab[filsD] > tab[k]:
k ← filsD
si k 6= i:
t[i], t[k] ← t[k], t[i]
i←k
k ← -1
Fin
8
dernier élément de la liste.
2.3
Tri par Tas
Proposer une méthode pour trier une liste en utilisant un tas. Quelle est la
complexité de cet algorithme ?
Solution :
On crée un tas vide que l’on remplit avec les n valeurs de la liste (O(n log n)).
On supprime la racine du tas (n fois) que l’on place à la fin de la liste (encore
en O(n log n)). 9