Piles, files et récursivité 1 Piles et Files 1.1 Piles Une pile est une structure de données de type LIFO (last in first out) : le dernier entré est le premier sorti. Elles peuvent par exemple être utilisées dans des algorithmes d évaluation d expressions mathématiques. Une pile P est entièrement définie par deux opérations (figure 1) : 1. empiler une valeur : c est l insérer au début de P, 2. dépiler une valeur, si P est non vide, c est sélectionner la première valeur de la pile et la supprimer de P A cela, il faut ajouter une manière de créer une pile vide, et de tester si la pile est vide. La première valeur de P est le sommet de la pile. Figure 1 Définition d une pile 1.2 Files Une file est une structure de données de type FIFO (first in first out) : le premier entré est le premier sorti. Elles peuvent par exemple être utilisées afin de mémoriser des données en attente de 1
traitement. Une file F est entièrement définie par deux opérations (figure 2) : 1. entrer une valeur c est l ajouter à la fin de de F, 2. sortir une valeur, si F est non vide, c est sélectionner la première valeur de F et la supprimer de F A cela il faut ajouter une manière de créer une file vide, et de tester si la file est vide. La première valeur de la file F est la tête de la file et la dernière est la queue de la file, Figure 2 Définition d une file 2 Réalisation en Python 2.1 En natif Le type liste intègre déjà toutes les méthodes pour réaliser ces deux structures de données : list.append(x) qui ajoute un élément à la fin de la liste ist.insert(i, x) qui insère un élément à la position indiquée. Le premier argument est la position de l élément courant avant lequel l insertion doit s effectuer, donc a.insert(0, x) insère l élément en tête de la liste, et a.insert(len(a), x) est équivalent à a.append(x). list.remove(x) qui supprime de la liste le premier élément dont la valeur est x. Une exception est levée s il existe aucun élément avec cette valeur. list.pop([i]), qui enlève de la liste l élément situé à la position indiquée, et le retourne. Si aucune position n est indiqué, a.pop() enlève et retourne le dernier élément de la liste Ainsi, pour utiliser une pile, on peut écrire le code 1 m a p i l e = [ 3, 4, 5 ] m a p i l e. append ( 6 ) m a p i l e. append ( 7 ) m a p i l e. pop ( ) m a p i l e. pop ( ) Listing 1 Pile utilisant une liste De même, on peut utiliser les listes pour réaliser une file. Cependant, les listes ne sont pas très efficaces dans ce cas précis ((first in first out) : alors que les ajouts et suppressions en fin de liste sont rapides, les opérations d insertions ou de retraits en début de liste sont lentes (car tous les autres éléments doivent être décalés d une position). Pour implémenter une file, il est plus judicieux d utiliser la classe collections.deque qui a été conçue pour fournir des opérations d ajouts et de retraits rapides aux deux extrémités. Le code 2 donne un aperçu de la création et de l utilisation de la classe. 2
from c o l l e c t i o n s import deque queue = deque ( [ " O b j e t 1 ", " O b j e t 2 ", " O b j e t 3 " ] ) queue. append ( " O b j e t 4 " ) queue. append ( " O b j e t 5 " ) queue. p o p l e f t ( ) queue. p o p l e f t ( ) Listing 2 File utilisant la classe collections.deque 2.2 Réalisation d une pile Le code 3 présente une implémentation possible d une pile en Python. c l a s s MaPile ( object ) : def init ( s e l f, m a x p i l e=none ) : s e l f. p i l e = [ ] s e l f. m a x p i l e = m a x p i l e #F o n c t i o n Empile def e m p i l e ( s e l f, element, i d x=none ) : i f ( s e l f. m a x p i l e!=none ) and ( l e n ( s e l f. p i l e )==s e l f. m a x p i l e ) : r a i s e V a l u e E r r o r ( " e r r e u r : P i l e P l e i n e " ) i f i d x==none : i d x=l e n ( s e l f. p i l e ) s e l f. p i l e. i n s e r t ( idx, e l e m e n t ) #F o n c t i o n D e p i l e def d e p i l e ( s e l f, i d x = 1) : i f l e n ( s e l f. p i l e ) ==0: r a i s e V a l u e E r r o r ( " e r r e u r : P i l e V i d e " ) i f idx< l e n ( s e l f. p i l e ) or idx>=l e n ( s e l f. p i l e ) : r a i s e V a l u e E r r o r ( " e r r e u r : l e l e m e n t n e x i s t e p a s " ) return s e l f. p i l e. pop ( i d x ) #L e c t u r e d un é l é m e n t de l a p i l e d i n d i c e donné def e l e m e n t ( s e l f, i d x = 1) : i f idx< l e n ( s e l f. p i l e ) or idx>=l e n ( s e l f. p i l e ) : r a i s e V a l u e E r r o r ( " e r r e u r : e l e m e n t d e p i l e n e x i s t e p a s " ) return s e l f. p i l e [ i d x ] #E x t r a c t i o n d é l é m e n t s e n t r e deux i n d i c e s donnés ( y c o m p r i s c o p i e t o t a l e ) def c o p i e p i l e ( s e l f, imin =0, imax=none ) : i f imax==none : imax=l e n ( s e l f. p i l e ) i f imin <0 or imax>l e n ( s e l f. p i l e ) or imin>=imax : r a i s e V a l u e E r r o r ( " e r r e u r : m a u v a i s i n d i c e ( s ) p o u r l e x t r a c t i o n " ) return l i s t ( s e l f. p i l e [ imin : imax ] ) #t e s t d une p i l e v i d e def p i l e v i d e ( s e l f ) : return l e n ( s e l f. p i l e )==0 #t e s t d une p i l e p l e i n e def p i l e p l e i n e ( s e l f ) : return s e l f. m a x p i l e!=none and l e n ( s e l f. p i l e )==s e l f. m a x p i l e #Retourne l a t a i l l e de l a p i l e def t a i l l e ( s e l f ) : return l e n ( s e l f. p i l e ) ######################################################################## # e x e m p l e s d u t i l i s a t i o n ######################################################################## mapile=mapile ( ) p r i n t m a p i l e. p i l e v i d e ( ) # a f f i c h e True m a p i l e. e m p i l e ( A ) # Empile l e c a r a c t è r e A m a p i l e. e m p i l e ( 5 ) # Empile l e c h i f f r e 5 m a p i l e. e m p i l e ( [ m o t 1, m o t 2, m o t 3 ] ) # Empile d e s mots p r i n t m a p i l e. c o p i e p i l e ( ) # A f f i c h e [ [ mot1, mot2, mot3 ], 5, A ] v a l=m a p i l e. d e p i l e ( ) p r i n t v a l # A f f i c h e A p r i n t m a p i l e. c o p i e p i l e ( ) # A f f i c h e [ [ mot1, mot2, mot3 ], 5 ] p r i n t m a p i l e. t a i l l e ( ) # A f f i c h e 2 p r i n t mapile. element ( ) # A f f i c h e 5 Listing 3 création et manipulation d une pile 3
2.3 Réalisation d une file Le code 4 présente une implémentation possible d une file en Python. c l a s s MaFile ( object ) : def init ( s e l f, m a x f i l e=none ) : s e l f. f i l e = [ ] s e l f. m a x f i l e = m a x f i l e #I n s e r t i o n d un é l é m e n t def e n t r e r ( s e l f, element, i d x =0) : i f ( s e l f. m a x f i l e!=none ) and ( l e n ( s e l f. f i l e )==s e l f. m a x f i l e ) : r a i s e V a l u e E r r o r ( " e r r e u r : F i l e p l e i n e " ) s e l f. f i l e. i n s e r t ( idx, e l e m e n t ) #S o r t i e d un élément def s o r t i r ( s e l f, i d x = 1) : i f l e n ( s e l f. f i l e ) ==0: r a i s e V a l u e E r r o r ( " e r r e u r : F I l e v i d e " ) i f idx< l e n ( s e l f. f i l e ) or idx>=l e n ( s e l f. f i l e ) : r a i s e V a l u e E r r o r ( " e r r e u r : e l e m e n t d e p i l e a d e p i l e r n e x i s t e p a s " ) return s e l f. f i l e. pop ( i d x ) #L e c t u r e d un é l é m e n t de l a f i l e d i n d i c e donné def e l e m e n t ( s e l f, i d x = 1) : i f idx< l e n ( s e l f. f i l e ) or idx>=l e n ( s e l f. f i l e ) : r a i s e V a l u e E r r o r ( " e r r e u r : l e l e m e n t n e x i s t e p a s " ) return s e l f. f i l e [ i d x ] #E x t r a c t i o n d é l é m e n t s e n t r e deux i n d i c e s donnés ( y c o m p r i s c o p i e t o t a l e ) def c o p i e f i l e ( s e l f, imin =0, imax=none ) : i f imax==none : imax=l e n ( s e l f. f i l e ) i f imin <0 or imax>l e n ( s e l f. f i l e ) or imin>=imax : r a i s e V a l u e E r r o r ( " e r r e u r : m a u v a i s i n d i c e p o u r l e x t r a c t i o n " ) return l i s t ( s e l f. f i l e [ imin : imax ] ) #t e s t d une f i l e v i d e def f i l e v i d e ( s e l f ) : return l e n ( s e l f. f i l e )==0 #t e s t d une f i l e p l e i n e def f i l e p l e i n e ( s e l f ) : return s e l f. m a x f i l e!=none and l e n ( s e l f. f i l e )==s e l f. m a x f i l e #Retourne l a t a i l l e de l a f i l e def t a i l l e ( s e l f ) : return l e n ( s e l f. f i l e ) ######################################################################## # e x e m p l e s d u t i l i s a t i o n ########################################################################m a f i l e=mafile ( ) p r i n t m a f i l e. f i l e v i d e ( ) # a f f i c h e True m a f i l e. e n t r e r ( A ) #f a i t e n t r e r l e c a r a c t è r e A m a f i l e. e n t r e r ( 5 ) #f a i t e n t r e r l e c h i f f r e 5 m a f i l e. e n t r e r ( [ m o t 1, m o t 2, m o t 3 ] ) # Empile d e s mots p r i n t m a f i l e. c o p i e f i l e ( ) # A f f i c h e [ [ mot1, mot2, mot3 ], 5, A ] z=m a f i l e. s o r t i r ( ) p r i n t z # A f f i c h e A p r i n t m a f i l e. c o p i e p i l e ( ) # A f f i c h e [ [ mot1, mot2, mot3 ], 5 ] p r i n t m a f i l e. t a i l l e ( ) # A f f i c h e 2 p r i n t m a f i l e. element ( ) # A f f i c h e 5 Listing 4 création et manipulation d une file 3 Récursivité Un algorithme de résolution d un problème P sur une donnée a est dit récursif si parmi les opérations utilisées pour le résoudre, on trouve une résolution du même problème P sur une donnée b. Dans un algorithme récursif, on nomme appel récursif toute étape de l algorithme résolvant le même problème sur une autre donnée Pour définir un algorithme récursif, il faut se doter de deux outils : 1. une expression où un appel de la fonction est réalisé à l intérieur de la fonction elle même 4
2. une condition d arrêt, permettant aux appels successifs de stopper. 3.1 Premier exemple : calcul de la factorielle Le code 5 présente un exemple introductif classique de la récursivité, à savoir le calcul de la factorielle d un entier. Puisque n! = n(n 1)! et puisque 1! = 1, nous disposons des deux outils précédemment cités pour construire de manière récursive cette fonction. Dans ce code, les deux impressions intermédiaires des résultats permettent de comprendre comment la récursivité fonctionne (figure 3) : la fonction s appelle elle même, avec des n décroissants, jusqu à ce que n arrive à la condition d arrêt, puis les résultats sont dépilés jusqu à arriver au dernier appel. def f a c t o r i e l l e ( n ) : p r int ( " A p p e l d e f a c t o r i e l d e " + s t r ( n ) ) i f n == 1 : return 1 e l s e : r e s = n f a c t o r i e l l e ( n 1) print ( " r e s u l t a t i n t e r m e d i a i r e p o u r ", n, " * f a c t o r i e l l e ( ", n 1, " ) : ", r e s ) return r e s p r i n t ( f a c t o r i e l l e ( 5 ) ) Listing 5 Calcul récursif de la factorielle Figure 3 Trace de l execution du code 5 3.2 Deuxième exemple : la suite de Fibonacci La suite de Fibonacci est donnée par la relation de récurrence F n = F n 1 + F n 2 avec F 0 = 0, F 1 = 1. Cette relation de récurrence est immédiatement transposable en une fonction récursive Python, donnée dans le code 6. 5
def f i b ( n ) : i f n == 0 : return 0 e l i f n == 1 : return 1 e l s e : return f i b ( n 1) + f i b ( n 2) Listing 6 Calcul récursif des termes de la suite de Fibonacci 3.3 Types de récursivité Il existe plusieurs types de récursivité : récursivité simple : un algorithme récursif est simple ou linéaire si chaque cas se résout en au plus un appel récursif. Le calcul de la factorielle en est un exemple récursivité multiple : un algorithme récursif est multiple si l un des cas qu il distingue se résout avec plusieurs appels récursifs. Si "plusieurs=2" on parle de récursivité binaire. Les tours de Hanoï est un problème qui se résout en récursivité binaire. récursivité croisée : deux algorithmes sont mutuellement récursifs si l un fait appel à l autre et l autre fait appel à l un. On parle aussi de récursivité croisée. récursivité terminale : un algorithme est récursif terminal si l appel récursif est la dernière instruction de la fonction. La valeur retournée est directement obtenue par un appel récursif récursivité imbriquée : Un algorithme est récursif imbriqué si l appel récursif contient lui aussi un appel récursif. La fonction d Akermann en est un exemple : n + 1 si m = 0 A(m, n) = A(m 1, 1) si m > 0 et n = 0 A(m 1, A(m, n 1)) si m > 0 et n > 0. 3.4 Récursivité et temps de calcul Reprenons le second exemple : il est également facile de réaliser la version itérative du calcul des termes de la suite, comme démontré dans le code 7 def f i b o n a c c i _ I t e r a t i f ( n ) : a, b = 0, 1 f o r i in r a n g e ( n ) : a, b = b, a + b return a Listing 7 Calcul itératif des termes de la suite de Fibonacci En comparant les temps de calcul des fonctions fibonacci_recursif et fibonacci_iteratif, on se rend compte (figure 4) que la version itérative est bien plus rapide que la version récursive. Par exemple, la version itérative calcule le quarantième terme en 0.016 millisecondes, alors que la version récursive est 13 millions de fois plus lente.. L analyse de l algorithme récursif fait apparaître que de nombreux appels apparaissent plusieurs fois (figure 5), et que les calculs s effectuent donc inutilement plusieurs fois (par exemple le sousarbre f(2) apparaît 3 fois). Il est alors intéressant de pouvoir mémoriser les valeurs calculées pour réduire d autant le temps de calcul. (code 8. Dans ce cas, la version récursive devient plus rapide, d autant plus que n est grand (figure 6) 6
Figure 4 Temps de calculs itératif et récursif pour la suite de Fibonacci d i c o = { 0 : 0, 1 : 1 } def fibm ( n ) : i f not n in d i c o : d i c o [ n ] = fibm ( n 1) + fibm ( n 2) return d i c o [ n ] Listing 8 Calcul récursif des termes de la suite de Fibonacci - calcul avec mémoire Plus généralement, la récursivité peut être extrêmement longue (il n est pas toujours possible de trouver des solutions comme précédemment). Il faut donc être très prudent quant à son application. Néanmoins, la récursivité est un moyen naturel de résolution de certains problèmes. Tout algorithme peut s exprimer de manière récursive mais il ne faut pas se lancer tête baissée dans l écriture d une fonction/procédure récursive. 7
Figure 5 Arbre des appels récursifs Figure 6 Temps de calculs itératif et récursif pour la suite de Fibonacci - version avec mémoire 8