• Aucun résultat trouvé

IStructuresrécursives Récursivité

N/A
N/A
Protected

Academic year: 2022

Partager "IStructuresrécursives Récursivité"

Copied!
10
0
0

Texte intégral

(1)

Récursivité

Lycée Berthollet, MP/MP* 2021-22

I Structures récursives

Définition 1 Une structure est diterécursivelorsque la description de cette structure fait réfé- rence à la structure elle-même.

Exemple 2 En calcul des propositions (logique), on dit qu’une expression est uneproposition si :

— soit c’est une variable logique (A,B...) ;

— soit elle est du typenon(P), oùPest uneproposition;

— soit elle est du type(PetQ),(PouQ)ou(P=⇒Q), oùPestQsont deuxpropositions.

Exemple 3 Les fractals sont des objets mathématiques “autosimilaires” qui peuvent être décrits de manière récursive. Voir un exemple dans la suite.

Exemple 4 La notion de “mise en abyme” en littérature ou au cinéma est une sorte de récursi- vité. Voir par exemple la publicité de la “vache-qui-rit”.

Enfin, ce qui nous préoccupe ici est la notion d’algorithme récursif :

Définition 5 Dans un langage informatique, une fonction est dite récursive, si elle s’appelle elle-même soit directement, soit par le biais d’une autre fonction.

Beaucoup de langages modernes permettent de gérer les fonctions récursives. C’est le cas du langage Python. Cette gestion nécessite de gérer les paramètres, variables locales et points de retour de chaque appel de la fonction dans une ou plusieurspiles, comme nous le verrons dans un exemple simple. Voici un exemple de fonction récursive en pseudo-code, qui calcule le n-ième terme de la suite(un)n∈Ndéfinie paru0=0 et la relationun+1=√

1+un:

fonction u(n):

si n==0, retourner 0

sinon, retourner sqrt(1+u(n-1))

(2)

II Fonctions récursives en Python

Voir le fichierC02-recursivite.pyen annexe

1 Factorielle

On programme dans un premier temps la factorielle de manière récursive et on observe la pile des paramètres ainsi que la pile des appels. On remarque que le nombre d’appels imbriqués est limité en Python (par défaut à 1000). On programme aussi cette factorielle de manière non récursive et on remarque que l’ordre de complexité est inchangé (linéaire dans les deux cas), mais que la fonction non récursive est environ 2 fois plus rapide.

2 Puissances

On programme le calcul des puissances entières positives d’un nombre de manière récursive (linéaire en l’exposantn), puis de manière récursive et rapide (logarithmique en l’exposantn).

3 PGCD

L’algorithme d’Euclide se prête très bien à la récursivité avec une complexité dont on pour- rait montrer qu’elle est au pire logarithmique.

4 Fibonacci

Le calcul naïf de la suite de Fibonacci de manière récursive est d’une complexité catastro- phique, alors qu’un algorithme itératif est très facile à écrire et efficace (linéaire). On montre comment faire un calcul récursif linéaire, mais cela reste sensiblement plus lent que le calcul itératif précédent.

On représentera au tableau l’arbre des appels pour avoir une idée de la complexité de l’al- gorithme naïf.

5 Permutations de séquences

On montre comment créer la liste de toutes les permutations d’une séquence donnée (liste ou chaîne de caractères) de manière récursive (ce qui est beaucoup plus délicat de manière non récursive). On mettra la complexité en lien avec l’arbre des appels.

6 Flocon de neige

Il est possible de dessiner assez facilement certaines courbes fractales à l’aide de la récur- sivité et du module graphiqueturtle. On donne l’exemple archi-classique du flocon de neige de Van Koch.

(3)

III Correction, terminaison, complexité

Sur certains des exemples précédents, on voit que la récurrence (normale ou forte) est l’ins- trument adapté aux preuves de correction dans le cas récursif, tandis que la terminaison repose sur la stricte décroissance de la taille du ou des paramètres des appels successifs. On donne aussi des exemples de calcul de complexité.

1 Factorielle

Terminaison

