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