On considère la fonction Pythonfactdu fichierC02-recursivite.pyen annexe. Lors des appels successifs imbriqués provoqués par l’appelfact(n), les valeurs prises par les arguments nsuccessifs forment une suite d’entiers positifs ou nuls strictement décroissante. Cette suite est donc finie et le nombre d’appels imbriqués est fini. Donc l’appel de la fonction factsur un entier positif ou nul termine toujours.

Complexités

Il est évident que la suite précédente est n,n−1, ...,1,0. Il y a donc n+1 appels imbri- qués. Excepté l’appel récursif, le code de chaque appel nécessite un test et une multiplication, sauf le dernier appel (argument 0) qui nécessite seulement un test. Si on fait l’approximation que le temps d’exécution d’une multiplication est constant (i.e.si on travaille avec des entiers standard, codés sur 32 ou 64 bits), on en déduit que l’exécution de l’appel fact(n) s’effec- tue en un temps de l’ordre de n, i.e. la complexité temporelle est linéaire. Cependant, cette approximation est inappropriée, puisque la croissance de la fonction factorielle est très forte.

Par exemple, 100 ! s’écrit avec 525 chiffres en base 2, donc n’est pas un entier standard. Il est donc exclu, même pour des entiers n petits, de considérer que les résultats intermédiaires sont des entiers standard. Les multiplications ne se font alors pas en temps constant. En parti- culier, il faut au moins calculer tous les chiffres (dans la base 2 de codage) du résultat. Si on reste dans le cas où les nombres k≤n sont des entiers standard, une multiplication de k par (k−1)! s’exécute alors en un temps proportionnel à log(k!), d’où une complexité de l’ordre de ∑nk=1log(k!) =log(∏nk=1k!). En particulier, la complexité est d’ordre supérieur ou égal à Θ(log(n!)). En utilisant la formule de Stirling, une composition d’équivalents dans un loga- rithme (autorisé) et le fait que log(n) =o(n),Θ(log(n!)) =Θ(nlog(n)), ce qui est strictement plus que linéaire. La complexité temporelle est donc strictement sur-linéaire si on considère des entiersntels quen! de puisse pas se coder de manière standard.

En ce qui concerne la complexité spatiale, elle repose sur la pile des environnements locaux des appels imbriqués d’une part et le stockage du résultat intermédiaire d’autre part. Pour la pile des appels, si l’entierninitial est standard, le stockage de chaque valeur locale de nprend une place constante et la pile des appels necessite une place mémoire linéaire en n. En revanche, pour la même raison que ci-dessus, la taille de stockage du résultat intermédiaire courant est maximale pour le dernier retour et de l’ordre de log(n!), soit en Θ(nlog(n)). La complexité spatiale de la fonctionfactest donc non linéaire, même si on considère que les entiers inférieurs ou égaux à l’argument initial ont tous la même taille de stockage.

(4)

Correction

Pourn∈Non définit l’assertion de récurrence suivante :

A

n: “En notantnle nom d’un entier Python représentant l’entier natureln, l’appel fact(n)retourne un entier Python représentant l’entier natureln!.”

On fait alors une démonstration par récurrence surn:

Initialisation : Sin=0, le test de l’appelfact(0)est positif donc la valeur retournée est 1=0!.

Héridité : Supposons que

A

n−1 soit vraie pour un certain n≥1. Lors de l’appel fact(n), le test est négatif, donc la valeur retournée est nfois la valeur retournée par fact(n-1), qui est (n−1)! par hypothèse de récurrence. Commen·(n−1)!=n!,

A

nest vraie.

Par le principe de récurrence, on a donc montré que, pour tout n∈N, l’appel fact(n) retourne un entier représentantn!.

2 Puissance rapide

Terminaison

On considère la fonction Python puissRapide du fichier C02-recursivite.py en an- nexe. Lors des appels successifs imbriqués provoqués par l’appelpuissRapide(x,n), les va- leurs prises par les arguments n successifs forment une suite d’entiers positifs ou nuls stric- tement décroissante, puisque, pour tout n∈N?, le quotient q de la division euclidienne de n par 2 est strictement plus petit que n, car, en notantr le reste de cette division,q≤ 2q+r2 = n2. Cette suite est donc finie et le nombre d’appels imbriqués est fini. Donc l’exécution de l’appel puissRapide(x,n)termine toujours.

Complexités

Pour simplifier les choses, on suppose ici que l’argument x est un flottant et quen est un entier standard (au sens qu’il peut être codé en 32 ou 64 bits, suivant la machine), ce qui entraîne que les opérations arithmétiques se font en temps constant.

On noten=n0,n1, ...,nk=0 la suite finie ci-dessus des valeurs des arguments successifsn des appels imbriqués. Pour chaquei∈[[0,k−1]],ni+1est le quotient de la division euclidienne de ni par 2. On note ri le reste de cette division. Remarquons que le dernier reste est rk−1= nk−1−2nk=nk−1, donc est non nul et donc vaut 1. Une récurrence immédiate donne

n=r0+2n1=r0+2r1+4n2=...=

k−1 i=0

ri2i,

ce qui est l’écriture denen base 2. Commerk−1=1, 2k−1≤n<2k et donck=blog2(n)c+1.

Comme l’exécution des instructions de chaque appel de la fonction, hors l’appel récursif, se fait en temps compris entre deux constantes (un test et entre zéro et quatre opérations arithmétiques), la complexité temporelle est logarithmique.

La pile des environnements locaux des appels est de taille logarithmique en n d’après ce qu’on vient de voir, et la place mémoire de ces environnements est constante (un flottant et un entier standard), donc la taille mémoire de la pile est enΘ(log(n)). Par ailleurs, le résultat

(5)

Correction

On considère un réelxreprésenté par un flottantx. Pour toutn∈N, on considère l’assertion de récurrence

A

n: “En notantnle nom d’un entier Python représentant l’entier natureln, l’appel puissRapide(x,n)retourne un flottant représentant le réelxn.”

On fait alors une démonstration par récurrence forte surn:

Initialisation : Sin=0, le test de l’appelpuissRapide(x,0)est positif donc la valeur retournée est 1=x0.

Héridité : Soitn∈N?. Supposons que

A

ksoit vraie pour toutk<n. Lors de l’appel de fonction puissRapide(x,n), le premier test est négatif. On noteqle quotient etrle reste de la division de n par 2. D’après l’hypothèse de récurrence, l’appel puissRapide(x,n//2) retourne xq. Dans les deux cas du second test, la valeur retournée est alors(xq)2xr=x2q+r=xn, donc

A

nest vraie.

Par le principe de récurrence forte, on a donc montré que, pour toutn∈N, l’appel de fonc- tionpuissRapide(x,n)retourne un flottant représentant le réelxn.

IV Conclusion

Au vu des exemples ci-dessus, on tire quelques enseignements.

Quand utiliser la récursivité ?

— Lorsqu’on ne sait pas faire autrement ;

— Lorsqu’on travaille sur des structures elle-même récursives (listes, arbres, fractales,...) ;

— Lorsque l’algorithme récursif est très élégant, et ne change pas l’ordre de grandeur de la complexité par rapport à un algorithme itératif. Attention cependant à la limite du nombre d’appels imbriqués !

Quand ne pas utiliser la récursivité ?

— Lorsqu’il existe un algorithme non-récursif raisonnablement simple et élégant faisant la même chose ;

— Lorsque l’arbre des appels est potentiellement très profond ;

— S’il y a des paramètres ou des variables locales “volumineux” dans la fonction.

(6)

V Annexe : code

# ---

# Mesure du temps d’execution d’une fonction

# --- import time

def teste(appel):

print(appel+’ ---> ’,end=’’) tInit = time.clock()

res = eval(appel) tFin = time.clock() print(res,tFin-tInit)

# ---

# Factorielle

# --- def facto(n):

if n==0:

return 1 else:

return n*facto(n-1) print(facto(10))

# produit une erreur:

# print(facto(1000))

# Visualisation de la pile des variables locales et des appels from inspect import *

def factoVisu(n):

print(locals()) if n==0:

pile = stack()

for i in range(len(pile)):

print(getframeinfo(pile[i][0])) return 1

else:

res = n*factoVisu(n-1) print(locals())

return res

(7)

# Comparaison avec la version non recursive def factoIter(n):

res = 1

for i in range(2,n+1):

res *= i return res

teste(’factoIter(900)’) teste(’facto(900)’) print(factoIter(10000))

# ---

# Puissance

# --- def puiss(x,n):

""" Calcule x**n de facon recursive """

if n==0:

return 1 else:

return x*puiss(x,n-1) print(puiss(2,32))

def puissRapide(x,n):

""" Calcule rapidement x**n de facon recursive """

if n==0:

return 1 else:

y =puissRapide(x,n//2) if (n%2==0):

return y*y else:

return x*y*y teste(’puissRapide(3,900)’) teste(’puiss(3,900)’)

(8)

# ---

# PGCD

# --- def pgcd(a,b):

if b==0:

return a else:

return pgcd(b,a%b) print(pgcd(10,6))

print(pgcd(1235674567543467567,908765345432342453243234254254233212356453456))

# ---

# Fibonacci

# --- def fibo(n):

if n<2:

return n else:

return fibo(n-1)+fibo(n-2) for i in range(10):

print(fibo(i),end=’ ’) print(fibo(30))

print(fibo(35)) def fiboIter(n):

if n<2:

return n else:

Fnm1,Fn = 0,1

for i in range(n-1):

Fnm1,Fn = Fn,Fnm1+Fn return Fn

for i in range(10):

print(fiboIter(i),end=’ ’) print(fiboIter(35))

print(fiboIter(100000) def fiboRecBis(n):

""" Retourne F(n-1) et Fn """

(9)

elif n==1:

return 0,1 else:

Fnm2,Fnm1 = fiboRecBis(n-1) return Fnm1,Fnm2+Fnm1

print(fiboRecBis(35)[1])

# Comparaison recursif/iteratif teste(’fiboIter(900)’)

teste(’fiboRecBis(900)[1]’)

# ---

# Permutations

# --- def permChaine(ch):

if len(ch)==1:

return [ch]

else:

res = []

for i,car in enumerate(ch):

res += [car+s for s in permChaine(ch[:i]+ch[i+1:])]

return res

print(permChaine(’abc’)) print(permChaine(’abricot’)) def permListe(liste):

if len(liste)==1:

return [liste]

else:

res = []

for i,item in enumerate(liste):

res += [[item]+s for s in permListe(liste[:i]+liste[i+1:])]

return res

print(permListe(list(’abc’)))

(10)

# ---

# Flocon

# --- from math import *

from turtle import * def fractTurtle(n):

if n==0:

forward(1) else:

fractTurtle(n-1) right(60)

fractTurtle(n-1) left(120)

fractTurtle(n-1) right(60)

fractTurtle(n-1) def floconTurtle(n):

clearscreen() speed(’fastest’)

# hideturtle() getpen()

setheading(240) fractTurtle(n) left(120) fractTurtle(n) left(120) fractTurtle(n) floconTurtle(5) exitonclick()

# ---

# Arbres et tris seront vus plus tard

# ---

Références

Documents relatifs

→ identifier un espace un espace où les hommes comme les activités tendent à se concentrer. où les hommes comme les activités tendent à se concentrer. pris en compte dans les

1) Quels sont les handicaps ou les inégalités cités dans ce texte ? 2) Souligne la partie du texte où est évoqué le chômage. 1er — Il est institué une organisation de

1) Quels sont les handicaps ou les inégalités cités dans ce texte ? 2) Souligne la partie du texte où est évoqué le chômage. 1er — Il est institué une organisation de

Ecrire une fonction Eq_Som_cube prenant pour argument un entier naturel n et qui renvoie la liste des entiers naturels inférieurs ou égaux à n et qui sont égaux à la somme des cubes

Ecrire le système en (u 1 , u 2 ) obtenu en appliquant la condition de maximisation du principe du maxi- mum de Pontryagin, et le résoudre sous une condition qu’on

Si aucun des problèmes (a) ou (b) n'a été résolu, les ajouter à la liste, dans un ordre à choisir, et aller en 2 pour' une nouvelle étape; dans le cas où les problèmes (a) et (b)

Une suite définie à l’aide de son premier terme et d’une relation entre des termes consécutifs est appelée « suite récursive ».. On dit d’une telle suite qu’elle est

Je n’ai pas suffisamment lu le récent livre de Laurent Lafforgue Géométrie plane et algèbre aux éditions Hermann mais le parcourir m’a sûrement donné une petite impulsion, à